Теория
Строка приходит из файла
В заданиях этого урока в файле находится длинная строка: набор букв, цифр и знаков, записанный подряд. Иногда весь текст занимает одну строку, иногда в условии описаны несколько строк. Не нужно заранее соединять или разбивать данные: способ хранения будет указан в конкретной задаче.
Строка в Python — это упорядоченная запись символов. Символом будем называть один знак текста: букву, цифру, минус или другой знак. Например, строка АБА-В содержит пять символов, и их порядок важен. Пока мы не преобразовали цифры в числа, значения вроде 7 и 0 остаются знаками текста. Для поиска фрагментов это удобно: мы сравниваем последовательности знаков.
Файл открывают через open, а read() получает его содержимое как одну строку. Метод strip() убирает пробельные символы, например пробелы и переводы строк, только по краям; символы в середине он сохраняет. В этом примере StringIO изображает текстовый файл в памяти, чтобы код можно было сразу запустить. С настоящим файлом чтение устроено так же. Открытие и чтение файла подробно разобраны в уроке мини-курса Python «Читаем данные из файла».
from io import StringIO with StringIO("АБА-В\n") as file: text = file.read().strip() print(text) # АБА-ВЕсли в файле записано АБА-В и перевод строки после него, переменная text получит строку с пятью символами. Пробел или перевод строки внутри содержимого останется на месте: удаляются только пробельные символы по краям.
- read() возвращает текст вместе с переводом строки.
- strip() убирает перевод строки на краю.
- Пробел между КОД и 24 остаётся, поэтому итоговая строка — КОД 24.
Позиции и непрерывные фрагменты
Чтобы проверить, что стоит в определённом месте, пронумеруем символы слева направо. Номер места называется индексом. В Python первая позиция имеет индекс 0, следующая — 1 и так далее. В строке АБВГД буквы стоят на индексах 0, 1, 2, 3 и 4.
Несколько символов, стоящих подряд, образуют непрерывный фрагмент. Например, БВГ — фрагмент строки АБВГД, а БГ — нет: между буквами есть В. В задаче о фрагментах нельзя перескакивать через символы или соединять начало строки с её концом.
Взять фрагмент по границам можно срезом. Запись text[start:end] включает символ с индексом start, но не включает символ с индексом end. Поэтому text[1:4] вернёт БВГ: попадут индексы 1, 2 и 3, а индекс 4 будет правой границей. Индексы и срезы подробнее объясняются в опубликованном уроке Python «Строки: символы, индексы и срезы».
text = "АБВГД"fragment = text[1:4]print(fragment) # БВГ- Фрагменту нужны три позиции: начало, следующая и ещё одна.
- С индекса 3 он займёт позиции 3, 4 и 5 — это ГДЕ.
- С индекса 4 останутся только две позиции. Последнее допустимое начало — индекс 3.
Соседние символы и перекрытия
Иногда условие относится к двум символам рядом, например к буквам АА. Проверить нужно каждое место, где пара может начаться. В строке длины 5 начала соседней пары — индексы 0, 1, 2 и 3: у каждого справа есть ещё один символ.
Два найденных вхождения могут перекрываться, то есть использовать общий символ. В строке ААА пара АА начинается сначала на индексе 0, затем на индексе 1. Вхождения занимают позиции 0–1 и 1–2; средняя буква участвует в обоих. Это два вхождения, а не одно.
Запись range(len(text) - 1) даёт все возможные начала пары: правая граница range не включается, поэтому последним началом становится предпоследняя позиция. Функция len(text) сообщает число символов в строке, а цикл повторяет проверку для каждого индекса. Запись count += 1 прибавляет единицу к счётчику. Границы цикла подробно разбираются в уроке Python «for и range».
Слово if начинает проверку: команды внутри отступа выполняются, только когда пара совпала с образцом. Сравнения и ветвление можно повторить в опубликованном уроке Python «Условия: сравнения и выбор из двух вариантов».
text = "ААА"pattern = "АА"count = 0 for i in range(len(text) - 1): if text[i:i + 2] == pattern: count += 1 print(count) # 2- Начало на индексе 0 даёт БА — это не АА.
- Начало на индексе 1 даёт АА — первое вхождение.
- Начало на индексе 2 тоже даёт АА. Вхождения перекрываются, поэтому ответ равен 2.
Самый длинный подходящий фрагмент
Представь длинную запись, где одни символы разрешены, а другие прерывают подходящий участок. Например, разрешена только буква А. В строке ААXААА первый участок имеет длину 2, второй — длину 3. Буквы по обе стороны от X нельзя соединить: фрагмент должен быть непрерывным.
Чтобы найти самый длинный участок, достаточно идти слева направо и держать две длины. current — сколько разрешённых символов подряд заканчивается в текущем месте; best — наибольшая длина, которую мы уже встретили. Проверка char in allowed отвечает, входит ли текущий символ в список разрешённых. Разрешённый символ увеличивает текущую длину, запрещённый обнуляет её. После каждого разрешённого символа команда max(a, b) оставляет большее из двух значений. Это постепенный выбор результата, знакомый по уроку Python «Отбор результата».
text = "ААXААА"allowed = "А"current = 0best = 0 for char in text: if char in allowed: current += 1 best = max(best, current) else: current = 0 print(best) # 3- После первой А current = 1 и best = 1.
- После второй А текущая длина 2, и лучший результат тоже становится 2.
- Символ X запрещён: current сбрасывается в 0, но best остаётся равен 2.
- Последние три А дают длины 1, 2 и 3. В конце best = 3.
current, но сохраняй уже найденное значение best.Ограничение на число вхождений
В следующем варианте нужно найти самый длинный фрагмент, внутри которого выбранная пара встречается не больше заданного числа раз. «Не более двух» разрешает ноль, одно или два вхождения; третье уже нарушает ограничение. Каждое вхождение пары по-прежнему считаем с перекрытием.
Для этой задачи удобно двигать левую и правую границы окна. Окно — это рассматриваемый непрерывный фрагмент. Мы расширяем его справа на один символ; если число вхождений стало слишком большим, передвигаем левую границу вправо, пока условие снова не выполнится. Окно остаётся непрерывным.
При добавлении правого символа новая пара могла только закончиться на нём. При удалении символа слева проверяем, начиналась ли там пара; если да, уменьшаем счётчик. Это позволяет учитывать перекрывающиеся пары и не пересчитывать весь фрагмент после каждого шага.
Повторять сдвиг слева будем циклом while: он выполняет команды, пока число вхождений превышает предел. Подробный разбор этого цикла есть в опубликованном уроке Python «while: повторяем, пока условие верно».
text = "ААААБААА"pattern = "АА"limit = 2left = 0occurrences = 0best = 0 for right in range(len(text)): if right > 0 and text[right - 1:right + 1] == pattern: occurrences += 1 while occurrences > limit: if text[left:left + 2] == pattern: occurrences -= 1 left += 1 best = max(best, right - left + 1) print(best) # 5Здесь right — индекс только что добавленного символа, а left — индекс первого символа окна. Его длина равна right - left + 1, потому что обе границы входят в него. Левая граница может сдвинуться несколько раз; цикл остановится, когда вхождений снова не больше двух.
- Во всей строке пары начинаются на индексах 0, 1, 2, 5 и 6: всего пять.
- Сдвигая левую границу с индекса 0 на 1, затем на 2, убираем пары с началами 0 и 1. Остаются три пары: с началами 2, 5 и 6.
- Сдвиг до индекса 3 удаляет ещё одну пару. Окно становится АБААА; в нём пары начинаются на индексах 5 и 6, их ровно две.
- Окно длины 6 с началом 1 или 2 содержит три пары, поэтому подходящего окна длиннее пяти здесь нет.
Из каких частей состоит выражение
Теперь строка содержит запись, похожую на арифметику. Сначала договоримся, какие записи в задачах этого урока считаются допустимыми. Точное правило состава выражения называется грамматикой. Здесь это не правила русского языка, а перечень частей и порядка, в котором они могут стоять.
Число — либо одиночный ноль, либо запись, которая начинается с цифры 6, 7, 8 или 9 и затем может содержать цифры 0, 6, 7, 8 или 9. Поэтому 0 и 760 — числа, а 07 — нет: ноль может быть числом сам по себе, но не началом многозначной записи.
Между числами стоит ровно один знак: вычитание - или умножение *. Это бинарные операторы: каждый соединяет число слева и число справа. Поэтому 7-0 допустимо, а -0 в начале строки — нет: перед минусом нет числа. Выражение не может закончиться оператором.
examples = ["0", "7-0", "760*8", "07", "-0", "7--8", "7*"]valid = ["0", "7-0", "760*8"]for expression in examples: print(expression, expression in valid)- В 8-0 сначала число 8, затем бинарный минус, затем число 0. Запись допустима.
- В 07*6 число 07 начинается с нуля и имеет ещё одну цифру. Запись запрещена.
- В 7--8 между числами стоят два знака подряд. Между числами должен быть ровно один оператор.
Для проверки иди по ожидаемому порядку: число, оператор, число, оператор, число. Бинарный означает, что у знака есть два соседа — левое и правое число. Подробный разбор арифметических выражений, типов чисел и операций есть в опубликованном уроке Python «Числа, типы и арифметические выражения»; здесь дополнительно проверяется допустимая запись цифр.
Найти длинное выражение за один просмотр
В большом файле не нужно вырезать и проверять каждый возможный фрагмент: их число быстро растёт. Вместо этого читаем строку слева направо и следим, может ли текущий символ продолжить уже начатое выражение. Фрагмент, который сейчас проверяется, будем называть кандидатом. Когда правило нарушается, незаконченный кандидат сбрасывается. Если на месте ошибки начинается новое число, поиск начинается с него.
Достаточно помнить два признака: ждём ли мы число и состоит ли текущее число только из нуля. После знака ожидается число. Если пришёл оператор или другой посторонний символ, незаконченная часть не подходит. Ноль завершает самостоятельное число, поэтому сразу следующая цифра не может продолжить его: при записи 07 нужно отбросить ноль и начать новый кандидат с цифры 7.
Знак оператора не является окончанием выражения: справа ещё должно встретиться число. Поэтому длину результата обновляем после каждой цифры, которая завершает число. Кандидат 7- имеет длину 2, но запись 7- не является завершённым выражением; число 7 до оператора уже остаётся допустимым выражением длины 1.
В этом проходе нужны и сам символ, и его позиция. Запись enumerate(text) при каждом шаге цикла даёт сразу оба значения: i — индекс символа, а char — символ на этой позиции. Так мы сможем измерить фрагмент от его начала до текущей цифры.
text = "x7-0*68z07-6*8!"digits = "06789"operators = "-*"expect_number = Truezero_only = Falsestart = Nonebest = 0 for i, char in enumerate(text): if char in digits: if start is None: start = i expect_number = False zero_only = char == "0" best = max(best, 1) elif expect_number: # Продолжаем выражение после оператора. expect_number = False zero_only = char == "0" best = max(best, i - start + 1) elif zero_only: # После отдельного нуля нельзя дописать цифру. start = i zero_only = char == "0" best = max(best, 1) else: best = max(best, i - start + 1) elif (char in operators and start is not None and not expect_number): expect_number = True zero_only = False else: start = None expect_number = True zero_only = False print(best) # 6Переменная start хранит начало текущего кандидата, а признаки говорят, какую часть мы ожидаем. После ошибки новая цифра может начать новый кандидат. Каждый символ рассматривается один раз, поэтому время работы растёт вместе с длиной строки. Такое время называют линейным. Повторить состояние цикла и счётчики можно в опубликованном уроке Python о счётчиках и накопителях.
- Буква x не подходит. Цифра 7 начинает новое число.
- Минус требует следующее число; ноль завершает его.
- Умножение требует ещё одно число. Цифры 6 и 8 завершают запись.
- Буква z прерывает кандидат. В выражении 7-0*68 шесть символов.
7-. Сохраняй длину только после числа, а при проверке вручную отдельно посмотри на начало и конец каждого найденного кандидата.Независимо проверить ответ
Большая программа может закончиться без ошибки и всё же пропустить последний символ или принять неверную запись. Поэтому сначала проверяют смысл: на короткой строке выписывают кандидаты и сравнивают их с тем, что нашёл код.
Для независимой проверки можно написать простой перебор: взять каждую пару границ, получить срез и отдельно проверить его целиком. На коротком тексте такой способ медленный, зато в нём легко увидеть все варианты. Основной алгоритм проходит файл один раз; полный перебор нужен только как проверка на маленьких данных.
- Проверь содержимое. Уточни, одна ли строка читается и какие символы разрешены условием.
- Разметь позиции. На короткой строке подпиши индексы и перечисли непрерывные кандидаты.
- Проверь правило. Отдельно проверь символы, соседние пары и границы выражения.
- Сравни способы. Убедись, что простой перебор и быстрый проход дают одинаковую длину.
- Проверь край. Добавь примеры, где ответ начинается в начале или заканчивается последним символом.
Проверку одного фрагмента удобно назвать и вызывать снова для другого. Такая именованная часть программы называется функцией. Запись def is_expression(candidate): задаёт функцию, которая получает проверяемый фрагмент в переменной candidate. Команда return завершает проверку и возвращает True, если запись подходит, или False, если правило нарушено. Создание и вызов функций подробно разбираются в уроке Python «Функции: параметры и возвращаемый результат».
def is_expression(candidate): digits = "06789" operators = "-*" size = len(candidate) if not candidate: return False if candidate[0] in operators or candidate[-1] in operators: return False expect_number = True i = 0 while i < size: if expect_number: if candidate[i] == "0": i += 1 if i < size and candidate[i] in digits: return False elif candidate[i] in "6789": i += 1 while i < size and candidate[i] in digits: i += 1 else: return False expect_number = False else: if candidate[i] not in operators: return False i += 1 expect_number = True return not expect_number print(is_expression("7-0")) # Trueprint(is_expression("07-6")) # False- Начать можно с 7. Кандидат 7-0 имеет длину 3 и заканчивается числом.
- После знака умножения запись 7-0* ещё не закончена: справа пока нет числа.
- После цифр 68 получается 7-0*68. Все части стоят в нужном порядке, длина равна 6.
- Буква z уже не относится к выражению. Самый длинный завершённый кандидат имеет длину 6.
Сверяй именно то, что просит условие: длину фрагмента, число вхождений или значение выражения. В этом уроке ищется длина, а не результат арифметического вычисления. Приём выбора ответа можно повторить в опубликованном уроке Python «Отбор результата».
На экзамене
Перед кодом сформулируй, какой результат требуется: длина одного непрерывного фрагмента, число вхождений или значение выражения. Затем определи разрешённые символы и то, что прерывает кандидата. Для арифметической записи отдельно запиши порядок: число, бинарный оператор, число; выражение должно начинаться и заканчиваться числом.
- Прочитай файл. Пойми, сколько в нём строк и нужно ли убрать крайний перевод строки.
- Определи кандидата. Это отдельный символ, пара, непрерывный участок или выражение целиком.
- Запиши условие. Назови разрешённые символы, предел числа вхождений и допустимое начало и окончание.
- Проверь короткую строку. Выпиши индексы, пересечения и крайние случаи до запуска большого файла.
- Выбери ответ. Сравни длины только завершённых подходящих фрагментов и выведи то, что просит задача.
Следи за невключаемой правой границей, перекрывающимися вхождениями и последней позицией начала фрагмента. Ноль — отдельное допустимое число, но не первая цифра многозначного; конечный оператор оставляет выражение незавершённым.
Практика
Практика временно недоступна. Можно продолжить читать теорию.
Итог
Что получилось
Теперь ты умеешь читать строку из файла, выбирать символы и непрерывные фрагменты по индексам, считать соседние вхождения с перекрытиями и находить самый длинный участок по условию. Ты также можешь проверить состав арифметической записи и пройти большой файл одним последовательным просмотром.
Если ответ вызывает сомнение, вернись к короткой строке и выпиши все начала фрагментов. Отдельно проверь правую границу, последнюю допустимую позицию, ведущий ноль и то, заканчивается ли выражение числом. Затем сравни быстрый алгоритм с простым перебором на небольших данных.
Ссылки на опубликованные уроки о файлах, строках и срезах, цикле for, цикле while, арифметических выражениях и выборе результата напоминают отдельные приёмы. Для этой темы не нужно заранее проходить весь курс: открывай ссылку там, где хочется повторить конкретное действие.