DP 优化方法大杂烩 I.

前方标 * 的是推荐阅读的部分或推荐做的题目。

由于文章内容过多,为分摊压力,更多内容详见 DP 优化方法大杂烩 II.

CHANGE LOG

  • 2021.12.21:计划重构整篇文章。当前重构至矩阵快速幂。
  • 2021.12.22:重构至 wqs 二分。
  • 2021.12.23:施工完毕。
  • 2022.1.25:第二遍重构文章,修改表述。
  • 2022.2.11:施工完毕。

0. 前言

动态规划是 OI 界的一个博大分支,因而衍生出了很多优化方法。最基本的是对 状态设计 的优化。如果状态从三维降到了两维,甚至一维,将对时间复杂度和常数产生巨大影响。

可惜的是,对状态设计的优化并没有一般性方法,全凭做题经验与观察性质的能力。文章涉及到的方法大都是对 如何转移 的优化,如根据决策单调性尝试分治。

一般用 tD / eD 描述动态规划 规模 的类型,其中 t 表示问题大小,e 表示转移时依赖子问题的大小。即状态数 nt,每个状态依赖于 ne 个前驱状态的信息。除非问题有特殊性质,解决 tD / eD 动态规划需要 O(nt+e) 的时间复杂度。

  • 1D / 1D:最长上升子序列。
  • 2D / 0D:最长公共子序列,普通背包问题。
  • 2D / 1D:多源最短路 Floyd。

1. 动态 DP

动态 DP 简称 DDP(Dynamic Dynamic Programming),其本质是用 矩阵 维护带修改的动态规划问题。

1.1 矩阵描述转移

部分动态规划转移方程涉及到的状态较少,且一个状态由其前驱的 线性组合 得到。其实并不一定需是线性组合,只需满足 结合律,见下方说明。此时可以用 矩阵乘法 描述转移方程。

斐波那契数列 fi=fi1+fi2 即一例。求解 fi 只需知道 fi1fi2,故记录 Fi,0 表示 fiFi,1 表示 fi1,转移方程即

Fi,0=Fi1,0+Fi1,1Fi,1=Fi1,0

注意到 FiFi1线性组合,因此

[Fi1,0Fi1,1]×[1110][Fi,0Fi,1]

这就是矩阵描述转移。

当转移系数如点权或边权带修时,重新 DP 的复杂度不可接受,考虑用数据结构如线段树维护 区间转移矩阵乘积 可支持单点修改。此方法还可快速计算一段区间或一棵子树的 DP 值,非常优美。DDP 也有局限性,它对转移方程要求较高。

说明:可以用矩阵描述的转移方程不一定必须是前驱的线性组合。广义 矩阵乘法

Ci,j=k=1nAi,kBk,j

只需满足 具有 结合律,且 分配律,则存在结合律。设三个 n 阶方阵 A,B,C 相乘,有

 q=1n(AB)i,q×Cq,j= p=1n(q=1nAi,pBp,q)Cq,j= p=1nq=1nAi,pBp,qCq,j(distributive law)= p=1nq=1nAi,p(Bp,qCq,j)(associative law)= p=1nAi,p(q=1nBp,qCq,j)(distributive law)= p=1nAi,p×(BC)p,j

常见广义矩阵乘法如 min,+ 卷积,max,+ 卷积,因为 a+min(b,c)=min(a+b,a+c),即加法对 min 有分配律。图论的邻接矩阵在 or,and 卷积下的 k 次方描述了每个点出发恰好走 k 步到达的节点。

1.2 算法介绍:树链剖分写法

接下来,我们以 P4719 为例,深入剖析一下动态 DP 的一般树剖写法与诸多细节。

若没有修改操作,本题是经典的 树上最大独立集 问题,详见没有上司的舞会。设 fi,0/1 分别表示 不选i 的最大权值,有

fi,0=xSimax(fx,0,fx,1)fi,1=vi+xSifi,0

加上修改操作,一般想法是用矩阵表示转移方程,再用数据结构维护。注意到一个节点可能有非常多的儿子,因此三方级别的矩阵乘法就凉了。

我们利用 矩乘与 ds 能够 快速转移方程 的特点,与 树链剖分 从根到任意节点的轻边个数级别为 O(log) 的优秀性质,设计新的状态与转移方程。它应满足从一个点的 重儿子 转移到该点的方程可用矩阵描述。

首先,对树 T 进行树链剖分,记 sii 的重儿子。设 gi,0/1 分别表示 不选i只能从轻儿子 转移的最大权值,有

gi,0=uson(i)usimax(fu,0,fu,1)gi,1=vi+uson(i)usifu,0fi,0=gi,0+max(fsi,0,fsi,1)fi,1=gi,1+fsi,0

定义 广义 矩阵乘法 A×BC 表示 Ci,j=maxk=1nAi,k+Bk,j。由于 max+ 具有分配律:max(ai+bj)=maxai+maxbj,而 + 本身具有结合律,因此这样的矩阵乘法具有结合律,是优秀的。这样,我们有

[fsi,0fsi,1]×[gi,0gi,1gi,0][fi,0fi,1]

f,g 需要 预处理(其实 f 不需要,但是预处理 g 依赖于 f)。记点 i 从其重儿子转移上来的矩阵为 Gi,考虑支持修改操作:

我们注意到,一次修改仅改变 O(log) 级别个节点的 G。这是因为仅在 轻儿子被修改时 需要更新 G,而 iR 的路径上只有 O(log) 条轻边。这样,我们不断跳重链,并实时维护每条 重链顶端节点 t 的父节点 fatGfat

思考到这一步,我们遇到了所有 DDP 都需特别注意的关键问题:如何在修改点 i 轻儿子的 f 后维护该点的 Gi。注意到每个轻儿子对于父亲的 G 的贡献以所有轻儿子的 f 的和的形式计算,它满足 可减性,因此只需在修改前先计算原来的 f,将其贡献从 gtpi 中减掉,再加上更新后的贡献即可。

如何求 f:注意到重链末端是叶子节点,而叶子节点没有 si,所以 fi=gi (ileaf(T))。因此一个点的 f 等于 它所在重链末端到它的矩阵积。记录 edi 表示 i 所在重链末端叶子节点,Di 表示 i 的 dfn,fx 可由 i=DxDedxGi

