2025.3.24 DP专题
题目按照主观难度增序排列
Luogu P1758 [NOI2009] 管道取珠
有上下两个长度分别为 \(n,m\) 的管道 \(a,b\),管道中有两种不同颜色的球用 \(A,B\) 表示。现在每次只能从管道末尾开始取球,求取完所有球每种颜色序列的方案数的平方和。
考虑转换题意,首要任务是方案数的平方怎么办。
注意到一个颜色序列 \(c\) 如果出现次数为 \(a_i\),\(a_i^2\) 就是两个独立系统取到相同颜色序列的方案数。也就是说我们可以直接统计两个人在两套管道中取到相同颜色序列的方案数,这样转移就比较简单了。
考虑设 \(f_{tot,i,j}\) 表示取 \(tot\) 个球,第一个人取了 \(i\) 个上管道的球,第二个人取了 \(j\) 个上管道的球。那么第一个人就取了 \(k-i\) 个下管道的球,第二个人就取了 \(k-j\) 个下管道的球。
转移只在颜色序列相同时发生。分讨 \(2\times2=4\) 种情况,有
注意一下边界,滚一下数组,直接转移即可。
代码实现
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e2 + 10, mo = 1024523;
int n, m; char a[maxn], b[maxn];
int f[2][maxn][maxn];
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> a[i]; reverse(a + 1, a + n + 1);
for(int i = 1; i <= m; i++) cin >> b[i]; reverse(b + 1, b + m + 1);
f[0][0][0] = 1;
for(int tot = 1; tot <= n + m; tot++) {
int u = tot & 1, v = u ^ 1; for(int i = 0; i <= n ; i++) for(int j = 0; j <= n; j++) f[u][i][j] = 0;
for(int i = max(tot - m, 0); i <= min(tot, n); i++) {
for(int j = max(tot - m, 0); j <= min(tot, n); j++) {
if(a[i] == a[j] && i && j) f[u][i][j] += f[v][i - 1][j - 1];
if(a[i] == b[tot - j] && i && tot - j) f[u][i][j] += f[v][i - 1][j];
if(b[tot - i] == a[j] && j && tot - i) f[u][i][j] += f[v][i][j - 1];
if(b[tot - i] == b[tot - j] && tot - i && tot - j) f[u][i][j] += f[v][i][j];
f[u][i][j] %= mo;
}
}
}
cout << f[(n + m) & 1][n][n] << endl;
return 0;
}
Luogu P4516 [JSOI2018] 潜入行动
给定 \(n\) 个点的一棵树,要用 \(k\) 个监听装置来监听所有的点。节点上放一个监听装置可以监听除这个点外与其相邻的所有点,求合法方案数 \(\bmod1e9+7\)。
树上 DP 先从子树的角度来考虑问题。如果以 \(v\) 为根的子树都被监听了,考虑它的父亲 \(u\) 可能的情况。发现会分成 \(v\) 是否有监听装置、是否被监听和 \(u\) 是否有监听装置、是否被监听几种情况。再加上题目本身是一个树形背包,我们可以设出状态 \(f_{u,k,0/1,0/1}\) 表示以 \(u\) 为根的子树内(不含 \(u\)) 所有点被监听,用了 \(k\) 个监听装置,\(u\) 是否有监听装置和 \(u\) 是否被监听 。所以答案即为 \(f_{1,k,0,1}+f_{1,k,1,1}\)。
我们先思考 \(u\) 是否被监听对转移有什么影响,如果当前 \(u\) 没有被监听,那么之前一定也没有被监听,所以只能从没有被监听的状态转移过来;如果当前 \(u\) 被监听了,就要讨论是否是 \(v\) 装置造成的,转移会复杂一些。。这里我们可以尝试大力分讨 \(u,v\) 的可能情况:
如果 \(u\) 没有被监听且没放装置,那么 \(v\) 一定不能放监听装置,所以 \(f_{u,i+j,0,0}=f_{u,i,0,0}\times f_{v,i,0,1}\);
如果 \(u\) 没有被监听但是放了监听装置,那么 \(v\) 有没有被监听到的都成为了合法方案,且 \(v\) 不能放监听装置,所以 \(f_{u,i+j,1,0}=f_{u,i,1,0}\times (f_{v,j,0,0}+f_{v,j,0,1})\);
如果 \(u\) 被监听了但是没放装置,这时候要考虑 \(u\) 是否是因为 \(v\) 第一次被监听的。如果是,那么 \(v\) 就一定有装置且被监听,贡献的合法方案是 \(f_{u,i,0,0}\times f_{v,j,1,1}\);如果不是,那么 \(v\) 无所谓是否有装置,但必须被监听,贡献的方案数就是 \(f_{u,i,0,1}\times(f_{v,j,0,1}+f_{v,j,1,1})\)。所以总转移 \(f_{u,i+j,0,1}=f_{u,i,0,0}\times f_{v,j,1,1}+f_{u,i,0,1}\times(f_{v,j,0,1}+f_{v,j,1,1})\);
如果 \(u\) 既有装置也被监听,\(v\) 让 \(u\) 第一次被监听的贡献为 \(f_{u,i,1,0}\times(f_{v,j,1,0}+f_{v,j,1,1})\);\(v\) 不是第一次让 \(u\) 被监听,这时候 \(v\) 没有任何限制,贡献为 \(f_{u,i,1,1}\times(f_{v,j,0,0}+f_{v,j,0,1}+f_{v,j,1,0}+f_{v,j,0,1})\),总贡献即 \(f_{u,i+j,1,1}=f_{u,i,1,0}\times(f_{v,j,1,0}+f_{v,j,1,1})+f_{u,i,1,1}\times(f_{v,j,0,0}+f_{v,j,0,1}+f_{v,j,1,0}+f_{v,j,0,1})\)。
就有总转移
但是实际上有更加简洁的做法,如果我们不讨论具体的 \(0/1\) 取值,而是用变量来替代就可以大大简化转移。不妨设现在有状态 \(f_{u,i,p_1,q_1},f_{v,j,p_2,q_2}\),考虑应该转移到什么状态?
当前状态只要之前被监听或 \(v\) 有装置就应被监听,所以监听状态表示为 \(q_1|p_2\),而装置的状态只取决于之前是否被监听,所以就是 \(p_1\)。这样我们就得到了一个简洁易懂的转移:
可能常数略大,但简单易懂!!
代码实现
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10, maxk = 1e2 + 10, mo = 1e9 + 7;
int n, k;
vector<int> e[maxn];
int sz[maxn], f[maxn][maxk][2][2], g[maxk][2][2];
void dfs(int u, int fa) {
sz[u] = f[u][0][0][0] = f[u][1][1][0] = 1;
for(int v : e[u])
if(v != fa) {
dfs(v, u);
int tot = min(sz[u] + sz[v], k);
for(int i = 0; i <= sz[u]; i++)
for(int j = 0; j <= sz[v] && i + j <= tot; j++) {
for(int p1 = 0; p1 < 2; p1++)
for(int q1 = 0; q1 < 2; q1++)
for(int p2 = 0; p2 < 2; p2++)
for(int q2 = 0; q2 < 2; q2++)
if(q2 | p1)
(g[i + j][p1][q1 | p2] += 1ll * f[u][i][p1][q1] * f[v][j][p2][q2] % mo) %= mo;
}
for(int k = 0; k <= tot; k++)
for(int p = 0; p < 2; p++)
for(int q = 0; q < 2; q++)
f[u][k][p][q] = g[k][p][q];
for(int k = 0; k <= tot; k++)
g[k][0][0] = g[k][0][1] = g[k][1][0] = g[k][1][1] = 0;
sz[u] = tot;
}
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> k;
for(int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
e[u].push_back(v), e[v].push_back(u);
} dfs(1, 0);
cout << (f[1][k][0][1] + f[1][k][1][1]) % mo;
return 0;
}

浙公网安备 33010602011771号