Implementation-задачи полосы CF 1100–1500 — это упражнения не на алгоритм (который часто очевиден), а на чистоту реализации. Соблазн расписать всё «случай за случаем» через вложенные if'ы заметно снижает шанс пройти все тесты с первого раза: каждое лишнее ветвление — это потенциальный источник off-by-one, забытого симметричного случая или ошибки на границах массива.
Эта задача — учебный пример того, как два указателя и один аккумулятор полностью заменяют 5–6 веток рассуждения «если конфет хватает / если кончились / если очередь Alice / если очередь Bob / если ход первый». Алгоритм — за один проход массива, за линейное время и без специальной обработки конца игры.
Что дано
В коробке стоит ряд из конфет, -я конфета имеет вес . Двое — Alice и Bob — едят их по очереди:
- Alice всегда ест с левого края оставшегося ряда.
- Bob всегда ест с правого края оставшегося ряда.
- Первой ходит Alice; на самом первом ходу она съедает только одну конфету (вес 1-й конфеты от левого края).
- На каждом следующем ходу игрок должен съесть минимально возможное непустое количество подряд идущих конфет со своего края так, чтобы суммарный вес съеденного на этом ходу был строго больше, чем съел соперник на предыдущем ходу.
- Если конфет на оставшемся ряду не хватает, чтобы превысить вес соперника, игрок съедает все оставшиеся конфеты, и игра заканчивается.
Нужно вывести три числа:
- Общее количество ходов, сделанных в игре.
- Суммарный вес съеденного Alice.
- Суммарный вес съеденного Bob.
Ограничения
- — количество тестов.
- — длина ряда конфет в одном тесте.
- — вес каждой конфеты.
- Время — 2 секунды, память — 256 МБ.
- Сумма по всем тестам .
Формат ввода
t
n_1
a_1 a_2 ... a_{n_1}
n_2
a_1 a_2 ... a_{n_2}
...
Формат вывода
Для каждого теста — одна строка из трёх чисел через пробел.
Примеры
Будем разбирать на нескольких показательных входах.
Пример A (типичный): , . Ответ: 6 23 21.
Пример B (один элемент): , . Ответ: 1 1000 0.
Пример C (всё одинаковое): , . Ответ: 2 1 2.
Пример D (возрастающий): , . Ответ: 6 45 46.
Пример E (Bob не может превысить): , . Ответ: 2 2 1.
Пример F (большая «семёрка» в середине): , . Ответ: 4 9 3.
Пример G (все единицы): , . Ответ: 4 4 3.
Разберём пример A руками
. Левый указатель , правый указатель . Веса съеденного: last = 0 (никто ещё не ел).
| Ход | Чей | Что добирали (значения) | Сумма ходa | Состояние массива | last |
|---|---|---|---|---|---|
| 1 | Alice | 3 | 3 | ||
| 2 | Bob | 5 | 5 | ||
| 3 | Alice | (сумма ) | 6 | 6 | |
| 4 | Bob | (сумма ) | 8 | 8 | |
| 5 | Alice | (сумма ) | 14 | 14 | |
| 6 | Bob | (массив исчерпан, — съели всё) | 8 | — |
Итог: 6 ходов, Alice = , Bob = . Сходится с эталоном.
Ключевое наблюдение из ручного прогона: последний ход Bob — особый. Bob набирает , что меньше предыдущего веса Alice (14), но после этих двух конфет массив уже пуст. По правилам Bob съедает всё оставшееся; это тот самый «специальный» случай, который при наивной реализации хочется отдельно обрабатывать через if. На самом деле он естественно обрабатывается тем же циклом, если правильно сформулировать условие выхода.
Идея решения
Алгоритм в одну фразу.
Два указателя (слева) и (справа), переменная
last— вес съеденного на предыдущем ходу. На каждом ходу игрок внутренний цикл, добирающий конфеты со своего края, пока накопленный вес не превыситlastили указатели не сошлись. Внешний цикл — пока .
Внутренний цикл — это вся реализация хода. Он сам обрабатывает три случая, которые без него потребовали бы трёх if:
- Игрок успел набрать — выход из внутреннего цикла, ход успешен.
- Массив кончился до того, как набрали — внутренний цикл всё равно выходит (по условию ), игрок ест всё что собрал. Внешний цикл на следующей итерации увидит и завершится.
- На самом первом ходу
last = 0— Alice ест ровно одну конфету (любая положительная конфета даёт сумму ), внутренний цикл выходит после первой добавки.
Все три случая закрываются одной формулировкой условия внутреннего цикла. В этом и состоит «чистая реализация»: вместо отдельных веток алгоритм опирается на инвариант, который верен во всех ситуациях.
Почему это работает корректно
- Условие «строго больше». Внутренний цикл идёт «пока съеденное меньше или равно
lastи массив ещё не исчерпан». Выход происходит ровно в тот момент, когда либо условие нарушено (eaten > last, ход успешный), либо массив пуст (l > r, ход «доедание»). - Минимальность набора. Цикл добирает конфеты по одной за итерацию. На момент превышения
lastмы останавливаемся — добавлять ещё одну конфету уже не нужно. Это и есть «минимально возможное непустое количество». - Чередование игроков. Переменная
turn(0 — Alice, 1 — Bob) флипается после каждого хода:turn ^= 1. Никакой двойной логики «если ход чётный / если ход нечётный» — один и тот же код для обоих игроков, отличается только указатель и счётчик-аккумулятор.
Решение: псевдокод
для каждого теста:
прочитать n и массив a
l ← 0; r ← n - 1
alice_total ← 0; bob_total ← 0
last ← 0
moves ← 0
turn ← 0 # 0 = Alice (ест слева), 1 = Bob (ест справа)
пока l ≤ r:
eaten ← 0
пока l ≤ r и eaten ≤ last:
если turn = 0:
eaten ← eaten + a[l]
l ← l + 1
иначе:
eaten ← eaten + a[r]
r ← r - 1
если turn = 0:
alice_total ← alice_total + eaten
иначе:
bob_total ← bob_total + eaten
last ← eaten
moves ← moves + 1
turn ← turn XOR 1
вывести moves, alice_total, bob_total
Сложность одного теста: — каждый из указателей сдвигается ровно раз суммарно по всем итерациям внутреннего цикла. С учётом — итог по всем тестам.
Код решения
Комментарии по реализации.
- Быстрый ввод обязателен. В Python —
sys.stdin.buffer.read().split()через единственное чтение буфера, иначе тестов с ( чисел) могут упереться в TL черезinput()в цикле. В C++ —ios::sync_with_stdio(false)достаточно. turn ^= 1— самый компактный способ переключить 0/1. Альтернативы:turn = 1 - turn,turn = (turn + 1) % 2. Все эквивалентны.- Тип для сумм. Максимум
eaten— сумма всех весов до , вint32укладывается с запасом. В Pythonintавтоматически большой, в C++ держимlong longдля аккумуляторов — привычка окупается на чуть более жёстких ограничениях. - Идиомы
++moves,eaten += a[l++]в C++ — типичны для соревновательного кода. Не стоит злоупотреблять, но в горячем цикле читаемы.
Проверим на всех 7 примерах из условия
Прогоняем алгоритм по каждому из примеров A–G и сравниваем с эталонными ответами.
A: ,
Уже разобрали в «руками на примере». Ходов 6, Alice 23, Bob 21. 6 23 21 ✓.
B: ,
Ход 1 (Alice): eaten = 0 ≤ 0 = last, добираем , l = 1. Теперь l = 1 > 0 = r, выход из внутреннего. alice = 1000, last = 1000, ход 1.
Внешний: l = 1 > 0 = r, выход. 1 1000 0 ✓.
C: ,
Ход 1 (Alice): добираем , l = 1. eaten = 1 > 0 = last — выход. alice = 1, last = 1.
Ход 2 (Bob): eaten = 0 ≤ 1, добираем , r = 1. eaten = 1 ≤ 1, добираем , r = 0. Теперь l = 1 > 0 = r, выход внутреннего. bob = 2, last = 2.
Внешний: l > r, выход. 2 1 2 ✓.
D: ,
Ход 1 (Alice): . alice = 1, last = 1.
Ход 2 (Bob): . bob = 13, last = 13.
Ход 3 (Alice): . alice = 15, last = 14.
Ход 4 (Bob): . bob = 36, last = 23.
Ход 5 (Alice): . alice = 45, last = 30.
Ход 6 (Bob): (массив исчерпан, , но l > r). bob = 46.
6 45 46 ✓.
E: ,
Ход 1 (Alice): . alice = 2, last = 2.
Ход 2 (Bob): добираем , r = 0. eaten = 1 ≤ 2, продолжаем — но l = 1 > 0 = r, выход. bob = 1, last = 1.
2 2 1 ✓.
F: ,
Ход 1 (Alice): . alice = 1, last = 1.
Ход 2 (Bob): (), r = 4. , сумма . bob = 2, last = 2.
Ход 3 (Alice): (), l = 2. , сумма . alice = 9, last = 8.
Ход 4 (Bob): (), r = 2. Теперь l = 3 > 2 = r, выход. bob = 3.
4 9 3 ✓.
G: ,
Ход 1 (Alice): . alice = 1, last = 1.
Ход 2 (Bob): (), r = 5. , сумма . bob = 2, last = 2.
Ход 3 (Alice): (), l = 2. , сумма (), l = 3. , сумма . alice = 4, last = 3.
Ход 4 (Bob): (), r = 3. l = 4 > 3 = r, выход. bob = 3.
4 4 3 ✓.
Все 7 примеров сходятся. Алгоритм корректен.
Крайние случаи
1. — одна конфета
Alice съедает её на первом ходу (любая положительная даёт eaten = a[0] > 0 = last). Внешний цикл сразу выходит. Ход 1, Alice = , Bob = 0. Покрыт примером B.
2. Указатели сходятся ровно после первого набора
Как в примере E (): после первого хода Alice указатели . Bob делает свой ход, после внутреннего цикла , и внешний цикл завершается.
3. Игрок не может превысить, и массив пуст ровно в этот момент
Как в примере F: на 4-м ходу Bob берёт одну конфету (), eaten = 1 ≤ 8 = last, но указатели сходятся. Bob съедает — это легитимный последний ход, игра заканчивается.
4. Все одинаковые конфеты
Пример G — все единицы. Каждый ход суммарный вес растёт ровно на 1: Alice 1, Bob 2, Alice 3, Bob (остаток). Видно, как даже на простейшем входе чередование съедает массив за минимальное число ходов.
5. Одна гигантская конфета в середине
Пример F с . На 3-м ходу Alice цепляет именно её и резко обгоняет Bob. После такой «гиганта» оставшийся массив игрока часто оказывается недостаточным для ответа — игра заканчивается на следующем ходу.
6. Очень длинный возрастающий массив
Пример D — сумма распределяется неравномерно: первые ходы дают маленькие суммы, к середине обе стороны набирают «крупными порциями». В таких случаях ходов получается мало (6 на ), даже несмотря на длинный массив.
Типичные ошибки
- Отдельная ветка для «массив кончился, игрок не дотянул». Соблазн написать что-то вроде
if l > r and eaten <= last: alice_total += eaten; break. Это работает, но удваивает логику добавления в счётчик. Если внутренний цикл правильно сформулирован — этот случай уже обрабатывается естественно: цикл выходит сам, а добавление в счётчик случается одно — в общей ветке после внутреннего цикла. - Условие внутреннего цикла «
eaten < last» вместо «eaten <= last». Тогда при равенствеeaten == lastмы не добавим следующую конфету и завершим ход, хотя по правилам нужно строго больше. Получится ход, в котором сумма равна предыдущей, что нарушает условие задачи. - Перепутанные направления. Alice ест с левого края (увеличиваем
l), Bob — с правого (уменьшаемr). Симметричная ошибка часто проявляется на тестах, где результаты Alice и Bob поменялись местами — пример D: ожидаем45 46, выводим46 45. - Забыть обнулить
eatenв начале каждого хода. Тогда вес ходов накапливается, и второй ход стартует с уже ненулевого аккумулятора. Пример E: Alice ест , Bob «должен» съесть . Еслиeaten = 2ещё с предыдущего хода, цикл сразу не входит, Bob ест 0. Безусловно WA. - Использовать
eaten = 0глобально (одна переменная на всю игру). Должен быть локальный аккумулятор хода. Глобальный вариант приводит к ошибке из п. 4 в усиленной форме. - Перепутать
lastиeatenв условии. Условие — «пока съеденное на текущем ходу не превышает съеденное на предыдущем». Сравнивать нужноeaten <= last, неlast <= eaten(это всегда true в нужных моментах) и неlast < eaten(это только проверяет, превысили ли — но не управляет циклом). intвместоlong longдля накопителей в C++ на чуть более жёсткой задаче. Здесь в данных всё помещается вint32, но привычка к 64-битным аккумуляторам полезна.
Анализ сложности
- Время: на тест. Внутренний цикл сдвигает один из указателей за каждую итерацию, оба указателя суммарно сдвигаются не более раз. Внешний цикл — не более итераций.
- Память: на тест (массив ).
- С учётом всех тестов: , что укладывается в TL 2 секунды с большим запасом.
Что ещё полезно потренировать
Задачи с похожей структурой (два указателя на массиве + аккумулятор, или симуляция игры с очередями) — все с возраст-нейтральных платформ.
- Codeforces 363B «Fence», — скользящее окно длины с минимальной суммой. Тоже один проход массива, no nested if'ов.
- Codeforces 339B «Xenia and Ringroad», — симуляция перемещения по кругу. Хорошая тренировка чистоты — соблазн писать «if впереди / if позади» убирается одной формулой по модулю.
- acmp.ru, раздел «Одномерные массивы» — много задач уровня 1100–1400 на «два указателя» или «один проход с аккумулятором». Старые задачи серии — отличная тренировка дисциплины «один цикл вместо пяти if'ов».
- Codeforces 1399C «Boats Competition», — два указателя на отсортированном массиве с подсчётом пар. Чистая реализация — без вложенных условий, аналогично сегодняшней задаче.
Для следующего шага — попробовать задачу, где один цикл заменяет уже 3–4 ветки рассуждений: типичный пример — задачи на «движение фигур по полю с ограничениями» или «обход с двумя стеками».
Итого
- Идея: два указателя , аккумулятор
last. Внешний циклwhile l ≤ r, внутренний —while l ≤ r and eaten ≤ last. Чередование игроков черезturn ^= 1. - Сложность: на тест, суммарно.
- Чистота: ни одного
ifдля «массив кончился», «первый ход», «игрок не дотянул» — все три случая обрабатываются единым условием внутреннего цикла. - Типичные ловушки:
<вместо<=в условии цикла, забытое обнулениеeatenна новом ходу, перепутанные направления указателей, отдельная ветка для конца игры.