上述过程要求我们在更新 Gi 时先 不要 在线段树上修改,因为我们还需 原来的 Gi 求出 ftopi,以从 Gfatopi 扣掉其贡献。这一要求体现在代码中即,设当前节点为 x,则将 gfatopx 减掉 ftopx 即从 x 所在重链末端 edx 一直到重链顶端 topxG 的乘积,在线段树上更新 Gx,再算出新的 ftopx 加到 gfatopx 上。

综上,时间复杂度线性对数平方,乘以矩阵乘法的时间,常数非常大。

const int N = 1e5 + 5;
const int inf = 1e9;

struct Matrix {
	int a, b, c, d;
	Matrix operator * (Matrix x) { // 广义矩阵乘法
		Matrix y;
		y.a = max(a + x.a, b + x.c);
		y.b = max(a + x.b, b + x.d);
		y.c = max(c + x.a, d + x.c);
		y.d = max(c + x.b, d + x.d);
		return y;
	}
} I, ans, G[N], val[N << 2];

int n, m, a[N], f[N][2], g[N][2];
int cnt, hd[N], nxt[N << 1], to[N << 1];
void add(int u, int v) {nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v;}
int dn, sz[N], fa[N], dep[N], son[N], dfn[N], rev[N], top[N], ed[N];
void GenMat(int x) {G[x].a = G[x].c = g[x][0], G[x].b = g[x][1], G[x].d = -inf;}

void dfs1(int id) {
	f[id][1] = a[id], sz[id] = 1, dep[id] = dep[fa[id]] + 1;
	for(int i = hd[id]; i; i = nxt[i]) {
		int it = to[i];
		if(it == fa[id]) continue;
		fa[it] = id, dfs1(it), sz[id] += sz[it];
		f[id][1] += f[it][0], f[id][0] += max(f[it][0], f[it][1]);  // 先处理出 f
		if(sz[it] > sz[son[id]]) son[id] = it;
	}
}
void dfs2(int id, int tp) {
	g[id][1] = a[id], top[id] = tp, rev[dfn[id] = ++dn] = id;
	if(son[id]) dfs2(son[id], tp), ed[id] = ed[son[id]];
	else ed[id] = id;
	for(int i = hd[id]; i; i = nxt[i]) {
		int it = to[i];
		if(it == fa[id] || it == son[id]) continue;
		dfs2(it, it), g[id][1] += f[it][0], g[id][0] += max(f[it][0], f[it][1]); // 再处理出 g
	}
}

struct SegTree {
	void build(int l, int r, int x) {
		if(l == r) return val[x] = G[rev[l]], void();
		int m = l + r >> 1;
		build(l, m, x << 1), build(m + 1, r, x << 1 | 1);
		val[x] = val[x << 1 | 1] * val[x << 1];
	}
	void modify(int l, int r, int p, int x) {
		if(l == r) return val[x] = G[rev[p]], void();
		int m = l + r >> 1;
		if(p <= m) modify(l, m, p, x << 1);
		else modify(m + 1, r, p, x << 1 | 1);
		val[x] = val[x << 1 | 1] * val[x << 1]; // 右乘左儿子
	}
	void query(int l, int r, int ql, int qr, int x) {
		if(ql <= l && r <= qr) return ans = ans * val[x], void();
		int m = l + r >> 1;
		if(m < qr) query(m + 1, r, ql, qr, x << 1 | 1); // 先递归右子树
		if(ql <= m) query(l, m, ql, qr, x << 1);
	}
} tr;

int modify(int x, int val) {
	g[x][1] += val - a[x], a[x] = val;
	while(top[x] != 1) {
		int tp = top[x], ft = fa[tp];
		ans = I, tr.query(1, n, dfn[tp], dfn[ed[tp]], 1);
		g[ft][1] -= ans.a, g[ft][0] -= max(ans.a, ans.b); // 先减掉 x 重链顶端父节点 ft 的 x 所在的轻儿子的 f 的贡献
		GenMat(x), tr.modify(1, n, dfn[x], 1); // update
		ans = I, tr.query(1, n, dfn[tp], dfn[ed[tp]], 1);
		g[ft][1] += ans.a, g[ft][0] += max(ans.a, ans.b), x = ft; // 再加回去
	} GenMat(x), tr.modify(1, n, dfn[x], 1);
	ans = I, tr.query(1, n, 1, dfn[ed[1]], 1);
	return max(ans.a, ans.b);
}
int main(){
	cin >> n >> m;
	for(int i = 1; i <= n; i++) a[i] = read();
	for(int i = 1, u, v; i < n; i++) add(u = read(), v = read()), add(v, u);
	dfs1(1), dfs2(1, 1);
	for(int i = 1; i <= n; i++) GenMat(i); tr.build(1, n, 1);
	for(int i = 1, x, y; i <= m; i++) x = read(), print(modify(x, read())), pc('\n');
	return flush(), 0;
}
  • 注意点 1:由于 dfn 大的节点深度大,且转移顺序 自下而上,所以线段树维护时应用 右区间乘左区间,查询时也要先向右区间递归。这与写法习惯有关。用行向量右乘矩阵和用列向量左乘矩阵,线段树 push_up 时的区间顺序不同。总之,读者需要根据实际意义理解并实现。
  • 注意点 2:就算初始全是单位矩阵 I,也要 初始化,因为大部分情况下 I[0000]
  • 技巧 1:当矩阵乘法仅有某些位置上的值为非常数时,仅维护这些位置的值可以有效减小常数,如切树游戏。
  • 技巧 2:一般情况下信息满足可减性,即直接从 gfai 中扣掉 fi 的贡献。不满足可减性时可以使用线段树维护带修半群元素积,但似乎如果没有可减性,我们甚至无法用矩阵描述转移,笔者也没有见过这样的题目。

1.3 全局平衡二叉树实现

等学会了 LCT 再来填坑。

upd:可能永远也不会填了,因为永远也不打算学 LCT。

1.4 例题

I. P4719 【模板】"动态 DP" & 动态树分治

动态 DP 的模板题。

