«Подсчитать количество подотрезков, удовлетворяющих условию» — формулировка, которая на полосе CF 1300–1800 встречается каждый второй контест. Когда условие алгебраическое (сумма, среднее, остаток), почти всегда работает один и тот же шаблон: записать условие через префиксные суммы, увидеть в нём равенство (или разность) двух значений, и свести задачу к подсчёту пар индексов с одинаковым (или отличающимся на константу) префиксом.
Эта задача — учебный экземпляр такого шаблона. Здесь условие «сумма подотрезка равна его длине» одной заменой переменной превращается в «сумма равна нулю», и дальше всё решается за один проход по массиву с хешмапом текущих частот префикса.
Что дано
В одном запуске несколько тестов. Для каждого теста:
- В первой строке — число .
- Во второй — строка из символов; каждый символ — десятичная цифра –. Эта строка интерпретируется как массив .
Подотрезок называется хорошим, если сумма его элементов равна длине подотрезка:
Нужно вывести количество хороших подотрезков для каждого теста.
Ограничения
- — число тестов.
- — длина массива в одном тесте.
- Сумма по всем тестам — до .
- Цифры .
- Время — 2 секунды, память — 256 МБ.
Формат ввода
В первой строке всех тестов — число . Далее для каждого теста две строки: и строка из цифр.
Формат вывода
Для каждого теста — одно число, количество хороших подотрезков.
Пример 1
Вход:
3
3
120
5
11011
6
600005
Вывод:
3
6
1
Разберём первый тест руками
Массив .
- Длина : подотрезки . Условие «сумма длина »: — да; — нет; — нет. Хороших: .
- Длина : сумма — нет; сумма — да. Хороших: .
- Длина : сумма — да. Хороших: .
Итого — сходится с ответом.
Решение «в лоб» — перебрать все пар , посчитать сумму на каждом и сравнить с длиной. При это операций — на 4 порядка больше допустимого. Нужно или .
Идея решения
Перепишем условие в более удобной форме. Введём вспомогательный массив . Тогда
Условие «сумма равна длине » эквивалентно
то есть
Получили классическую постановку: подсчитать подотрезки массива с нулевой суммой.
Заведём префиксные суммы , . Сумма на отрезке равна . Условие «сумма равна нулю» эквивалентно .
Значит, количество хороших подотрезков — это количество пар индексов , , таких что .
Подсчёт пар с одинаковым значением — стандартная задача на хешмап. Один проход:
- Завести
cnt: dict[int, int] = {0: 1}— стартовое значение уже встречено один раз. - Идти по массиву, обновлять текущий префикс .
- К ответу добавлять
cnt[p](столько раз раньше встречалось такое же значение префикса — каждая прошлая встреча образует пару с текущей). - Затем
cnt[p] += 1.
После всего прохода в ответе накоплено число пар.
Почему это работает
Биекция между «хорошими подотрезками » и «парами равных префиксов , » — точная:
- хороший (по нашему алгебраическому выводу).
- Пара , , с задаёт подотрезок , который хороший.
Каждый хороший подотрезок задаёт ровно одну пару префиксов. Поэтому количество хороших подотрезков количество таких пар.
Подсчёт пар через «cnt до момента » работает потому, что для каждого мы знаем, сколько индексов имели — это в точности cnt[P_j] до увеличения. После увеличения cnt[P_j] готов для следующего шага.
Размер значений префикса
. Префикс может принимать значения от до . При — диапазон . Это влезает в int с большим запасом; хешмап с ключами в таком диапазоне — без проблем.
Решение: псевдокод
для каждого теста:
прочитать n и строку s длины n
cnt = {0: 1} // P_0 = 0 уже встречено
p = 0 // текущий префикс
ans = 0
для i от 1 до n:
b_i = (s[i-1] как цифра) - 1
p += b_i
ans += cnt.get(p, 0)
cnt[p] = cnt.get(p, 0) + 1
вывести ans
Сложность: в среднем на один тест.
Код решения
Различий между языками два значимых пункта. Первый — выбор контейнера: в Python dict или defaultdict(int) достаточно. В C++ — unordered_map<int, int>, но с защитой от анти-хеш-атак стоит использовать кастомный хеш или map<int, int>. Второй — чтение: строка цифр без разделителей читается одной строкой, преобразование в массив — поэлементно (ord(c) - ord('0') в Python, c - '0' в C++).
Комментарии по реализации.
- Тип ответа. , максимум пар — при идеальной картине (все префиксы равны).
intпереполнится. В C++ —long long; в Python — встроенныйint, без беспокойства. cnt[0] = 1в начале. Аналог «нулевого префикса» — отрезки, начинающиеся с первого элемента, должны быть учтены. Без этого правильный ответ невозможен.- Кастомный хеш в C++. На Codeforces встречаются тесты против стандартного
unordered_map<int>(порождают операций на вставку).SplitMix— стандартное решение: переписывается через серию умножений и сдвигов, распределение становится равномерным независимо от закономерностей входа. - Чтение цифр из строки. Строка в условии — без разделителей; в C++ читается
cin >> sкак обычное слово; в Python —data[idx].decode()даётstr, по которому потом идём посимвольно. - Глобальные структуры. В Python
cntсоздаётся в каждом тесте заново — это правильно, иначе тесты «загрязнят» друг друга. В C++ аналогично:unordered_map cnt;локально внутриwhile (t--).
Проверим на всех примерах из условия
Пример 1: 120
, . Префиксы : (длина ).
Идём по :
cnt[P_i] до этого | ans += ? | cnt[P_i] после | |||
|---|---|---|---|---|---|
| (это ) | |||||
| () |
Итого ans = 1 + 0 + 2 = 3. ✓
Пример 2: 11011
, . Префиксы: .
cnt[P_i] до | ans += | |||
|---|---|---|---|---|
Итого: . ✓
Пример 3: 600005
, . Префиксы: .
cnt[P_i] до | ans += | |||
|---|---|---|---|---|
| () |
Итого: . ✓
Какой именно подотрезок? , значит хороший подотрезок — , длина , сумма . Подходит.
Крайние случаи, на которых можно споткнуться
1. Все элементы — единицы
, . Все префиксы равны нулю, и . Хороших подотрезков — каждый. Это согласуется с задачей: сумма любого подотрезка из одних единиц равна его длине.
При ответ — около . long long обязателен.
2. Все элементы — нули
, . Префиксы: — все разные. Хороших подотрезков нет, ответ . Что согласуется: для длины нужна сумма , но сумма нулей — .
3. Длина
Хороший подотрезок длины — это . Алгоритм видит: на шаге с имеем , префикс не меняется, и предыдущее значение префикса было одним из ранее увиденных — встречаем минимум , что даёт к ответу.
4. Один тест с большим
Сумма по тестам — один тест с ничем не отличается по сложности от тестов с каждый. Главное — не накладывать на каждый тест инициализаций; локальный cnt в каждом тесте обнуляется автоматически.
5. Граница long long для ответа
Максимум — . int (2^{31} \approx 2.1 \cdot 10^9) переполнится. long long обязателен в C++; в Python — нет проблем.
6. Хеш-атака на unordered_map
Без кастомного хеша возможен TL на специально подготовленных тестах. С SplitMix или аналогом — гарантированный в среднем. Альтернатива — map<long long, long long>, , медленнее, но устойчивее.
Типичные ошибки
- Забытая инициализация
cnt[0] = 1. Без этого подотрезки, начинающиеся с , не учитываются. Симптом: ответ меньше эталонного на конкретное число, зависящее от данных. intвместоlong long. Если массив длинный и все элементы — единицы, ответ выходит за пределыint. Лекарство — всегдаlong longпод счётчик пар.unordered_map<int, int>без защиты. Анти-хеш-тест даёт TL. Лекарство — кастомный хеш илиmap.- Подсчёт пар после увеличения cnt, а не до. Если сначала
cnt[p] += 1, потомans += cnt[p]— каждый префикс учтёт сам себя, в результатеansзавышен на . - Сброс
cntмежду тестами не происходит. Еслиcntобъявлен глобально и не очищается — соседние тесты «загрязняют» друг друга. Симптом: на одиночном тесте всё хорошо, в наборе — WA. - Чтение строки как массива чисел через пробел. В этой задаче строка цифр — без пробелов между ними. Чтение через
cin >> n >> a[0] >> a[1] ...не сработает; нужно читать как строку и брать символы. - Перебор пар через двойной цикл. Соблазн «сначала собрать все префиксы, потом для каждой пары проверить» — операций. Сразу подсчёт через хешмап — единственный правильный способ.
Анализ сложности
- Время: на один тест (хешмап в среднем за на операцию). Суммарно .
- Память: под хешмап в худшем случае (все префиксы разные).
Что ещё полезно потренировать
- Codeforces 1015E1 «Stars Drawing (Easy)» — другой вариант «префикс по строке + подсчёт», полоса CF .
- Codeforces 86D «Powerful Array» — алгоритм Мо с префиксами, шаг вперёд от текущей задачи; полоса CF , но идея «состояние через префиксы» здесь центральная.
- Codeforces 220B «Little Elephant and Array» — частоты в подотрезке через offline-обработку и префиксы; учит видеть, когда «префикс» — это не сумма, а другой накопительный признак.
- acmp.ru задача 442 «Подсчёт инверсий» — родственный шаблон «считать пары через структуру данных», вместо хешмапа здесь Fenwick.
Итого
- Идея: трансформация превращает условие «сумма равна длине» в «сумма равна нулю»; равенство префиксов соответствует подотрезку с нулевой суммой; подсчёт пар через хешмап — .
- Сложность: время и память на один тест.
- Ловушки: забытая
cnt[0] = 1,intвместоlong long,unordered_mapбез кастомного хеша, увеличениеcntдо подсчёта вместо после.