10月模板清理

20241016

区间DP - 回文字串

\(f[i][j]\) 表示把 \(s[i \sim j]\) 变成回文,最少补几个,从 \(f[i][j - 1], f[i + 1][j], f[i + 1][j - 1]\) 三种情况转移过来即可。感性理解一下这样的状态定义是有最优子结构的。

区间DP - 合唱队

肯定可以区间 \(dp\),再注意到状态的转移和上一步有关,所以记 \(f[i][j][0/1]\) 表示 \(H[i \sim j]\) 的方案数,其中 \(0\) 表示最新的一个人插进了队形左端(即 \(H[i]\)),\(1\) 就是右端(即 \(H[j]\)),根据大小关系转移即可。

高维DP - 三倍经验

唯一值得注意的是此题可以倒推,并且倒推之后的答案统计是 \(O(1)\) 的。正推的答案统计是 \(O(n)\) 的。

高维DP - 方格取数

一个很朴素的想法是把两条路的位置同时都实时记录下来,这样就好转移了,事实证明是可以的。

\(f[i][j][p][q]\) 表示第一条路走到了 \((i, j)\),第二条路走到了 \((p, q)\) 的最大权值和。它可以转移到四种情况,即:

  • 第一条路 往右走, 第二条路 往右走。
  • 第一条路 往右走, 第二条路 往下走。
  • 第一条路 往下走, 第二条路 往右走。
  • 第一条路 往下走, 第二条路 往下走。

分 点重合/不重合 两种情况赋予转移代价,套上 \(BFS\), 直接转移就是 \(O(n^4)\) 的,当然需要标记一下每个点是否访问过。

考虑优化成 \(O(n^3)\),注意上面的方法两条路走的步数始终相同。记 \(f[i][j][k]\) 表示两条路都走了 \(i\) 步,第一条路处于第 \(j\) 行,第二条路处于第 \(k\) 行,最大权值和。起点是 \((1, 1)\),因此 \((x - 1) + (y - 1) = i\), 则 \(y = i - x + 2\)。最后特判一下只走了 \(1\) 步的情况就行,因为不满足这个式子。

环形DP - 石子合并

学到一个 \(trick\) : 区间相关的环形问题,可以把序列复制一遍接在后面,然后只推到 \(len = n\) 就可以了,最后取 \(\max_{i \in (1, n + 1)} f[i][i + n - 1]\)。这样比 \(n\) 次区间 \(dp\) 快很多,因为不同的断点位置下的区间可以使用相同的一段很小的区间。

环形DP - 环上最大子段和

最大子段和 + 前后缀max 连一下就可以。

树型DP - 没有上司的晚会

一个最大独立集问题,记 \(f[u][0/1]\) 表示选 \(u\) 或者不选 \(u\),对于一个儿子 \(v\),有转移:

\[f[u][0] = \sum\min(f[v][0], f[v][1]) \]

\[f[u][1] = \sum[v][0] + val[u] \]

树型DP - 二叉苹果树

树上背包,记得给 \((u, v)\) 留一条边就可以了,并且一定要连这条边,不然转移没有意义。

状态压缩BFS - 关灯问题

只有 \(n\) 能进行状态压缩。一开始的想法是记 \(f[i][S]\) 表示前 \(i\) 个操作,强制选 \(i\),然后得到了 \(S\) 这个集合,最少操作数。然后还“机智”地发现开个 \(vector[i]\) 表示 \(f[i][?]\) 有哪些更新出来了。

然而无济于事,因为此题 \(dp\) 的顺序并不是简单的递推(会有后效性),因此采用记忆化 \(BFS\) 进行实现。

答案: \(min(f[i][0])\)

初始化: \(f[0][(1 << n) - 1] = 0\),其他初始化为 \(inf\)

式子就是 \(f[i][S] = min(f[j][T] + 1, f[i][S])\)\(T\) 的话就照着题目要求根据 \(S\) 推出来。

时间复杂度为 \(O(2^nmn)\)

最后发现其实当前具体在哪个操作并不重要(下一步转移都是向全部操作),所以可以优化成一维。并且由于广搜良好的性质找到一个 \(f[0]\) 就直接输出就可以!

状态压缩DP技巧 - A Simple Task 「CF11D」

一开始想成树形dp了,然后发现非常的难写,因为点之间有各种路径计数,很难补充不漏。每次扩展和前一个状态的联系也不是很大。

题解是一种巧妙的状压dp。显然可以对已选点集进行状压。先考虑如何统计环的数量,对于 \(i \to j\) 的一条简单路径,若路径外一点 \(k\) 同时和 \(i\)\(j\) 有连边,那么就成了一个环。基于此,我们可以转化成利用 \(dp\) 统计简单路径的数量,而统计答案时在另找一个点连环。(状压没想到,但这一步转化想到了……)