II. CF750E New Year and Old Subsequence

题意简述:多次询问一个字符串的子串 [l,r],求至少删去多少字符后才能使得其不包含子序列 2016 而包含 2017n,q2×105

大力子序列自动机上 DP:设 fi,0/1/2/3/4 分别表示包含 /2/20/201/2017 且不包含 2016[1,i] 中最少需要删去多少个字符,有:

{fi,0=fi1,0+[si=2]fi,1=min(fi1,1+[si=0],fi1,0[si=2])fi,2=min(fi1,2+[si=1],fi1,1[si=0])fi,3=min(fi1,3+[si=7si=6],fi1,2[si=1])fi,4=min(fi1,4+[si=6],fi1,3[si=7])

对于直接接在 f 后面而非 + 后方的 [],如果艾佛森括号中的条件不成立不能转移。由于转移维度非常小,仅有 5,故可将上式改写成 min+ 广义矩阵乘法形式。

每次询问用初始向量 [0][l,r] 用线段树维护的区间矩阵积,得到的 fr,4 即为所求。若 fr,4= 则无解。

不难看出实际上 i=lrGi 的第一行就是 fr,其中 Gifi1fi 的转移矩阵,所以直接求出矩阵区间矩阵积也可。不过用向量乘矩阵 f×G 可以做到平方,而该方法 G×G 必须三方,常数较大。时间复杂度 O(k2(kn+qlogn)),其中 k 是转移维度,本题中为 5

*III. P3781 [SDOI2017]切树游戏

题意简述:给出 n 个节点的树,点有小于 m=27 的权值 vi,权值带修。多次询问有多少非空子连通块满足所有点权值异或和为 kn,q3×104

首先设计 DP,设 fi,x 表示以 i 为根的子树异或和为 x 的方案数。合并 u 及其儿子 v 时,我们有转移方程

fu,jfu,j+xy=jfu,xfv,y

每个节点 u 的初始值为 fu,j=[vu=j]。转移方程是异或卷积,两边同时取 FWT,得到 FuFu(Fv+1)。这样,O(nmlogm) 预处理每个节点的初始 FWT 值,单次对整棵树进行一遍树形 DP 的复杂度为 O(nm)。时间复杂度 O(nmq)

考虑套上动态 DP,设 gi,x 表示从 i轻儿子 中选择若干个子树使异或和为 x 的方案数,那么有转移方程

gu,jgu,j+gu,xfv,y(vheavy son(u))

由于我们要查询全树的 fi,x,所以还需记录 hi,x 表示 usubtree(i)fu,xlhi,x 表示 ulightson(i)hu,x。设 Fi,LFi,Hi,LHi 分别表示 fi,gi,hi,lhi 的异或生成函数,有转移

Fi=LFi×(Fsi+x0)×xviLFi=xvi×ulight son(i)(Fu+x0)Hi=LHi+Hsi+FiLHi=ulight son(i)Hu

[FsiHsix0]×[LFi×xviLFi×xvi0010LFi×xviLHi+LFi×xvi1][FiHix0]

树剖线段树维护。修改需要实时更新 LFLHLH 可以直接减,LF 需要维护乘以 0 的个数与非零值之积,因为乘以 0 时丢失了信息,不可逆。矩阵乘法的常数优化见 基于变换合并的树上动态 DP 的链分治算法。视 n,q 同阶,时间复杂度为 O(nmlog2nk3),其中 k=2

洛谷上树剖被卡了,必须使用全局平衡二叉树。LOJ 可以通过。

IV. P6573 [BalticOI 2017] Toll

究极套路题。注意到同一层大小仅有 k 但层数很多,不难想到用矩阵描述每层之间的转移,用线段树支持查询,甚至不需要修改。用向量乘矩阵优化,时间复杂度 O(nk3+ok2logn)

V. P7359 「JZOI-1」旅行

裸题。不带修所以不需要线段树,直接倍增即可。时间复杂度 O((n+q)V3logn),其中 V=2

VI. LOJ #3539. 「JOI Open 2018」猫或狗

给定一棵 n 个节点的树,节点有权值 0/1/2。带修权值并查询使所有权值为 1 的点与权值为 2 的点不连通最少需要割掉的边数。1n105

启示:连通性相关树上最优化问题考虑将连通性作为 DP 的维度。设 fi,0/1 表示使 i 不与 1/2 连通的最小代价。转移显然:

fi,0=[vi=1]×(+)+json(i)min(fi,0,fi,1+1)

对于 fi,1 同理。带修点权考虑动态 DP,时间复杂度线性对数平方。细节:轻儿子对 f 的贡献以和的形式计算,满足可减性,因此不需要线段树维护带修半群元素积。

2. 矩阵快速幂优化

2.1 算法简介

矩阵快速幂优化 DP 与动态 DP 的本质思想相同,都是用矩阵描述转移方程。不同的是,前者每一轮的转移方程相同,可以快速幂优化,后者则是套上各种数据结构支持修改。

仍然以斐波那契数列为例,因为每一轮转移的矩阵都是 G=[1110],因此转移 n 轮的矩阵就是 Gn。即 [fifi1]×Gn[fi+nfi+n1]。由于矩阵乘法具有 结合律,因此可以快速幂优化,从而在对数时间内求解问题。

2.2 常见技巧

  1. 拆点:对于图上转移,当边权不为 1 时,需要把每个点拆成 w 个点,w 是边权。因为 fi,j 会从 fu,jw 转移,所以每个点要维护 w 个时刻的信息。如例 VII. 和 VIII.
  2. 向量乘矩阵:对于多组矩阵快速幂询问且转移矩阵 G 相同,我们预处理 G2 的幂次方 Di=G2i。我们知道向量乘矩阵可以做到平方,因此单次查询 A×Gi 在快速幂过程中就不需要 G 自乘,只需要向量乘矩阵,时间复杂度 O(k2(k+q)logV)。如例 VIII.
  3. 对于转移中存在的特殊时刻,可以配合技巧 2 做到特殊点数量 ×k2logV 的时间复杂度,如例 VIII. 与 IX.。

2.3 例题

*I. P3176 [HAOI2015]数字串拆分

题意简述:定义 f(s) 表示将 s 拆分成若干个不大于 m 个数的方案数。给出数字字符串 t,求 g(t)=f(x),其中 x 为将 t 分割成若干个允许有前导零的数后它们的和。例如 t=12 时,答案为 f(1+2)+f(12)|s|500,m5

注意到 m 的范围很小,仅有 5,所以 f(s) 可以用矩阵快速幂 m3logs 计算。记转移矩阵为 G,那么答案即为 (xGx)0,0。我们记 Di 表示 g(t[1:i])=Gx(注意区分 gg,前者是数值,后者是矩阵),则答案为 (Dn)0,0

不难写出转移方程 Di=j=0i1dj×f(t[j+1:i])。因为 t[j+1:i] 会很大,达到 10n 级别,所以快速幂的时候需要对指数高精:矩阵没有费马小定理!时间复杂度为 O(n3m3),无法承受。

注意到 f(t)=Gt=i=1|t|G10|t|itit 是字符串表示的十进制数,t1 为最高位),因此我们可以预处理出形如 Gc×10k (1c<10,0k<n) 的矩阵。记 Dl,rf(t[l:r]),它可以由 Dl+1,r×G10rltl 得到。

