Редакция для Выравнивание зарядов
Submitting an official solution before solving the problem yourself is a bannable offence.
Автор:
Разбор задачи «Выравнивание зарядов»
Идея задачи
Дан массив a. За одну операцию можно выбрать один элемент и увеличить его на текущее значение x, после чего x увеличивается на 1. Изначально x = 1.
Нужно найти минимальное количество операций, после которого все элементы массива станут кратны k.
На первый взгляд задача выглядит необычно: мы не можем просто прибавлять к числам что угодно, потому что доступные прибавления жёстко заданы — сначала 1, потом 2, потом 3 и так далее. Значит, нужно понять, в какой момент удобно обработать каждый элемент.
Шаг 1. Что нужно каждому элементу
Рассмотрим один элемент a[i].
Нас интересует его остаток по модулю k:
- если
a[i] % k == 0, то элемент уже подходит; - иначе ему не хватает
need = k - (a[i] % k)
чтобы стать кратным k.
Например, если k = 5:
- для
7остаток равен2, значит не хватает3; - для
13остаток тоже3, значит не хватает2.
Но здесь есть важная тонкость.
Мы не можем просто взять и прибавить need в любой момент. На некотором шаге значение прибавления равно текущему x, а оно определяется номером операции. Значит, элемент можно обработать только в такой момент, когда:
x % k == need
То есть нам подходят не одно число, а целая последовательность:
need, need + k, need + 2k, need + 3k, ...
Именно эти значения x дают нужный остаток по модулю k.
Шаг 2. Почему одинаковые need конфликтуют
Пусть несколько элементов требуют одну и ту же величину need.
Например, k = 4, а три элемента имеют остаток 3. Тогда каждому из них нужно добавить число, сравнимое с 1 по модулю 4.
Подходящие значения x:
1, 5, 9, 13, ...
Но один и тот же шаг нельзя использовать для нескольких элементов одновременно. Значит:
- первый такой элемент можно обработать при
x = need; - второй — не раньше чем при
x = need + k; - третий — не раньше чем при
x = need + 2k; - и так далее.
Если некоторый need встретился cnt раз, то для последнего такого элемента понадобится значение:
need + (cnt - 1) * k
После этого операция будет выполнена, и число совершённых операций станет на 1 больше. Поэтому вклад этой группы в ответ:
need + (cnt - 1) * k + 1
Шаг 3. Как получить общий ответ
Для каждого ненулевого остатка мы считаем, сколько раз встретилось соответствующее значение need.
Когда мы встречаем очередной элемент с таким need, его минимальный момент обработки равен:
need + (count[need] - 1) * k
Тогда число операций, которое уже должно быть сделано к этому моменту:
need + (count[need] - 1) * k + 1
Итоговый ответ — максимум по всем таким значениям.
Если все элементы уже кратны k, то ответ равен 0.
Небольшой пример
Пусть:
n = 5, k = 4
a = [1, 2, 3, 4, 5]
Посчитаем need для каждого элемента:
1 % 4 = 1, значитneed = 32 % 4 = 2, значитneed = 23 % 4 = 3, значитneed = 14 % 4 = 0, ничего не нужно5 % 4 = 1, значитneed = 3
Получаем значения:
need = 3встречается 2 разаneed = 2встречается 1 разneed = 1встречается 1 раз
Для группы need = 3:
- первый элемент можно обработать при
x = 3 - второй — при
x = 7
Значит, понадобится как минимум 7 + 1 = 8 операций.
Для остальных групп максимум меньше, значит ответ определяется именно этой группой.
Почему жадный подход здесь корректен
Мы фактически не строим последовательность операций явно, а только ищем самый поздний момент, когда обязательно должен быть обработан какой-то элемент.
Это корректно по двум причинам:
- Для элемента с данным
needподходят только значенияxвидаneed + t * k. - Если одинаковый
needвстречается несколько раз, то эти элементы неизбежно займут разные такие моменты, начиная с самого раннего возможного.
Поэтому для каждой группы одинаковых need оптимальное поведение очевидно: занимать самые ранние доступные подходящие значения x.
А общий ответ — это последний момент среди всех групп.
Алгоритм
Для каждого теста:
- Считать
n,kи массивa. - Создать словарь
count. Для каждого элемента:
- найти
rem = a[i] % k; - если
rem == 0, пропустить; - иначе вычислить
need = k - rem; - увеличить
count[need]; - посчитать текущее требуемое число операций:
cur = need + (count[need] - 1) * k + 1- обновить ответ максимумом.
- найти
- Вывести ответ.
Сложность
Для одного теста все действия выполняются за O(n) в среднем, если использовать map/unordered_map в C++ или dict в Python.
С учётом ограничения на суммарный размер входа решение работает достаточно быстро.
- Время:
O(sum(n)) - Память:
O(sum различных need)
Реализация на C++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n;
long long k;
cin >> n >> k;
unordered_map<long long, long long> cnt;
long long ans = 0;
for (int i = 0; i < n; ++i) {
long long a;
cin >> a;
long long rem = a % k;
if (rem == 0) {
continue;
}
long long need = k - rem;
cnt[need]++;
long long cur = need + (cnt[need] - 1) * k + 1;
ans = max(ans, cur);
}
cout << ans << '\n';
}
return 0;
}
Комментарии к реализации
- Используем
long long, потому что значения могут быть большими. unordered_mapхранит количество элементов для каждогоneed.- Если элемент уже делится на
k, он не влияет на ответ. - Формула
need + (cnt[need] - 1) * k + 1— ключевая в задаче.
Реализация на Python
import sys
input = sys.stdin.readline
t = int(input())
for _ in range(t):
n, k = map(int, input().split())
a = list(map(int, input().split()))
cnt = {}
ans = 0
for x in a:
rem = x % k
if rem == 0:
continue
need = k - rem
if need not in cnt:
cnt[need] = 0
cnt[need] += 1
cur = need + (cnt[need] - 1) * k + 1
if cur > ans:
ans = cur
print(ans)
Комментарии к реализации
- В Python удобно использовать обычный словарь
dict. - Все вычисления также безопасно выполняются на целых числах произвольной длины.
- Код почти полностью повторяет математическую идею решения.
Типичные ошибки
1. Забыть случай, когда элемент уже кратен k
Если a[i] % k == 0, его не нужно учитывать. Иначе можно получить неверный ответ.
2. Вывести максимум без +1
Частая ошибка — взять максимум по значениям
need + (cnt - 1) * k
Но это момент значения x, а не число уже совершённых операций. Правильный ответ на 1 больше.
3. Пытаться сортировать массив или моделировать процесс буквально
Это не нужно. Полная симуляция операций будет слишком сложной и не даёт пользы. Достаточно считать частоты значений need.
4. Использовать int вместо long long в C++
Значение ответа может быть достаточно большим, поэтому безопаснее сразу использовать long long.
Вывод
Главная идея задачи — сгруппировать элементы по тому, какой остаток им нужно добрать до кратности k. После этого каждая группа обрабатывается независимо, а ответ определяется самой «поздней» группой.
Это хороший пример задачи, где после небольшого математического наблюдения решение становится очень коротким и эффективным.
Комментарии