Нахождение самой длинной подцепочки в текстовом файле с помощью 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 шт):

Автор решения: A1pha

В общем, я понял, в чём дело: функция findall не находит все подстроки разом, а сначала находит, записывает и более не учитывает, из-за этого и получается неверный ответ.

Если кто-то сможет довести это до ума, буду признателен. Мне кажется, что таким способом не получится решить данную задачу.

→ Ссылка
Автор решения: Fox Fox

Готовый консольный скрипт для тестирования (для строки 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")
→ Ссылка
Автор решения: Stanislav Volodarskiy

Машина регулярных выражений не выдаёт перекрывающиеся совпадения. То есть, будут просмотрены не все искомые подстроки, а только каждая четвёртая. Простого способа исправить ситуацию нет.

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)
→ Ссылка
Автор решения: Vladimir Bogdanov

За счет небольшого усложнения генератора и затрат памяти на очередь, размер которой не превысит количества искомых 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))
→ Ссылка