nm3|Σ|+n2m3 预处理后即可 n2m3 DP:di=j=0i1djDj+1,i。其中 Σ 表示字符集即进制数,本题中为 10

*II. CF576D Flights for Regular Customers

好题!将所有边按照 di 排序并依次加入,并实时维护经过 K 条边后能够到达的所有点。

设答案向量为 AAi=0/1 表示能否走到 i。有初始状态 A1=1。设当前走了 K 步,先让 A 乘以 GdjK 次方,其中 j 是下一条要加的边。加进去后,判断走 dj 步能到达的点到 n 的最短距离 Dj,所有 dj+Dj 的最小值即为所求。因为尽管 dj<dj+1,但并不一定满足 dj+Djdj+1+Dj+1,故不能在第一次连通时就认为找到答案。

根据或对与的分配律,使用矩阵快速幂解决。01 矩阵乘法用 bitset 优化,时间复杂度 O(nm3logdω)代码

III. P1707 刷题比赛

有点裸,不过可以用来熟悉矩阵快速幂。

*IV. P4569 [BJWC2011]禁忌

题解

V. P5059 中国象棋

注意到每一行是独立的,且方案数为 fibn+3(n+2):设 fi 表示第 i 列(从 1 下标开始,共有 n+1 列)不放棋子且任意两个不放棋子的格子间隔不超过 2 的方案数,则有 f0=f1=1fi=fi1+fi2, 则 fi=fibi+1。总方案数为 fn+1+fn=fn+2=fibn+3,再减去全部不放的 1 种和只有一个放的 n+1 种即可。故答案为 (fibn+3n2)n+1。矩阵快速幂计算 fib。注意 P1018,慢速乘。时间复杂度 O(log2n)

VI. P1397 [NOI2013] 矩阵游戏

比较裸的矩阵加速 DP。我们没有办法快速十进制转二进制(复杂度 log2n 直接爆炸),所以只能十进制预处理。时间复杂度 O(log10n),有 10×23=80 的常数。

VII. P3597 [POI2015]WYC

非常显然的矩阵快速幂,由于边权只有 3 所以拆点:存储 fi,j,fi,j+1fi,j+2 表示以 i 结尾的长度为 j,j+1,j+2 的路径数量。最后再记一个 sum 表示路径总数,得到转移矩阵 B。预处理出 B1,B2,,B61,然后倍增求解即可。时间复杂度 O(n3logk),有 27 倍常数。注意矩阵每个数不可以取模,在任何时候都要对 kmin

*VIII. P6772 [NOI2020] 美食家

结合各种矩阵快速幂的常见优化技巧,拆点 + 预处理矩阵的 2 的幂次方,容易做到 O((5n)2(5n+k)logT),复杂度与 m 无关。

*IX. AT2371 [AGC013E] Placing Squares

考虑平方的组合意义实在是太神仙了,考虑维护 gi,k=0j<ifj(ij)k,其中 fi 表示 [0,i] 的答案。转移把柿子写出来,用矩阵快速幂优化即可,注意要分标记点和非标记点讨论。时间复杂度 m×33×logn

3. 状态压缩优化

状态压缩优化应用于以集合为状态的动态规划中。此时用 0/1 表示某个元素是否在集合中,就可以用一个二进制数表示所有状态。

3.1 常见技巧

  1. 枚举子集:如果一个集合状态 S 由其所有子集 TS 转移得到,这样转移的时间复杂度为 i=0n(ni)2i=3n。代码实现形如:
for(int T = (S - 1) & S; ; T = (T - 1) & S) {
	......
	if(!T) break; 
}
  1. 进阶:高维前缀和(SOSDP)和子集卷积(Subset Convolution)本质上也是一种状压 DP。关于 SOSDP 详见 位运算卷积,子集卷积与高维前缀和

状压 DP 并没有一个很固定的模板,因此本质上仅是用二进制表示集合(或划分)的技巧,而非 DP 的优化方法。此外,状压 DP 是 插头 DP 的基础。

3.2 例题

高难度例题:II, VI, VII, VIII.

I. P1357 花园

注意到 m,k 非常小而 n 非常大,明示矩阵快速幂。首先考虑朴素 DP:状压,记 fi,S 为位置 i 及其前 m1 个位置所表示状态为 S 时的方案数,转移枚举下一位是 C / P,判断合法性。

由于是环形 DP,所以枚举一开始 m 个位置的状态 S,初始值 fm,S=1,那么 ss+fn+m,S。总时间复杂度 O(2mlogn(2m)3)=O(16mlogn)

实际上最终答案可以由转移矩阵 Gn 次方的对角线求和得到:fm,Sfn+m,S 有贡献。复杂度降至 O(8mlogn)

*II. AT695 マス目

题解

*III. ACM/ICPC Regional Aizu 2013 Hidden Tree

注意到一棵 balanced tree 上所有数都是最小数乘以 2 的幂,因此,对于不同的 p×2r (2p) 我们单独处理。