这样定义出来的状态是 \(f[S][i][j]\),表示选择的点集为 \(S\)\(i \to j\) 的简单路径的数量。转移比较好写。空间复杂度会达到 \(2^{19} \times 19^2 = 189,267,968\),存不下,时间应该还要再乘一个 \(19\), 也爆掉了。

考虑怎么优化时空。注意到后两维其实都是我们主动加的限制,但仅仅是为了统计答案而同时维护这两维显得有些不划算。能不能利用好 \(S\) 中包含 \(i, j\) 的性质进行一些优化呢?

考虑强制规定 \(lowbit(S)\) 那一位是起点,\(i\) 作为终点,这样只要遇到一个满足 \((1 << i) = lowbit(S)\)\(i\), 就统计答案。

状态: \(f[S][i]\),意义如上。

答案: \(ans = \sum_{lowbit(S) = 2^i}f[S][i]\)。最后记得 \(ans = (ans - m)/2\), 因为会误算 一条边和两个端点的“假环”

初始化: \(f[2^i][i] = 1,i \in [0, n - 1]\)

转移:

\[\begin{align} ans &+= f[S][i] &\text{lowbit(S) = (1 << j)} \\ f[S | (1 << j)][j] & += f[S][i] & \text{lowbit(S) < (1 << j)} \end{align} \]

20241017

数位DP - Windy数 「SCOI2009」

数位dp经典题。分两步求解:

  • 预处理 \(f[i][j]\),表示有 \(i\) 位,最高位填 \(j\),方案数。
  • \(dp(x)\) 表示 \(1 \sim x\) 有多少个 \(Windy\) 数,则答案为 \(dp(b) - dp(a - 1)\)

\(a[1 \sim cnt]\) 表示 \(x\) 的每一位(从高到低),那么每一位的转移分 \(now \lt a[i]\) (后面的位随便填,可直接调用 \(f\) 统计答案)和 \(now = a[i]\) (继续往下走)两种情况转移。走到 \(i = 1\) 记得 \(ans\) 还要额外 \(+1\),就是这个数本身的答案。

20241018

数位DP - 手机号码 「CQOI2016」

可以是数位dp,也可以是高维dp……

  • 还是先说一下我超级复杂的容斥做法吧!

\(f1\) 表示 相邻 2/1个 数相同,数的个数。

\(f2\) 表示 相邻 2/1个 数相同,且同时出现 4 和 8,数的个数。

\(f3\) 表示 同时出现 4 和 8,数的个数。

则答案为 \((r - l + 1) - (f1 + f3 - f2)\)

写三个 数位dp预处理 和 三个数位统计就可以啦!

狂打5KB 的AC代码,慎入

一看题解感觉自己……(哎至少是自己亲手做出来的一道蓝题!)

  • 正解是记 \(f[x][last1][last2][f4][f8][flag]\) 表示 \(x\) 位数,上一个数是 \(last1\),上上个数是 \(last2\),是否出现 4,是否出现 8,是否有数字连续出现 3 次,数的个数。

    转移比较典型,分 \(i < a[x]\)\(i = a[x]\) 两种情况转移。

    注意这里我们要求的是至少要连续出现 3 次了,和我上面的做法是反着的。

    AC代码(带注释)

20241023

数位DP - Round Numbers 「USACO06NOV」

首先读题读对:二进制下数位dp

还是很典的数位dp,先写预处理,再写 \(dp(x)\),最后 \(dp(r) - dp(l - 1)\)

一开始我的定义是 \(f[i][j]\) 表示 \(i\) 位数,含有 \(j\)\(0\) 的圆数个数。由于包含了前导零的情况,转移容易算重,非常不好写……

考虑增加一位区分有没有前导零,记 \(f[i][0/1][k]\) 表示 \(i\) 位数,最高位是 \(0/1\),含有 \(k\)\(0\) 的圆数个数。接着来考虑贡献组成:

设当前求解的数为 \(x\),有 \(cnt\) 位。

  • \(Case\ 1\):含有前导零,贡献为

\[\sum_{(1 \le j \le i \le cnt) \cap (j \gt i - j)}f[i][1][j] \]

  • \(Case\ 2\):不含前导零,最高位就强制填 \(0\)。从高位到低位统计,若 \(a[i] = 0\),直接过;若 \(a[i] = 1\),可以选择填 \(0\) 然后算贡献。

(数位dp终于做完了……)

图上DP - 跑跑

由于空间跑路器的存在,直接跑最短路再 \(popcount\) 是错滴。

看到 \(2^k\),想到这种题应该和倍增有关系。

考虑一个情景:

  • 如果 \((i \to k)\)\((k \to j)\) 都有一条长为 \(2^0\) 的路,那么 \((i \to j)\) 就有一条长度为 \(2^1\) 的路!然后用跑路器就可以 \(1s\) 跑完,即 \(i, j\) 之间存在一条权值为 \(1\) 的边。

这个情景告诉我们,如果我们能知道 哪些点之间能用跑路器,即 “存在 \(2^k\) 的路径” ,并给他们在新图上连一条权值为 \(1\) 的边,就差不多做完了。

