Нахождение самой длинной подцепочки в текстовом файле с помощью re
Условие задачи такое:
В текстовом файле находится цепочка из символов, в которую могут входить заглавные буквы латинского алфавита A…Z. Найдите длину самой длинной подцепочки, в которой есть ровно три буквы Z (не обязательно стоящие рядом).
Ко мне пришла идея решить её через библиотеку re:
import re
with open("text.txt") as file:
string = file.read()
substrings = re.findall(r"[^Z]*Z" * 3 + r"[^Z]*", string)
print(len(max(substrings, key=len)))
Но ответ не сходится: правильный — 338, код выводит 297. Можете подсказать, в чём ошибка?
Ответы (4 шт):
В общем, я понял, в чём дело: функция findall не находит все подстроки разом, а сначала находит, записывает и более не учитывает, из-за этого и получается неверный ответ.
Если кто-то сможет довести это до ума, буду признателен. Мне кажется, что таким способом не получится решить данную задачу.
Готовый консольный скрипт для тестирования (для строки AZBZCZDZEEE
выводит 9):
def longest_substring_with_three_z(v_file):
with open(v_file, "r") as h: v_data = h.read()
v_result = v_start = v_count = 0
for v_end, v_char in enumerate(v_data):
v_count += v_char == "Z" # Увеличиваем счетчик, если текущий символ "Z"
# Если количество "Z" больше 3, сдвигаем начало подцепочки вправо
while v_count > 3:
v_count -= v_data[v_start] == "Z" # Уменьшаем счетчик, если символ в начале подцепочки "Z"
v_start += 1 # Сдвигаем начало подцепочки вправо
# Если в подцепочке ровно 3 "Z", обновляем максимальную длину
if v_count == 3: v_result = max(v_result, v_end - v_start + 1)
return v_result
# Пример использования:
import os
print("-" * 75 + "\nДлина самой длинной подцепочки в тексте, в которой есть ровно три буквы Z:\n" + "-" * 75)
v_file = "input.txt"
if not os.path.exists(v_file): print(f"Файл {v_file} не найден в текущем каталоге!")
else: print(f"Результат: {longest_substring_with_three_z(v_file)}")
print("\nНажмите любую клавишу для продолжения...")
os.system("pause > nul" if os.name == "nt" else "read > /dev/null")
Машина регулярных выражений не выдаёт перекрывающиеся совпадения. То есть, будут просмотрены не все искомые подстроки, а только каждая четвёртая. Простого способа исправить ситуацию нет.
1
Решение без re
. Читаем файл, разбиваем на слова без 'Z', возвращаем длины этих слов. По длинам строим накопительные суммы sums
. По списку накопительных сумм пустим два итератора с интервалом в четыре позиции. Разница между итераторами – сумма длин четырёх последовательных слов. Выбираем максимум, добавляем три в честь трёх удалённых разделителей между четырьмя словами:
def cumsum(seq):
s = 0
yield s
for v in seq:
s += v
yield s
with open('24_3.txt') as f:
sums = tuple(cumsum(map(len, f.read().split('Z'))))
it1 = iter(sums)
for _ in range(4):
next(it1)
print(max(b - a for a, b in zip(sums, it1)) + 3)
Если не жалко ещё раз скопировать данные, последние четыре строки можно заменить одной:
print(max(b - a for a, b in zip(sums, sums[4:])) + 3)
2
Можно сделать решение без линейной дополнительной памяти. chars(f)
выдаёт последовательные символы из файла. zs(seq)
выдаёт позиции символов 'Z' в последовательности seq
. Кроме этого он выдаёт позиции начала (-1) и конца последовательности, как если бы она была окружена символами 'Z'. itertools.tee
берёт итератор и делает из него две копии. Под капотом он запоминает значения исходного итератора. В нашей задаче он будет помнить четыре целых числа – память константная.
По последовательности позиций бегут два итератора с интервалом в четыре позиции. Разница между итераторами – суммарная длина четырёх последовательных слов без 'Z'. Выбираем максимум, вычетаем единицу: у нас один символ 'Z' в конце строки лишний.
Это решение в константной памяти, можно обрабатывать файлы любых размеров:
import itertools
def chars(f):
while c := f.read(1):
yield c
def zs(seq):
yield -1
i = -1 # to process empty seq
for i, c in enumerate(seq):
if c == 'Z':
yield i
yield i + 1
with open('24_3.txt') as f:
it1, it2 = itertools.tee(zs(chars(f)))
for _ in range(4):
next(it2)
print(max(b - a for a, b in zip(it1, it2)) - 1)
P.S. В первой программе блок for
был вынесен из with
, потому что делалась копия файла в памяти. Во второй программе блок должен оставаться внутри.
P.P.S. itertools.tee
можно заменить в учебных целях на функцию interval(seq, k)
, которая принимает seq
и возвращает из неё пары значений на расстоянии k
друг от друга. Тогда логика вычислений превращается в однострочник: символы из файла → позиции 'Z' → интервалы позиций → максимум из длин интервалов:
def interval(seq, k):
it = iter(seq)
ring = [v for _, v in zip(range(k), it)]
i = 0
for v in it:
yield ring[i], v
ring[i] = v
i = (i + 1) % k
with open('/home/sv/Downloads/24_3.txt') as f:
print(max(b - a for a, b in interval(zs(chars(f)), 4)) - 1)
За счет небольшого усложнения генератора и затрат памяти на очередь, размер которой не превысит количества искомых Z (три в данном случае) плюс 1, получаем готовые длины цепочек, из которых нужно выбрать максимальную.
Если в последней строке поменять '(c for c in data)' на функцию chars(f) от Станислава, то все должно работать.
P.S. При желании, можно легко получить начальный и конечный индексы цепочек.
from collections import deque
data = "AZBZCZDZEEE"
char_Z = 'Z'
count_Z = 3
def sequence_length(symbol, iterable, scount):
idx_store = deque()
idx_store.append(-1)
for idx, symb in enumerate(iterable):
if symb == symbol:
if len(idx_store) - 1 == scount:
yield idx - idx_store.popleft() - 1
idx_store.append(idx)
if len(idx_store) - 1 == scount:
yield idx - idx_store.popleft()
print(max(sequence_length(char_Z, (c for c in data), count_Z), default=None))