计算每个数中有多少个质因子 2,记做 r,状压 fi 表示当前所有数的和为 i 时,最多的叶节点个数。按位置顺序枚举所有 2r,并令 fk+2rmax(fk+2r,fk+1) (2rk)

后面的条件是因为加入 2r 之后其后面比 2r 小的部分就没法跨过 2r 和前面比 2r 小的部分互相抵消。记 c=log2n+maxr,最大约为 8+9=17,则时间复杂度为 O(Tn2c)

IV. 2021 联考模拟北大附 瘟疫公司

给出一张 n 个点有边权的无向图。定义点集 S 的代价 cS 为连通这些点的最小代价和最大代价异或和。试确定一个点集序列 S1S2Sk 满足 S1 包含 1 个点且 Sk 包含所有点,最小化 i=2k|SiSi1|×cSin20m100

首先 kruskal 2n(nlogn+m) 求出每个点集 S 的代价 cS,我们有显然的状态设计 fS 表示扩展为 S 的最小代价。朴素暴力是 fS=minTSfT+cS×|ST|,直接计算的复杂度是 O(3n)。但注意到一个关键性质:我们只关心 T 的大小而非具体包含了哪些元素,这启发我们修改状态设计 gi,S 表示 S 大小为 i 的子集 TminfT。转移即