现在考虑怎么求 “点之间存不存在这种路径”。

图上/期望DP - 绿豆蛙的归宿

有向图概率期望dp。

这其实是 \(DAG\)\(dp\)。转移顺序就确定了。

然后就是期望 \(dp\) 的部分。期望 \(dp\) 很多状态都是倒着定义的,形如 “当前在某个中间状态 \(i\),要到终止态 \(n\) 还需要的期望值”

此题就是 \(f[u]\) 表示当前到达了 \(u\),走到 \(n\) 还需要的期望步数。

因为转移顺序中没有回头路可走,所以不用考虑自转移的情况。

\[f[u] = \sum\frac{f[v] + w(u, v)}{d[u]} \]

\(d[u]\)\(u\) 的入度。

“自转移” 参见这道题:百事世界杯之旅-luogu

图上DP - 采蘑菇

因为说了是有向图,所以感觉就很能dp,但是不是 \(DAG\),遇到环就dp不了了。

再思考,进了一个环肯定要把贡献全部拿完才会出来,再根据“恢复系数”,可以预处理每个环的贡献,最后环缩点。

缩点后整张图就变成一张 \(DAG\) 了,就可以愉快的dp了。

最后注意因为 \(s\) 不一定是入度为0的点,所以要么 先一次 \(dfs\) 把涉及的子图全部跑出来,再 \(topsort\);要么直接 \(bfs\),每个点可能重复入几次(没被卡时间)

时间复杂度瓶颈在算环的贡献,最坏情况是 \(O(mlog_p{\frac{1}{w}})\)\(p\) 是恢复系数,\(w\) 是初始蘑菇数。

DP优化 - 单调队列优化 - 能量收集

\(f[i][j]\) 表示走到 \((i, j)\),最大收益。

\[f[i][j] = \max_{j - t \le k \le j + t}f[i - 1][k] + val[i][j] \]

感觉既可以 \(st\) 表,也可以单调队列优化。\(2000ms\)嘛,\(O(n^2logn)\) 能卡。

考虑一组 \(a \lt b\)\(f[i - 1][a] \lt f[i - 1][b]\),那么 \(a\) 就永远无法成为贡献点了,因为位置不如 \(b\) 优,而且值也没有 \(b\) 大。

由此,我们需要维护一个 \(f[i][j]\) 递减的单减栈,队尾维护单调性,队首清理过期元素即可。

20241024

单调队列优化 - 跳房子「NOIP2017 普及组」

考虑已知跨步的范围后怎么做。记 \(f[i]\) 表示跳到第 \(i\) 个格子,最大分数,容易写出转移:

\[f[i] = \max_{l \le d[i] - d[j] \le r} f[j] + a[i] \]

显然 \(g\) 可以二分,直接做是 \(O(n^2logn)\) 的。

考虑单调队列优化dp,维护一个 \(f[i]\) 单调递减的单调栈。

一个细节是若 \(d[i + 1] - d[i] \lt l\) 是不能直接把 \(i\) 加入的,要记一个 \(nw\) 表示现在加到哪一个了,每次判断距离是否 \(\ge l\) 了。总之细节比较多。

自己写过之后看了一下别人的代码,发现很多东西可以合并。比如 \(nw\)\(0\) 开始而不要从 \(1\) 开始,且当 \(head \le tail\) 时才更新 \(f[i]\),这样开头涉及 \(0\) 的部分就不会出错了。代码也简洁多了。

并且单调队列的过期清理应该也只需要一次,放在插入元素之后搞一次。如果 \(f[nw]\) 真的在 \(f[q[tail]]\) 处遇到了这些过期元素,那也是没问题的,因为要么就是 \(f[nw] >= f[q[tail]]\),这些东西本来就会弹出去。要么就是 \(f[nw] < f[q[tail]]\),那之后过期清理也就对了。

AC代码

20241025

有依赖的背包问题 - AcWing

发现自根往下选并不好决策,考虑倒着做。

定义:记 \(f[i][j]\) 表示 在 \(i\) 的子树内,选的总体积为 \(j\) 且必选 \(i\),最大价值。

答案:\(\max_{1 \le i \le vmax}f[root][i]\)

转移:\(f[i][j] = \max_{j - k \ge a[i]}f[i][j - k] + f[son[i]][k]\)

初始化:\(f[i][a[i]] = b[i]\),其他的设为 \(-inf\)

void dfs(int u){
	f[u][a[u]] = b[u];
	for(auto v : G[u]){
		dfs(v);
		G(i, vmax, a[u]){
			G(j, i, a[v]){
				if(f[u][i - j] >= 0 && f[v][j] >= 0)
					f[u][i] = max(f[u][i], f[u][i - j] + f[v][j]);
			}
		}
	}
}
posted @ 2024-10-16 21:52  superl61  阅读(19)  评论(0)    收藏  举报