一、状态数优化
1. 状态数的自简化
有时候,真正有用的 dp 状态非常少。通过观察状态的实际可达范围,可以主动缩小 dp 数组规模,从而减少求解时间。
例题 1:A96798.美丽数组
题意:给定数组 a,求所有长度为 k 的子序列 b 的美丽值之和。美丽值定义为:
1≤i<j≤kmin∣bi−bj∣
思路:
- 将 b 排序后,若相邻差的最小值为 x,则有:
bk−b1≥(k−1)x⇒x≤k−1bk−b1
因此美丽值的取值范围相对较小。
- 直接计算“美丽值恰好等于 x”较困难,改为计算“美丽值 ≥x”的方案数,再做差分更容易。
固定阈值 lim,定义:
dp[i][j]:在前 i 个数中,选了 j 个,且最后一个选的是 ai 的方案数
转移:
dp[i][j]=k<iai−ak≥lim∑dp[k][j−1]
优化:
- 对每个 i,满足 ai−ak≥lim 的最大可行 k 随 i 单调变化;
- 用双指针 + 前缀和维护可转移区间,即可把每次转移从 O(n) 降到 O(1)。
最终对每个 lim:
cnt[lim]=i=k∑ndp[i][k]
再对 cnt[⋅] 做一次差分即可得到“美丽值恰为 lim”的方案数,并累加贡献。
例题 2:A96797.Hack,还是不 Hack,这是个问题
题意:
- 有 3 道题,每题按过题人数决定分值(500,1000,…,3000)。
- 你可以 hack 某些人:每 hack 一次加 100 分。
- 每个人某题得分形如:
scorei=250xi(250−ti)
求最终排名的最小可能值。
思路要点:
- 总分最大 9000,因此 hack 次数最多 90;
- 能被 hack 的人数量也至多 90。
- 先枚举每道题的分值(等价于确定每题可 hack 人数),复杂度 O(73);
- 确定后“hack 越多越好”,你的分数可直接计算;
- 再用 dpi,j,k 统计每题 hack 次数固定时,你能达到的最小排名,时间复杂度约为:
O(303⋅90)
总体复杂度:
O(73⋅303⋅90)
例题3:[CSP-J 2025] 多边形
题意:给定 n 个小木棍的长度,求 3个及以上的小木棍的合集中,组成多边形的方案数。
思路:dpi,j:前i个小木棍当中,组成总和为j的方案数,其中 j 的取值可以很大,但是真正有用的取值只有0∼5000,因此任何超过 5000 的总和全部默认为 5001 。
2. 状态合并
在初步设计 dp 时,往往会设计一些“含义清晰但信息冗余”的状态。进一步分析后,若发现其中一些维度并不会影响后续决策,就可以合并这些状态,从而减少状态数。
例题:([CSP-S 2019] Emiya 家今天的饭)
题意:
给定一个二维矩阵a,定义 ai,j 代表了用第 i 种烹饪方法和第 j 种食材做的菜的种数,现有三条规定:
- 至少做一道菜
- 每个烹饪方法最多只能做一道菜
- 所有食材至多在一半的菜中出现
求做菜的方案数
思路:
首先约束所有的食材的出现次数非常的麻烦,于是考虑正难则反,有且仅有一种主要食材的出现次数会超过一半,考虑求解这一类的方案数即可。
顺着这点继续往下思考,我们可以依次枚举每一种主要食材(列),然后依次求出它们超过一半的方案数。接着我们需要知道当前这一列做了几道菜以及总共做了几道菜,于是就有了状态定义:fi,j,k 代表了前 i 行当中,选择了 j 道菜 , 其中有 k 道菜是当前食材做的方案数。
令si=∑j=1mai,j,于是总方案数即为:
tot=i=1∏n(si+1)−1
容易想到 DP 求不合法的方案数。我们枚举被选择的数量超过了总数量一半的那一列 (c),然后令 (f_{i,j,k}) 表示考虑前 (i) 行,总共选择了 (j) 个位置,第 (c) 列选择了 (k) 个位置的方案数。转移时考虑当前行不选、选第 (c) 列之外的位置或选择第 (c) 列的位置,即可得到转移方程:
fi,j,k=fi−1,j,k+fi−1,j−1,k⋅(si−ai,c)+fi−1,j−1,k−1⋅ai,c
转移时注意 (j) 和 (k) 从 (0) 开始枚举。最终答案就是
ans=tot−j=1∑nk=⌊2j⌋+1∑jfn,j,k
这么做的时间复杂度是O(mn3)的,还不足以通过本题,于是需要考虑使用优化。
我们会发现:fi,j,k 的后两维都是在描述相似的东西“选了几个”,而我们在乎的仅仅只是第 c 列有没有超过其他列的总数,至于其他列到底选了多少个我们并不在乎。因此我们考虑将两者合并成同一个意义,也就是第 c 列和其他列“相差了几个”。
我们可以将状态变为 fi,j,表示前 i 行,当前列的数比其他列的数多了 j 个,则有转移:
fi,j=fi−1,j+ai,c⋅fi−1,j−1+(si−ai,c)⋅fi−1,j+1
转移仍然是 O(1) 的,但总复杂度降为 O(mn2),可以通过此题。
3. 下标换元(状态重定义)
本节属于状态设计优化:通过状态重定义触发上文的“自简化”或“状态合并”,从而减少状态数。
3.1 状态换元
有时只需转换状态定义,就能把状态数显著压缩。
例题:A96793.Welcome24ever 和巧克力
题意:
给定数组 a 和正整数 k,构造数组 b,满足:
k≤i=1∏nbi
最大化:
k⋅∏i=1nai∏i=1n⌊biai⌋,n=100, ai≤107
思路:
朴素状态:
fi,j=前 i 个数,使 x=1∏ibx=j 的最优值
但 j 的范围是巨大的,显然不可行。
换元:
fi,j=考虑前 i 个数后,剩余需求为 j 的最优值
其中
j=⌈∏x=1ibxk⌉⟺x=i+1∏nbx≥j
转移:
fi,⌈bij⌉←fi−1,j×⌊biai⌋×ai1
处理取整:
⌈bij⌉=⌊bij−1⌋+1
并利用性质:
⌈y⌈xj⌉⌉=⌈xyj⌉=⌊xyj−1⌋+1
因此对任意转移,第二维始终形如:
⌊xk−1⌋+1
而 ⌊xk−1⌋ 的不同取值个数可用整除分块证明为 O(k),从而触发状态数自简化:
状态数 =O(nk)
接下来优化转移:不能枚举所有 bi。
对固定状态 fi−1,j,观察到 ⌈bij⌉ 的不同取值也很少(整除分块,约 O(j))。因此:
- 对每一种可能的 ⌈bij⌉ 分组;
- 在组内只取能使 ⌊biai⌋ 最大的 bi(通常取最大的 bi)。
于是单次转移复杂度变为:
O(j)
总体复杂度可写为:
On×t=1∑k(t+tk)=O(n⋅k0.75)
3.2 转成补集(容斥)
若题目中出现两个限制条件方向相反(≤ 与 > 同时出现),往往难以直接计数。可以对其中一个方向取补集,配合容斥把问题变成“同向限制”。
例题:([CSP-S 2025] 员工招聘)
题意:
给定长度为 n 的 01 串:
- 1 的位置可以录取人
- 0 的位置一定不能录取人
共有 n 个人,第 x 个人忍耐度为 cx。为所有人分配应聘顺序:若某人在应聘时,前方录取失败人数超过 cx,则他一定不会被录取。
求所有方案中,录取人数超过 m 的方案数。
思路:
预处理:只考虑 1 位置
0 位置对“能否录取”不产生可控贡献(必失败),可用“特殊元素优先处理”:
- 先把所有 1 位置安排并计数;
- 剩下的位置再乘上排列数补回。
下文“位置”均指 1 的位置。
定义:
- wi:前 i 个字符中 0 的个数
- ti:前 i 个字符中,有多少个 1 位置最终无法录取
- vi:第 i 个位置能录取所需的最小忍耐度
则:
vi=ti+wi
Step 1:先看 m=1
当“录取人数超过 1”,可先算“全都录取不了”的方案数 res,再做:
ans=∣Ω∣−res,∣Ω∣=n!
令计数函数:
S(p)=∣{x∣cx≤p}∣
若记第 r 个 1 的原下标为 posr,则“全不录取”的乘法计数可写成类似:
res=r=1∏k(S(posr−1)−(r−1))
其中 k 为 1 的总数。
Step 2:出现反向限制,用容斥统一方向
枚举某个状态 s(二进制表示哪些 1 位置录取):
- 若位置录取:要求 vi≤cx
- 若位置不录取:要求 vi>cx
两类条件方向相反,直接计数困难。定义事件 Ai:位置 i “无法录取”。则需求为:
i∈s⋂Ai
利用补集与容斥:
i∈s⋂Ai=∣Ω∣−i∈s⋃Ai
注意空集合 T=∅ 时:
i∈∅⋂Ai=Ω
并写成标准容斥形式(含空集):
i∈s⋂Ai=T⊆s∑(−1)∣T∣i∈T⋂Ai
做法:对每个状态 s,枚举其子集 T,计算 ⋂i∈TAi 并按 (−1)∣T∣ 加减。
Step 3:把重复的子集枚举用 DP 计算
在 Step 2 中,很多量会被重复计算,因此用 DP 代替“对每个 s 枚举所有 T”。
示例状态:
dpi,j,k:前 i 个 1 中,j 个钦定不录取,k 个因容斥而不录取
把容斥求和结构嵌入 dp 递推即可。
4. 最优性去状态
在最优化 DP 中,常可利用支配关系 / 单调性 / 等价路径删掉“不可能成为最优解”的状态,或只保留“代表状态”,从而降低复杂度。
例题:打家劫舍
设共有 n 个房屋,第 i 个房屋价值为 ai,相邻房屋不能同时打劫。
状态定义:
dpi: 在前 i 个房屋中,且打劫了第 i 个房屋时的最大价值
转移:
dpi=x≤i−2max(dpx)+ai
关键性质:
dpi≥dpi−2+ai≥dpi−2
因此:
- 与 i 同奇偶的最大值一定在 dpi−2
- 与 i 异奇偶的最大值一定在 dpi−3
从而:
x≤i−2max(dpx)=max(dpi−2,dpi−3)
优化后转移:
dpi=max(dpi−2,dpi−3)+ai
只需维护少量历史状态即可。
例题 2:[NOIP2023] 天天爱打卡
题意:
一共有 n 天,大 Y 可以花费 d 的能量在任何一天跑步,但大 Y 不能在连续的 k 天跑步。
此外,将给出 m 段任务区间 [li,ri]。如果在这段区间的每一天都跑了步,则会获得 vi 的能量。
大 Y 的初始能量为 0,请你最大化跑完步后大 Y 最终的能量。
思路:
有一个最朴素的状态设计方案就是 fi 表示考虑前 i 天并且第 i 天跑步的最大能量值 ,然后定义 gi=j=1maxifj
转移方程:
fi=j=i−k+1maxi⎩⎨⎧gj−2−(i−j+1)⋅d+[lp,rp]⊆[j,i]∑vp⎭⎬⎫
这样子直接暴力转移的时间复杂度是O(nmk)的。
可以想到,对于枚举的区间 [j,i],能够贡献答案的任务区间 [lk,rk] 一定满足j≤lk≤rk≤i。
现在我们将区间 [li,ri] 看作是二位平面上的点 (ri,li) , 那么我们就能把后面的暴力求和变成求解二维平面上矩形的点权和,于是乎变成了一个基础的二维数点问题。
那么我们按照右端点顺序把任务区间加入,则 rk≤i 的条件就能天然满足。计算贡献时,我们就只需要查询满足 lk≥j 的任务区间的权值和即可,这个可以用树状数组维护,单次计算贡献的复杂度降至 O(logm),整体时间复杂度O(nklogm)
接下来考虑优化 n ,由于最终的得分和完成的任务以及跑步的时间有关,因此会发现在一个区间开始或结束的时间开始跑步是一定不劣的,在一个区间的结束的时间完成跑步也一定不劣。因此我们的决策点就只有每个任务的 li,ri 两个点,这样就可以大大的减少状态数量,时间复杂度也就成了 O(mklogm) ,这就是通过最优性质来保留一定不劣的点,减少状态数。
最后,还剩下一个瓶颈没有解决:对于每一个端点都要枚举前 k 个。所以我们需要接着优化 dp 转移
转移为
dpi=1≤j≤ipi−pj+1≤kmax{gpre(j)+gain(i,j)−cost(i,j)}.
另外,由于相邻关键天数之间可能存在空档天,需要区分“pj−1 与 pj 是否相邻”:
pre(j)={j−2,j−1,j≥2 且 pj=pj−1+1,否则.
最后有
gi=max(gi−1,dpi).
接下来我们考虑从 i→i+1的过程中,每个决策点的 gpre(j) 不变,cost(i,j) 变化量一样,gain(i,j) 的变化量取决于 j 的大小,也就是说实际上这是一个区间修改+区间查询的问题,很自然想到用线段树来维护每个决策点 gpre(j)+gain(i,j)−cost(i,j) 的取值,然后在进行区间查询最值即可,查询范围可以双指针快速解决。时间复杂度为O(mlogm)
5. 最优性换维
把原来 DP 里“状态的一维”挪到“dp 记的值”里(或者反过来),从而:
- 把难维度(范围大/不好压)变成 dp 值(通常记最小/最大),
- 把好维度(范围小/可二分/只关心最左最右)留在状态里,
- 最终把复杂度从
O(大维度) 换成 O(小维度) 或 O(小维度 log …)。
具体而言:
你可以把 DP 看成一个函数:
-
原写法:
f(情况/状态)=在这种情况下最优的(最大/最小)值
例如:f[w] = 重量不超过 w 时最大价值
-
换维写法:
g(目标最优值)=要达到这个值,状态量至少/最多要多少
例如:g[v] = 达到总价值 v 所需的最小重量
但是,也不能无脑的进行换维:
因为你换维之后,经常会出现这种直觉陷阱:
- 你只保留了“达到某个值的最小资源”这一个最优信息;
- 但你必须证明:
以后做转移时,只需要知道这个“最小资源”就够了,不会因为丢了别的信息导致错过全局最优/合法性。
通常证明套路是:
对任意最优解,取“最后一步/最后一个物品/最后一段”,前面部分必然也是对应子问题的最优(否则就能替换得更好,矛盾)。
例题1:A94852.Knapsack 2
题意:n 个物品,每个有重量 w[i]、价值 val[i],背包容量 W,最大化总价值。
常规 DP(容量当维度)
dp[cap] = 最大价值
复杂度 O(nW),但如果 W 很大(比如 1e9)就炸了。
换维 DP(价值当维度)
令 S = sum(val)(如果价值和不大,比如 1e5),定义:
dp[v] = 达到总价值 v 的最小总重量
- 初始化:
dp[0]=0,其他是 +inf
- 转移(倒序 v):
dp[v + val[i]] = min(dp[v + val[i]], dp[v] + w[i])
最后答案是最大的 v,使得 dp[v] <= W。
这就是“定义域值域互换”:从“容量 -> 最大价值”换成“价值 -> 最小容量”。
例题2:LIS 最长上升子序列(“二分性/最左1”压维)
题意:给数组 a,求 LIS 长度。
常规 DP(O(n2))
f[i] = 以 i 结尾的 LIS 最长长度
换维/压维写法(O(nlog n))
定义:
dp[len] = 长度为 len 的上升子序列,其“最小可能结尾值”
性质:dp[len] 随 len 单调递增(或至少可二分定位)。
对每个 a[i],找最大的 len 使得 dp[len] < a[i],更新 dp[len+1]=min(dp[len+1], a[i])。
这里就是:
- dp 值关于某一维具有二分性(能二分找位置),
- 或者“只需关注最左/右的 1”:
你可以把“能否形成长度 len”想象成一排 0/1,可行的 len 是前缀连续的 1,只要知道“最右的 1 在哪”就够了。
二、转移优化
在优化前先说明两种 dp 转移类型
- 填表法 (dpi←∑x=1i−1dpx) ,常规的,利用前方已知信息来直接求出$ dp_i$
- 刷表法 (dpi+1←dpi+1+dpi) ,当你求出了 $ dp_i$ 时,将 $ dp_i$ 这个值分配到后续的 dp 当中。
针对不同的转移类型,会有不同的转移优化,例如在填表法中常见的优化有前缀和,在刷表法中常见的优化有维护差分数组,刷表法常在计数类型动态规划中出现。
1.前后缀优化
此处不过多赘述,主要就是维护前缀/后缀的最值/总值/第k大,进而实现快速转移。
2.多步拆单步

类似于图论当中的“建虚点”,在此处可以通过建立另外的数组来记录值,然后进而快速转移,上文的前后缀优化也可以看作是多步拆单步。
3.倍增
倍增适用于转移轮数很大(n=109) ,并且转移方程和转移轮数无关的情况,经典问题有各种矩阵乘法,完全背包。
例题:「NOI2020」美食家
4. ds 优化 dp 转移
根据转移方程选取合适的ds实现转移,例如上文[NOIP2023] 天天爱打卡:fi=j<imaxfj+g(i,j) ,需要实现区间修改和区间最值,那么用线段树来实现快速转移会很合适。
5. 决策单调性优化