gi,S={minpSgi,Spi<|S|min0j<|S|gj,S+cS×(ij)i=|S|

时间复杂度 O(2n(n2+m))

启示:DP 状态的设计时尽量 忽略无用信息,抓住 对转移有关键影响的量。思路:求出代价 设计朴素暴力 发现只有子集大小有关 将子集大小加入 DP 状态。

V. P5911 [POI2004]PRZ

状压枚举子集,时间复杂度 O(3n)

*VI. CF1463F Max Correct Set

考虑问题的弱化版本。当 x=y 时,我们可以构造出一组最优解 1111000011,即 x1x0 打包在一起。

因此,对于更一般的情况,我们猜测最优解存在循环节 x+y。对于前 min(n,x+y) 个位置做状压 DP,时间复杂度 O(2max(x,y)(x+y))。证明略。代码

*VII. CF1152F2 Neko Rules the Catniverse (Large Version)

神仙题。按位置的顺序 DP 显然行不太通,注意到对于所有相邻位置均有值域上的限制,因此考虑 按值域从小到大 DP

不妨设当前值为 i,考虑要知道哪些信息才能转移。首根据长度限制 k,我们得及时终止,所以要知道序列里已经放了多少个数。记录 j 表示序列长度为 j。根据值域限制,只有权值 im 的位置之后或序列的开头才能放。因此,为求出将 i 放入序列的方案数,记录 S 表示 im 的权值有哪些在序列中,这是一个状压形式的维度。

转移就很简单了,方案数作为系数直接乘上去:

fi,j,S×(popcount(S)+1)fi+1,j+1,(2S+1)&(2m1) (i<nj<k)fi,j,Sfi+1,j+1,(2S)&(2m1) (i<n)

由于 n 很大,所以直接 DP 不太行。但是 k×2m 很小,所以直接矩阵快速幂即可。时间复杂度 O(k38mlogn)代码

*VIII. CF1342F Make It Ascending

题目相当于求将序列 a 划分为若干集合 S1,S2,Sc,集合之间有序,满足 Si 之和小于 Si+1 之和(单调递增),且存在一个单调上升序列 p 满足 piSi,即我们最终会将 Si 所有其它元素累和到原序列位置为 pi​ 的元素上。求最多划分出多少集合。

朴素的想法是将所有限制全部装进 DP 里面,即设 fi,j,k 表示当前考虑了 mask 为 i 的数集,最后一个划分出的集合对应的 p 的最小值为 j,且总和为 k 时,最多划分出多少集合。转移根据实际意义显然。

这样复杂度太大了,因为我们把值域装了进去。考虑如何把值域去掉。由于若 k>kfi,j,kfi,j,k,则前者显然无用。这说明 对于相同的 fi,j,k,仅有最小的 k 有用。根据这一性质,我们尝试 忽略无用状态,交换 DP 的某一维度和值域。这意味着,我们设 fi,j,k 表示当处于上述意义下的 i,j 以及 集合个数k 时,最后一个集合的和的最小值。转移即在 i 这一维枚举子集,同时枚举 j,k

转移条件有三个:

  1. pSap>fi,j,k
  2. 存在 pS 使得 p>j。设 pmin 为最小的这样的 p
  3. iS=

fi,j,k 可以转移到 fiS,pmin,k+1,并令后者对 pSap 取最小值。容易发现时间复杂度为 O(3nn2)。由于不合法状态较多,跑不满(若 fi,j,k 则不用枚举 i 的补集的子集),时限非常大,故可以通过。代码

4. 单调队列优化:tD / eD

单调队列优化常见于 任何维度 的动态规划。可见它是多么基础。

4.1 算法简介

“当一个选手比你小还比你强,你就打不过他了”。这是对单调队列非常形象的概括。

具体地,单调队列通过 及时排除不可能成为最优转移的位置,将寻找决策点的复杂度均摊成 O(1),从而保证时间复杂度。能够单调队列优化的转移方程一般可写成:

fi=maxj[li,i1]fj+Aj+Bi

其中 Aj仅与 j 有关 的变量,Bi仅与 i 有关 的变量,同时还需满足可行决策左端点 li 随着 i 增加而单调不降。不难发现我们要求 fi+Ai 的最大值,其本质就是 滑动窗口:在将元素 fi 加入队列 Q 时,若 ft+Atfi+Ai 说明 t 一定不优于 i,不断弹出队尾直到队列为空或 ft+At>fi+Ai。同时,在转移时若队首位置 h<li 说明 h 已经过时,不断弹出队首直到 hli。弹出 h 使得队列为空的情况在大部分题目中不会发生,因为 i1Q。部分题目则需要特判。

经过上述操作后,取出队首 h,它一定满足 h[li,i1]fh+Ah 在所有 [li,i1] 的下标中最大,否则它要么作为队首弹出,要么被某个更大的 f+A 弹出。因此 h 就是 i 的决策点。

4.2 常见技巧

一般的转移方程并不友好,因为贡献 val 常常与 i,j 同时有关。此时可尝试将 val 拆开,写成 val=Aj+Bi 的形式,如当 val=ji 时,Aj=jBi=i,因此将 fj+j 而非 fj 加入单调队列可保证正确性。

结合下方应用以更好理解。

4.3 应用:单调队列优化多重背包

记物品个数为 n,背包容量为 V。第 i 个物品体积为 Vi,价值为 vali 且最多放 ci 个。设 fi,j 为前 i 个物品体积之和为 j 的最大价值,暴力显然有

fi,j=max0kci  kVijfi1,jkVi+k×vali

考虑 拆贡献:考察每个 fi1,jfi,j (jjVijj) 的贡献:记 k=jjVi,那么 0kcifi1,j+k×valifi,j。注意到 kj,j 同时有关,那么在 i 固定时,对于每个相差 Vij,相同的合法的 j 的贡献不一样。

注意到当 j 增加 Vi 时,对于相同的 j,其 k 值就会增加 1。遇到这样的情况,我们会在 j 处减去一定的关于 j 的贡献,并在 j 处增加一定的关于 j 的贡献。这本质上是将 k 看做 jVijVi。改写上式:记 j=qVi+r (0r<Vi),则

fi,j=q×vali+maxk=0min(ci,q)fi1,jkVi(qk)×vali

这很好理解,因为上式的 q 就是 jVi,而我们尝试枚举 k,因此 j 对应的 q=jVi=jVik=qk。它是一个仅与 j 有关的量。

因为对于相同的 jq=qk 的值是定值,为 jVi,同时 q×vali 是一个只关于 j 的值,所以可以用单调队列优化区间 max。实际上,这就是将 jj 的贡献减去了 jVi,并将 j 自身的贡献增加了 jVi。这并不改变 jj 的贡献,因为 k=jjVi。例题如 III.

4.4 例题

I. *P2254 [NOI2005] 瑰丽华尔兹

注意到求最长滑行距离等价于求最少使用魔法的次数,那么设 ft,i,j 表示 t 时间使钢琴在 (i,j) 的最少魔法使用次数。O(nmt) 无法承受。

注意到一段时间内移动方向相同,设 fk,i,j 表示在第 k 段结束后使钢琴在 (i,j) 的最少魔法使用次数。以向右移动为例:设持续时间为 L=tksk+1,则:

fk,i,j=minp=max(0,jlas)jfk1,i,p+L(jp)

并满足 (i,p),(i,p+1),,(i,j) 之间没有障碍。使用常见的拆贡献套路,将 L(jp) 拆成 pLj,这样贡献就分别与 p,j 有关,可以使用单调队列优化。其余方向同理,时间复杂度为 O(nmk)

II. P3572 [POI2014]PTA-Little Bird

fi=minj=iki1fj+[djdi]

由于 djdi 的贡献仅有 1,因此将 i 压入队列和队尾 t 比较时,若 fi<ftfi=ftdidt,说明 i 不劣于 t:当 fi<ft 时,显然有 fi+[didi]ft+[dtdi]。时间复杂度 O(nq)

III. P3423 [POI2005]BAN-Bank Notes

单调队列优化多重背包模板题。由于要记录转移点,时空复杂度均为 O(nk)

IV. P3487 [POI2009]ARC-Architects

POI 真的很喜欢出 单调队列

字典序最大给予我们贪心的思路,即优先选择最大的数字肯定更优,若数字相同则尽量选更前面的,因为这样剩下来的选择就更多。如 [9,7,9,6],选前面的 9 就比选后面的 9 好。

但是有 bi<bi+1 的限制,说明如果我们选最大的,可能导致后面没法选了。因此不妨设我们在选 bi,我们只能从下标为 [bi1+1,ki+1] 的数当中挑选,因为如果 bi 再大一点,那么无论如何我们都选不齐 k 个数。实现就是经典滑动窗口,若当前数等于单调队列队尾则不弹出,保证了相同数优先选择更前面的。时间复杂度线性。

V. P3512 [POI2010]PIL-Pilots

POI 真的很喜欢出 单调队列!直接 two-pointers + 两个单调队列就做完了。时间复杂度线性。

VI. P3422 [POI2005]LOT-A Journey to Mars

POI 真的很喜欢出 单调队列!一开始看错题,以为可以来回走,以为是个神题,想了两个小时 ……

首先破环成链,考虑从 i 开始顺时针方向 ii+1i+2i+n1i+n 能不能走到,也就是要满足 pidipi+pi+1di+di+1 …… 前缀和 优化一下即对于每个 k[i,i+n1],都要有 pkpi1dkdi1,稍做变形得到 pi1di1minik<i+npi1di1,经典的滑动窗口。

对于逆时针方向,同理要有 di1piminink<idk1pk(手动模拟一下容易得到),也是滑动窗口。时间复杂度线性。

*5. wqs 二分优化:2D / 1D

王钦石二分,简称 wqs 二分,又称带权二分,斜率凸优化。常与斜率优化(例题 IX XII. 详见 Part 7),二分队列(Part 8)等方法结合使用。

该算法常见于 限制选取物品个数 的 DP。它有很明显的标志,因此看起来比较套路。

f(i) 表示最多(或恰好,或至少)选取 i 个物品时的答案,若 f 为凸函数则适用 wqs 二分。通常 f 的凸性难以证明,考场上可以猜结论:nk 做不了就是凸的

常见证明凸性的方法

  • 考虑选取 i1i+1 个物品的最优方案,调整得到选取 i 个物品的方案,同时证明 f(i) 不大于或不小于 f(i1)+f(i+1)2
  • 将问题抽象成费用流模型,则每一滴流量带来的贡献单调不减或单调不增。

不妨设 f(i) 下凸,即 f(i)f(i1) 随着 i 增大而增大。我们将 f 形成的凸包画出来:

其中用绿色标出的点为 (m,f(m))

考虑用斜率 k 的直线 lk 截凸包,则对于点 (i,f(i)),其在 y 轴上的截距为 f(i)ki。如果我们可以求出 lk 截到的点的横坐标 p 及其在 y 轴上的截距 bk,则根据 bk=f(p)kp 可知 f(p)=bk+kp因此,我们希望 p=m。这样已知 kbk,可得 f(m)=bm+km

通过比较 pm 的大小可知 k 偏大还是偏小。若 p<m 说明斜率偏小,应当更大;若 p=m 说明已经找到答案;若 p>m 说明斜率偏大,应当更小。注意,当 f 上凸时大小关系会反转,读者应当根据实际意义理解。这样,通过二分可在 logV 次给定斜率 k 求切点的操作后求得 f(m)

问题转化为求切点。用 lk 切下凸包,可知截距为 f(p)kp 的最小值,对应横坐标即取到最小值的 p。换言之,我们希望求出 minp=1nf(p)kp 以及对应的 argmin

进行一步非常巧妙的转化:我们发现 minp=1nf(p)kp 即原问题在 每选一个物品有 k 的代价不限段数 的情况下的最小代价。因为原问题可用 2D / 1D DP 解决,所以去掉一维物品个数后,用 1D / 1D DP 解决转化后的问题,同时记录 选取物品个数 以求出切点横坐标。得到截距 bk 和切点横坐标 p 后容易根据斜率 k 算出切点纵坐标 f(p)=bk+kp

时间复杂度为 O(clogV),其中 c 为一次内层 DP 复杂度,通常 c 等于 O(n)O(nlogn)

5.2 细节:数据类型

大部分题目 f(i) 为整数,所以斜率 f(i+1)f(i)i+1i=f(i+1)f(i) 为整数,因此王钦石二分斜率 k 为整数。

部分题目涉及小数运算,斜率不是整数。这种情况建议平衡精度与效率,设定二分次数 logϵ,并在找到解 p=m 时结束王钦石二分。

5.3 特殊情况:三点共线

三点共线是王钦石二分最重要也最容易出错的细节之一。

三点共线可能导致我们无论如何也二分不到想要的 k。即斜率为 kp<m,斜率为 k=k+1k1p>m

如图,设 m=xC。若二分得到斜率 k2,则无论切到的是点 BC 还是 D,斜率为 k2 时它们在 y 轴的截距 bk2 相同,所以算出来的 f(m)=bk2+k2m 也相同,不影响最终答案。

有一点需要格外注意

  • 如果在保证答案最优的前提下求得段数最大值,那么当 p<m 时应有 lmid+1p>m 时应有 rmid
    • k=k1 时截到点 B,因为 xB<xC,所以 k1 一定不是 我们要找的斜率,lmid+1
    • k=k2 时截到点 D,因为 xD>xC,所以 k2 可能是 我们要找的斜率,rmid
    • 所以 mid 应为 l+r2
  • 相反,如果求得段数最小值,那么当 p<m 时应有 lmidp>m 时应有 rmid1
    • k=k3 时截到的点是 D,因为 xD>xC,所以 k3 一定不是 我们要找的斜率,rmid1
    • k=k2 时截到的点是 B,因为 xB<xC,所以 k2 可能是 我们要找的斜率,lmid
    • 所以 mid 应为 l+r2+1

对比上述两种情况,可知在保证答案最优的前提下,求物品个数 最大值最小值 对二分过程有很大影响。

注意:上述结论的前提为 f 下凸。f 上凸时可类似讨论。

易错点:注意,若二分求得斜率 k,切点横坐标 p 及其截距 bk,则答案为 bk+km 而非 bk+kp。这是初学者常见错误。根据实际意义理解,bk+kpf(p) 而不是要求的 f(m)

5.4 细节:更新答案

若边界值变为 mid 则更新答案,直接给答案赋值 而不是取 min/max。相反,若边界值变为 mid±1 则不更新,因为 mid 处的答案 没有被取到

如果答案所对应的斜率没有被 DP 过怎么办?实际上不会出现这种情况。类比普通二分能二分出边界值,wqs 二分也能二分出边界斜率。

5.5. 技巧

wqs 二分的常用技巧:用结构体将 DP 值与所选取物品个数结合在一起,不仅方便更新 DP 值,还能快速比较两个 DP 值的偏序关系:根据 5.3 所述细节,若 DP 值相同还需根据所选取的物品个数钦定大小关系。重载运算符可以做到。具体实现见例题 III.

5.6 参考博客

在此感谢这些博主。

5.7 例题

很多时候我们都猜测答案是凸函数,而非严谨证明。

I. P2619 [国家集训队] Tree I

本题是 wqs 二分的经典题。但在这里的用途并不是优化 DP。

fi 表示恰好选取 i 条白边时的答案,感性猜测 fi 是关于 i 的下凸函数,使用 wqs 二分即可。

II. CF739E Gosha is hunting

n3 的 DP 显然:设 fi,j,k 表示考虑了前 i 个神奇宝贝,用了 j 个宝贝球和 k 个超级球时期望最大个数。猜测答案 F(a,b) 关于 a,b 都上凸,因此对其中一维使用 wqs 二分,时间复杂度平方对数。

更进一步地,两维上都具有凸性使得我们可以二分套二分做到线性对数平方。代码

*III. P4383 [八省联考2018]林克卡特树

神仙题,顺便巩固一下树形 DP。题目本质是选择 恰好 K+1 条树上不相交简单路径,求最大权值。猜测它可以 wqs 二分,实际上确实可以(不然就没法做了)。

尝试设计在 wqs 二分内部的树形 DP。单走一个 f,发现做不了,我们只好多记录一些状态:由于一个节点 度数不超过 2,因此设 fi,j (j[0,2]) 表示在以 i 为根的子树中,节点 i 的度数为 j 时的答案,且 j=1 时与 i 相连的链不计入链的总数,所减去的斜率也不算。这是 记录路径延伸情况 的常见套路。

另外记录当前 DP 值下链的条数的 最大值 方便判断 wqs 二分截得的点的横坐标。记 gi=max(fi,0,fi,1k,fi,2),设当前合并 i 与其儿子 u,有转移方程:

fi,0=max(fi,0,fi,0+gu)fi,1=max(fi,1,fi,0+fu,1+wi,u,fi,1+gu)fi,2=max(fi,2,fi,1+fu,1+wi,uk,fi,2+gu)

不难发现 gj 表示以 j 为根的子树中,与 j 的父亲不连边时的答案,而 fi,1k 相当于在 i 处直接把链掐断了。因此 g1 即为所求。

注意点:计算 fi,1 时链的总数及其减去的贡献 k 一定要留着最后形成一条链时计算,否则会使答案错误。此外,同一层 i,u 内的转移顺序应该是 fi,2,fi,1,fi,0,观察一下转移方程就能知道原因。时间复杂度线性对数。代码

*IV. CF802O April Fools' Problem (hard)

神仙题!双倍经验:CF802N April Fools' Problem (medium)

这个题神仙之处在于如何对贪心进行反悔:f(i) 下凸,且斜率不可能为负数,因此二分斜率在 [0,2×109] 之间。

接下来考虑怎么贪心:对于每一个 bi 找到在它之前没有被选过的 aj,若 aj+bik0 则选择 aj 并将答案加上 aj+bik。但是这样是错的,如 k=5a={1,233}b={3,1},我们会将 b1a1 匹配,但是 b2+a1k 更小。

考虑如何反悔:每个 bi 要么不选,要么和 aj 匹配,贡献为 aj+bik。要么把某一个 bj 顶替掉,贡献为 bibj。将 bi 提出,发现我们要找的就是 ajkbj 的最小值,用堆维护即可。注意还需记录堆中的每个数形如 ajk 还是 bj,从而计算打印好的题目数量。时间复杂度线性对数平方,代码 非常短。

V. P1484 种树

注意题目求的是 最多 k 个,所以斜率下界从 0 开始。这样可以保证二分出来的点一定不在 k 右边且 f1fp 单调不降,即 fpf1fk 的峰值。若斜率从负数开始,我们二分得到的 fk 不一定是最优解,因为可能 fk1>fk:权值有负数。

时间复杂度线性对数,关于本题的反悔贪心解法见例题 VII. 给出的链接。

VI. P1792 [国家集训队]种树

双倍经验。显然如果 n<2k 则无解。否则破环成链,从 12 开始分别 DP 一遍,最终 DP 结果即第一次的 gn1 和第二次的 gn 的最大值。时间复杂度同上题。

VII. P3620 [APIO/CTSC 2007]数据备份

三倍经验。相邻两个坐标相减,即求不相邻的 k 个数之和的最小值,wqs 二分即可。时间复杂度同上题。本题的贪心做法详见 贪心 专题。

四倍经验:SP1553 BACKUP - Backup Files

VIII. CF958E2 Guard Duty (medium)

五倍经验。

*IX. P5896 [IOI2016] aliens

题解

*X. P5308 [COCI2019] Quiz

猜想答案关于 k 是上凸函数,wqs 二分去掉 k 的限制。设 fi 表示剩下 i 个人时的最大收益,有 fi=maxi<jnfj+jij。显然的斜率优化。时间复杂度 O(wn),其中 w 是设置的二分次数。

XI. P4072 [SDOI2016]征途

注意到题目限制恰好 m 天,猜想最优答案是关于 m 的下凸函数,果断 wqs 二分。设 si 为路程长度前缀和,d=snm,方差式子乘上 m2 就是 m[(l1d)2+(l2d)2++(lmd)2],其中 li 是每一段路程长度。化简即 m(li22lid+d2)md2 提出来,乘个 m 再合并同类项就是 sn2+mli2

求后一项的最小值可以斜率优化:fi=minj=0i1fj+m(sisj)2。时间复杂度线性对数,非常优秀。

XII. P4983 忘情

发现题目中的式子就是 (1+xi)2,wqs 二分 + 斜率优化。注意 wqs 二分的斜率可以达到 (1+xi)21016 级别。

*XIII. P5633 最小度限制生成树

带权二分。视选择与 s 相连的边为选择一个物品,将所有这样的边的权值减掉 K。无解的情况有:每次减去的权值 K 最大时 s 的度数大于 kK 最小时 s 的度数小于 k

但若每次二分都对边重新排序,时间复杂度无法承受,可以一开始先进行初始化,则二分 check 内部只需要使用归并排序即可。时间复杂度 O(m(logm+logVlogn))

注意本题有不使用带权二分的复杂度更优的神仙解法。见 贪心 专题。

*XIV. CF321E Ciel and Gondolas

看到这题笔者首先想到了 O(knlogn) 的决策单调性分治,代码

猜测答案关于 k 是上凸函数,故使用 wqs 二分 + 内层决策单调性二分队列将时间复杂度优化为 O(nlogVlogn)代码

*XV. 某模拟赛 AK 吧

q 次询问在 alar 恰好 选出 k 段的最大子段和。n,q5×104

关于 最多k 段的最大子段和有经典贪心做法,见 CF280D。但本题显然不可以 nqlogn 做。

恰好选 k 段让我们想到 wqs 二分。我们只需求出对于 lr,如果每选一段就要付出 K 的代价,最终选出几段以及对应的最大值。

区间询问考虑线段树,每个区间 [l,r] 维护区间长度 L1 条直线,分别表示选 0,1,2,L1,L 个段对应的代价。因为 固定段数,贡献随着斜率 K 增大而 线性地 减小(因为每选一段就有 K 的代价),因此,段数为 k 的贡献表现出来的就是一条直线 y=kx+bb 表示恰好选 k 段的最大子段和,x 表示二分的斜率,y 表示此时的贡献。

根据经典结论,bk 具有凸性,因此两个区间 bp,i=maxj=0ibls,j+brs,ij 可直接类似闵可夫斯基和地贪心合并。并不需要显式地建出凸包,查询时在 b 上二分即可得到该斜率(即横坐标)对应的段数(即直线斜率)。

由于子段的延伸情况仅和区间端点相关,故对每个区间分别记录两个端点总共四种状态。对于左端点为 1 的状态,暂时不看作完整子段,当与左边的右端点为 0 的区间合并时再计入,当与左边的右端点为 1 的区间合并时可看做单独子段,也可以和左边区间最右子段合并。查询时也类似记录即可。

wqs 二分上下界为 a,为了卡常可以适当调小一点。时间复杂度 O(nlog2n(logn+loga))

posted @   qAlex_Weiq  阅读(14172)  评论(16编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示