Редакция для Число разбиений
Submitting an official solution before solving the problem yourself is a bannable offence.
1. Идея
Нужно посчитать количество разбиений числа n на натуральные слагаемые, если порядок не важен.
Например, для 4 разбиения такие:
43 + 12 + 22 + 1 + 11 + 1 + 1 + 1
Всего 5.
Это классическая задача на динамическое программирование, очень похожая на подсчёт числа способов набрать сумму, если можно использовать числа 1, 2, ..., n сколько угодно раз, а порядок использования не учитывается.
Будем хранить dp[s] — количество способов получить сумму s.
2. Наблюдения
Наблюдение 1
Если разрешить использовать только числа от 1 до x, то можно постепенно наращивать ответ.
Сначала учитываем только 1, потом 1 и 2, потом 1, 2, 3 и так далее.
Наблюдение 2
Когда мы добавляем очередное число x, для каждой суммы s >= x можно:
- не использовать
x, - или использовать его хотя бы один раз.
Если использовать x хотя бы один раз, то после выбора одного x остаётся добрать сумму s - x, и это можно сделать всеми способами, уже посчитанными в dp[s - x].
Поэтому переход такой:
dp[s] += dp[s - x]
Наблюдение 3
Очень важно, в каком порядке идти по циклам.
Если внешний цикл идёт по x от 1 до n, а внутренний — по s от x до n, то каждое разбиение будет посчитано ровно один раз, без учёта перестановок.
Именно поэтому разбиения 1 + 3 и 3 + 1 не считаются отдельно.
3. Алгоритм
- Считываем
n. - Создаём массив
dpдлиныn + 1, заполненный нулями. - Полагаем
dp[0] = 1.- Это означает, что сумму
0можно получить ровно одним способом: ничего не взять.
- Это означает, что сумму
- Для каждого числа
xот1доn:- для каждой суммы
sотxдоn:- прибавляем к
dp[s]значениеdp[s - x]; - берём результат по модулю
1000000007.
- прибавляем к
- для каждой суммы
- Выводим
dp[n].
4. Почему это работает
Докажем, что после обработки всех чисел от 1 до x значение dp[s] равно числу разбиений суммы s на слагаемые, не превосходящие x.
База
До начала циклов:
dp[0] = 1,dp[s] = 0дляs > 0.
Это верно: сумму 0 можно получить одним способом, а положительные суммы — никак, если ещё не разрешено использовать никакие числа.
Переход
Пусть уже после обработки чисел от 1 до x - 1 всё посчитано правильно.
Теперь добавляем число x.
Для каждой суммы s:
- старое значение
dp[s]— это число разбиенийs, где числоxне используется; - значение
dp[s - x], которое прибавляется, соответствует разбиениям суммыs - xс числами доx, а если к каждому такому разбиению добавить одно слагаемоеx, получится разбиение суммыs, гдеxиспользуется хотя бы один раз.
Эти два множества разбиений не пересекаются и вместе дают все разбиения суммы s с числами до x.
Значит, после обновления:
dp[s]становится числом всех разбиенийsна слагаемые не большеx.
Почему порядок не учитывается
Поскольку мы добавляем числа в порядке возрастания: сначала 1, потом 2, потом 3 и так далее, — каждое разбиение строится в единственном виде: как набор слагаемых, отсортированных по неубыванию.
Поэтому перестановки одинаковых слагаемых не создают новых способов.
Следовательно, после завершения всех циклов dp[n] — это именно количество разбиений числа n на натуральные слагаемые без учёта порядка.
5. Сложность
Внешний цикл выполняется n раз.
Внутренний цикл для каждого x проходит по суммам от x до n, то есть суммарно это O(n^2) операций.
- Время:
O(n^2) - Память:
O(n)
При n <= 5000 это укладывается в ограничения.
6. Код на C++17
#include <iostream>
#include <vector>
using namespace std;
int main() {
const long long MOD = 1000000007LL;
int n;
cin >> n;
vector<long long> dp(n + 1, 0);
dp[0] = 1;
for (int x = 1; x <= n; x++) {
for (int s = x; s <= n; s++) {
dp[s] += dp[s - x];
if (dp[s] >= MOD) dp[s] -= MOD;
}
}
cout << dp[n] << '\n';
return 0;
}
7. Код на Python 3
MOD = 1000000007
n = int(input())
dp = [0] * (n + 1)
dp[0] = 1
for x in range(1, n + 1):
for s in range(x, n + 1):
dp[s] += dp[s - x]
if dp[s] >= MOD:
dp[s] -= MOD
print(dp[n])
Комментарии