计数 / 划分型
方案数·高精度·整数划分
本课摘要
计数 / 划分型课程回答“计数与划分问题如何避免重复或遗漏方案”。内容以方案数·高精度·整数划分为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断计数 / 划分型的适用条件与状态边界
- 围绕“方案数·高精度·整数划分”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
换个问题:不求「最好」,改数「多少种」
前面几类线性 DP 都在问同一句话——最优是多少:最长的子序列、最大的子段和、代价最小的对齐。 所以它们的转移里都坐着一个 (或 )。可现实里的问题未必都求极值: 「上 级楼梯,每步跨 1 或 2 级,有多少种走法?」「把 拆成若干正整数,有几种拆法?」 答案不再是一个「最好的值」,而是一个计数。
关键洞察小到出乎意料:把转移里的 换成加法 ,DP 就从「记录最优」变成「累计方案」。 道理在于 DP 的两大要件恰好都适配计数——最优子结构变成「大问题的方案由子问题的方案拼成」,无后效性保证「不同来路拼出的方案互不重复」。于是原来在若干候选里挑最大的那一步,现在变成把若干候选的方案数全部加起来。
先用一个极小的例子热身。上 级台阶:走法有 、、 三种。 若这是最优题,我们会问「最少几步」(答案 2 步);而这里问「几种走法」,答案是 3——同一个状态骨架,读出的东西完全不同。 这一节就把计数型线性 DP 讲透:先是数楼梯(一维斐波那契计数),再深入整数划分(二维计数),最后点一句高精度这个计数题的老搭档。
数楼梯:f[i] = f[i−1] + f[i−2]
设 表示跳到第 级台阶的不同走法数。怎么递推?盯住最后一步: 站在第 级,上一步只可能来自两处——从第 级跨 1 级上来,或从第 级跨 2 级上来。 这两类走法不重不漏(最后一步的跨度不同,绝不会数成同一种),于是把两边的方案数相加:
地基要撒对:——「还没上台阶、原地站着」本身算一种走法(这颗 1 是所有计数的种子);——到第 1 级只有「跨 1 级」一种。 往后每格都是前两格之和,于是 长成 ——正是斐波那契数列。
这套「最后一步从哪来、把各来源方案数相加」的思路是计数 DP 的通法。换个场景就成了有界计数:如果每步能跨 级, 转移就扩成一段区间求和 ;如果每种「零件」还带件数上限,就是下面例题 P1077 摆花 那样的有限件计数。 把这类「枚举本步取什么、累加各分支」的骨架写成中文伪代码:
# 一维计数(数楼梯 / 有界跳跃)
f[0] = 1 # 地基:空走法算 1 种
for i = 1 to n:
f[i] = 0
for t = 1 to K: # 最后一步跨 t 级(数楼梯 K=2)
if i - t >= 0:
f[i] += f[i - t] # ★把 max 换成累加
# 有限件计数(第 i 种零件最多取 c 个,摆花即此形)
for i = 1 to n:
for j = 0 to m:
for k = 0 to min(c_i, j):
g[i][j] += g[i-1][j-k]本质 · 算子决定问题,骨架不动
计数 DP 和最优 DP 共用同一副状态与转移骨架——「最后一步从哪来」这套拆分毫不改变;变的只是如何聚合各来源: 最优用 挑一个,计数用 全加起来。两处硬改动记死:、地基 (空方案是唯一的起点火种)。 这与 A 部分的 背包综合变形是同一个道理——那里也是把背包转移的 换成 、,就从「最大价值」变「凑数的方案数」。
跟着算一遍
用 走一遍(每步跨 1 或 2 级),把方程跑起来,手上先猜答案该是 8:
看走法数一格一格叠出来
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
深化 · 整数划分:二维计数 dp[i][j]
数楼梯是一维计数。再上一层——整数划分:把正整数 写成若干正整数之和(无序, 与 算同一种),问共有多少种拆法。 比如 有 7 种:。 难点在「无序」:直接枚举会把 和 数两遍。破法是再加一维限制零件的大小,逼拆分只按「从大到小」这一种写法出现。
设 表示把 拆成若干正整数、且每个数都不超过 的方案数。对「最大能用的零件 」分两类:
左项 dp[i][j−1] = 完全不用 j · 右项 dp[i−j][j] = 至少用一个 j
不用 :那能用的数就收窄到 ,方案数正是 。至少用一个 :先拿掉一个 , 剩下的 仍可继续用 的数去拆(可以再用 ),方案数是 。两类不重不漏,相加即得。 边界 (把 0 拆开只有「空拆分」一种)。答案 (零件不限大小)。
常见陷阱 · 计数题常爆 long long,数楼梯更要高精度
方案数增长极快,随手用 会溢出——计数一律先想 ;题目要求取模的(如摆花 )则每步累加后立刻取模。 更极端的是数楼梯(例题 P1255): 时 有上千位,连 也远远装不下,必须写高精度(用数组逐位存、逐位进位相加)。 「计数 DP + 高精度」是一对常见搭档,见到「求方案数且 很大又不取模」就该警觉。
看整数划分的二维表填满
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
例题
#include <iostream>
#include <cstring>
using namespace std;
// 数楼梯:f[i] = f[i-1] + f[i-2],n≤5000 时结果远超 long long,必须高精度。
// 用 int 数组逆序存每一位(下标 0 = 个位),f[i] 由 f[i-1] + f[i-2] 逐位进位得到。
int n;
int a[5005][2005]; // a[i]:第 i 级走法数的高精度表示,a[i][0]=位数
void add(int *c, int *x, int *y) // c = x + y(高精度加)
{
int len = max(x[0], y[0]);
for (int i = 1; i <= len; i++)
{
c[i] += x[i] + y[i];
c[i + 1] += c[i] / 10; // 进位
c[i] %= 10;
}
if (c[len + 1] > 0)
{
len++;
}
c[0] = len; // 记录位数
}
int main()
{
cin >> n;
a[0][0] = 1; a[0][1] = 1; // f[0] = 1
a[1][0] = 1; a[1][1] = 1; // f[1] = 1
for (int i = 2; i <= n; i++)
{
add(a[i], a[i - 1], a[i - 2]);
}
for (int i = a[n][0]; i >= 1; i--) // 逆序输出每一位
{
cout << a[n][i];
}
cout << endl;
return 0;
}
// TAG: 线性DP 计数 斐波那契 高精度#include <iostream>
using namespace std;
const int MOD = 1000007;
int n, m, a[105];
int f[105][105]; // f[i][j]:前 i 种花恰好摆 j 盆的方案数
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
for (int j = 0; j <= m; j++) // 0 种花只有「摆 0 盆」1 种;j>0 无解
{
f[0][j] = (j == 0) ? 1 : 0;
}
for (int i = 1; i <= n; i++)
{
for (int j = 0; j <= m; j++)
{
for (int k = 0; k <= a[i] && k <= j; k++) // 第 i 种取 k 盆
{
f[i][j] = (f[i][j] + f[i - 1][j - k]) % MOD;
}
}
}
cout << f[n][m] << endl;
return 0;
}
// TAG: 线性DP 有界计数 摆花 取模#include <iostream>
using namespace std;
const int MOD = 2015;
int n, k;
int f[1005][1005]; // f[i][j]:1..i 的排列中恰有 j 处 a[t]<a[t+1] 的方案数
int main()
{
cin >> n >> k;
f[1][0] = 1; // 单个数:0 处上升
for (int i = 2; i <= n; i++) // 把新数 i 逐个插进已有排列
{
for (int j = 0; j < i; j++)
{
// 插进原有 j 个「上升位」之一或末尾(共 j+1 处)→ 上升数不变
long long same = (long long)f[i - 1][j] * (j + 1);
// 插进其余位置 → 新增一个上升位 → 由 j-1 处上升转来(j=0 时无此项)
long long grow = (j > 0) ? (long long)f[i - 1][j - 1] * (i - j) : 0;
f[i][j] = (same + grow) % MOD;
}
}
cout << f[n][k] << endl;
return 0;
}
// TAG: 线性DP 逐个插入 排列计数 不等数列
