Теория
Что нужно знать
Задание 24 — это программирование, а не ручной счёт. Почти всегда нужно прочитать одну длинную строку из файла и найти в ней подстроку с ограничением: без запрещённых букв, с заданным числом вхождений, с равным количеством двух символов или с определённой структурой. Ответ — одно число, но получить его без ошибок можно только аккуратной программой.
Файл и строка
В задании 24 данные лежат в текстовом файле. Чаще всего это одна очень длинная строка — до 10^6 символов. Читать её через input() нельзя: нужен файл. Для одной строки хватает readline(), для нескольких — read().splitlines().
- s = open('24.txt').readline() читает первую строку вместе с переводом строки.
- s = s.strip() убирает пробельные символы по краям; если важны пробелы в начале, используйте rstrip().
- Для нескольких строк: lines = open('24.txt').read().splitlines().
- Файл лучше открывать в блоке with, но на экзамене достаточно одной строки open(...).readline().
- Размер 10^6 символов означает, что алгоритм должен быть линейным или близким к линейному.
s = open('24.txt').readline().strip()lines = open('24.txt').read().splitlines()n = len(s)
Индексация и срезы
Строка в Python — последовательность символов с нумерацией от нуля. Срез s[i:j] берёт символы с i до j, не включая j. Это главный источник ошибок: длина среза равна j − i, а последний индекс — j − 1.
- s[0] — первый символ, s[-1] — последний.
- s[i:j] — с i по j-1; s[:k] — первые k; s[i:] — до конца.
- len(s[i:j]) = max(0, j - i).
- s[::-1] — перевёрнутая строка; s[i:j:k] — с шагом k.
- Для окна длины L, начинающегося в i: s[i:i+L]; последний старт — len(s)-L.
s[i:j] = s[i], …, s[j-1]len(s[i:j]) = j - iокно длины L: i от 0 до len(s)-L
Встроенные операции
Большую часть задания можно решить без ручных циклов, если правильно выбрать строковую операцию. Важно помнить, что count считает непересекающиеся вхождения, find возвращает -1 при отсутствии, а replace заменяет все вхождения.
- len(s), s.count(x), s.find(x), s.rfind(x).
- s.replace(a, b) — все вхождения; s.replace(a, b, k) — первые k.
- s.split(sep) — список частей; s.split() — по любым пробелам.
- sep.join(parts) — склеить список обратно.
- x in s, s.startswith(x), s.endswith(x) — проверки.
- count и replace не перекрывают вхождения: в 'AAA' строка 'AA' встречается один раз.
s.count(x) — число непересекающихся вхожденийs.find(x) → индекс или -1s.replace(x, y) → новая строка
Подстрока и подпоследовательность
Подстрока — это непрерывный кусок строки. Подпоследовательность — набор символов в порядке следования, но не обязательно подряд. В задании 24 чаще нужна подстрока; если в условии сказано «подпоследовательность», нельзя использовать срез s[i:j] и скользящее окно без учёта разрывов.
- «идущих подряд» — подстрока, окно или срез.
- «подпоследовательность» — можно пропускать символы, обычно нужен другой алгоритм.
- «количество подстрок» — важно, считать ли вхождения с перекрытиями.
- «длина подстроки» — ответ в символах, а не в количестве вхождений.
- Проверьте по примерам из условия, что именно считается подстрокой.
подстрока: s[i:j]подпоследовательность: индексы i₁ < i₂ < …
Скользящее окно
Скользящее окно — основной приём для задач «максимальная длина при ограничении на количество символов». Правая граница добавляет символ, левая убирает, пока окно не станет допустимым. Каждый символ входит в окно и выходит из него не более одного раза, поэтому алгоритм линейный.
- left = 0; для right от 0 до len(s)-1 добавляйте s[right].
- Пока условие нарушено, вычитайте s[left] и увеличивайте left.
- После восстановления условия обновляйте ответ: right - left + 1.
- «Не более K» — цикл while count > K.
- «Ровно K» — обновляйте ответ только при count == K, а окно держите не больше K.
- «Не менее K» — обычно проще считать через префиксные суммы или хранить первое вхождение.
длина окна = right - left + 1амортизированно O(n): right и left двигаются только вперёд
Префиксные суммы
Если нужно сравнивать количества двух символов, удобно превратить строку в последовательность +1 и −1. Тогда равное количество означает одинаковую префиксную сумму. Самый длинный отрезок с равными количествами — это расстояние между первым и последним вхождением одного и того же значения префикса.
- P[0] = 0; P[i+1] = P[i] + вес(s[i]).
- Вес: +1 для одного символа, −1 для второго, 0 для остальных.
- Храните первое вхождение каждого значения P в словаре.
- Для отрезка i+1…j нужно P[i] == P[j]; длина = j - i.
- Для «ровно K» ищите P[j] - P[i] == K, то есть P[i] == P[j] - K.
- Для двух условий сразу используйте кортеж из двух префиксных сумм.
P[j] - P[i] = количество A − количество B на отрезкеравные количества: P[i] = P[j]ровно K: P[i] = P[j] - K
Группы и повторения
Иногда строка состоит из повторяющихся групп или блоков одинаковых символов. Тогда удобно сжать её в список пар (символ, количество) или идти по строке с состоянием — каким должен быть следующий символ.
- Подряд идущие одинаковые символы собирайте в группы: 'AAABB' → [('A', 3), ('B', 2)].
- Для группы WXYZ проверяйте, что следующий символ продолжает цикл: W→X→Y→Z→W.
- Для RLE длина кодировки = сумма цифр в количествах + число групп.
- Если группа может быть неполной в начале и конце, разрешите начать с любого символа шаблона.
- Для «ровно N полных групп» делите длину цепочки на длину группы.
RLE: 'AAAB' → '3A1B' или '3AB'длина RLE = число групп + количество цифр в записях количествпозиция в шаблоне = i mod len(pattern)
Палиндромы
Палиндром читается одинаково в обе стороны. Проверка — s == s[::-1]. Для подсчёта палиндромов фиксированной длины переберите все окна; для самого длинного — расширяйте центр; для задачи о заменах — считайте нечётные частоты.
- Проверка: s == s[::-1].
- Палиндром чётной длины имеет центр между символами, нечётной — в символе.
- Любой палиндром длины больше 3 содержит палиндром длины 2 или 3.
- Для подсчёта окон длины L: for i in range(len(s)-L+1).
- Чтобы собрать палиндром перестановками, нечётных частот должно быть не больше одной.
- Минимальное число замен для палиндрома: количество нечётных частот // 2.
s == s[::-1]число окон длины L = len(s) - L + 1замены = odd // 2
Числа и слова в строке
Если строка содержит буквы и цифры, её удобно разбить на «слова» или «числа», заменив лишние символы пробелами. Затем split() отбрасывает пустые части, а int() или len() дают нужную величину.
- Цифры: замените все нецифровые символы пробелами и вызовите split().
- Слова: замените цифры пробелами; словом считается непрерывный блок букв.
- Для максимального числа сравнивайте int(x), а не строки: '9' > '10' лексикографически.
- Ведущие нули в IP-адресе недопустимы, хотя int('012') = 12.
- Для IP делите по точкам и проверяйте 4 части, каждую в диапазоне 0…255.
- Маску проверяйте посимвольно: '?' означает любую цифру.
replace для замены классов символовmax(map(int, parts))0 ≤ part ≤ 255, 4 части, без ведущих нулей
Регулярные выражения
Регулярное выражение — это шаблон, по которому ищут подстроки. В задании 24 оно помогает, когда нужно найти все числа, слова, пары «буква-цифра», арифметические выражения или пересекающиеся вхождения. Шаблон читается слева направо, а квантификаторы задают количество повторений.
- import re; основные функции — re.findall, re.finditer, re.search и re.sub.
- Классы: \d — цифра, \w — буква или цифра, [A-Z] — диапазон, [^0-9] — любой символ, кроме цифры.
- Квантификаторы: + одно и более, * ноль и более, ? ноль или один, {m,n} от m до n повторов.
- Жадные * и + захватывают максимум; ленивые *? и +? — минимум.
- Группы (...) позволяют достать часть совпадения через m.group(1).
- Для пересекающихся вхождений используйте lookahead: re.finditer(r'(?=(ABA))', s).
- Для арифметического выражения подходит шаблон (\d+)([+*\-])(\d+); результат проверяйте по всем совпадениям.
- Если совпадений нет, findall вернёт пустой список — обработайте этот случай отдельно.
re.findall(r'\d+', s) — все числаre.finditer(r'(?=(ABA))', s) — пересекающиеся вхожденияre.sub(r'[^0-9]', ' ', s) — заменить всё, кроме цифр
Сложность и проверка
При длине 10^6 квадратичный алгоритм может работать минуты. Считайте количество операций: один проход — хорошо, вложенный цикл по всем парам — плохо. Перед сдачей проверьте программу на короткой строке, где ответ можно посчитать вручную.
- O(n) — один проход, окно, префиксные суммы; O(n²) — перебор всех пар границ.
- Не вызывайте s.count() или s.find() внутри цикла по всем i без необходимости.
- Проверьте пустую строку, строку из одного символа и случай без подходящих подстрок.
- Проверьте границы среза: range(len(s)-L+1), а не range(len(s)-L).
- Убедитесь, что ответ — целое число, как требует бланк.
- Для отладки выведите длину строки и первые/последние символы.
n = 10^6: O(n) — норма, O(n²) — слишком долгоответ — одно целое число