2025清北学堂五一假期 CSP-S(普转提)核心算法精选营 Day4(5.4)
Day4:树形DP、状压 DP、数位 DP、DP 优化、总结。
授课老师:钟皓曦
1.树形 DP
顾名思义,在树上的 \(\text{DP}\)。
例1.求树的重心
什么是树的重心?
重心:点到其他点的距离之和最小。
于是,我们设 \(f[i]\) 为以 \(i\) 为根的子树的所有点到 \(i\) 的距离之和。
显然,\(i\) 为叶子节点时,\(f[i] = 0\)。若 \(j\) 为 \(i\) 的儿子节点,则 \(f[i] = \sum(f[j] + size[j])\)。其中,\(size[j]\) 是以 \(j\) 为根的子树的大小。
可是,我们只有 \(1\) 号点的信息,怎么办?此时,我们需要换根 \(\text{DP}\)。
把根换到 \(p\) 时,\(f[p] = f[1] - (f[p] + size[p]) + (n - size[p])\)。也就是原来的根减去 \(p\) 的信息再加上以 \(p\) 为根的子树外的点的贡献。
\(\color{red}\texttt{Code:}\)
vector<int> z[maxn];//z[i][j]代表从i出发的第j条边会走到z[i][j]
int f[maxn];//f[i] 代表以i为根子树的所有点到i的距离之和
int size[maxn];//size[i] 代表以i为根的子树的大小
void dfs(int now,int fa)//当前要处理f[now] 同时now的父亲为fa
{
//递归计算儿子们的dp值
for (auto x : z[now])//枚举从now出发的所有边
if (x != fa)//x是now的儿子
dfs(x,now);
//计算now的dp值
f[now]=0;size[now]=1;
for (auto x : z[now])//枚举从now出发的所有边
if (x != fa)//x是now的儿子
{
size[now] += size[x];
f[now] += f[x] + size[x];
}
}
void dfs(int now,int fa,int sum)
//当前要处理以now为根的结果 now的父亲是fa sum记录不在now子树内的信息
//sum代表now子树外的所有点到now的距离之和
{
ans = min(ans, f[now] + sum);
for (auto x : z[now])
if (x != fa)//计算x子树外的所有点到x的距离之和
dfs(x,now, sum + f[now] - (f[x] + size[x]) + (n-size[x]));
}
void dfs(int now,int fa,int maxv)
{
ans=max(ans,max(g[now],maxv));
for (auto j : z[now])
if (j != fa)
{
if (g[j] + 1== g[i]) x=h[i];
else x=g[i];
dfs(j,now,max(x,maxv)+1);
}
}
int main()
{
//读入这棵树
dfs(1,0);
}
例2.
求树上路径总长度和。
例3.
一棵树有 \(n\) 个点,其中有一些节点被打了标记。保证树是联通且无环的。
定义 \(f_i\) 为第 \(i\) 个节点到所有被标记节点距离的最大值。
找出所有点的 \(f_i\) 的最小值。

举例:一棵 \(7\) 个节点的树,编号 \(1 \sim 7\),被标记节点有 \(2,6,7\),因此 \(f(i) = [2,3,2,4,4,3,3]\),\(f_i\) 最小的节点为 \(1,3\),最小值为 \(2\)。
2.状压 DP
有的时候,我们需要把一个变化的数组存放在状态里,然而,直接操作是不行的。于是,状压 \(\text{DP}\) 闪亮登场。
例如,数组中有一个数 \(12310\),为了优化,我们可以把它拆成几位,用四进制存放它。
例4.旅行商问题
\(N\) 个点,第 \(i\) 个点的坐标为 \((x_i,y_i)\),从 \(1\) 号点出发,求所有点都走一遍的最优方案,使得距离之和最小。
首先,我们通过瞪眼法得出两个性质:
- 最优方案中,每个点只会走一次;
- 两点之间,线段最短(
废话,数学没学吗)。
\(\color{red}\texttt{Code:}\)
我建议大家,状压 \(\text{DP}\) 开始最好以 \(0\) 为下标。——zhx
时间复杂度 \(\mathcal O(n^2 \times 2^n)\),\(n \le 18\) 可过。
暴力呢?\(\mathcal O(n!)\),\(n \le 11\)。
例5.互不侵犯
\(\texttt{P1896 [SCOI2005] 互不侵犯}\)。
\(\color{red}\texttt{Code:}\)
#include<bits/stdc++.h>
using namespace std;
int n,k;
int f[2][2][2];//f[i][j][s] 代表前i行已经放完国王了 总共放了j个国王
//s代表第i行哪些位置放了国王 此时方案数是多少
int main()
{
cin >> n >> k;//代表n*n棋盘放k个国王
f[0][0][0]=1;
//O(nk2^(2n))
for (int i=0;i<n;i++)
for (int j=0;j<=k;j++)
for (int s=0;s<(1<<n);s++)//要从f[i][j][s]这个状态向外转移 在第i+1行放国王
if (f[i][j][s])
for (int r=0;r<(1<<n);r++)//第i+1行怎么放国王
{//要检查有没有国王能互相攻击到
if (r&s) continue;//有两个国王在同一列
if ((r>>1)&s) continue;
if ((r<<1)&s) continue;
if ((r<<1)&r) continue;
f[i+1][j+__builtin_popcount(r)][r] += f[i][j][s];
}
}
3.插头 DP
例 \(5\) 的优化方法 \(\color{red}\texttt{Code:}\)
int f[][][][];
//f[i][j][k][s] 代表已经放国王放到(i,j)这个位置了 k代表已经放了k个国王
//s代表轮廓线上方每个位置有没有放国王
int main()
{
cin >> n >> k;
f[1][0][0][0] = 1;
//O(n^2k2^(n+1))
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)//要在第i行第j列这个格子放国王
{
for (int r=0;r<=k;r++)
for (int s=0;s<(1<<(n+1));s++)
if (f[i][j-1][r][s])//方案数不为0
{
//ij不放国王
f[i][j][r][(s|(1<<(j-1)))^(1<<(j-1))] += f[i][j-1][r][s];
//ij放国王
if (j>=2 && (s>>(j-2)&1)==1) continue;
if (j>=1 && (s>>(j-1)&1)==1) continue;
if ((s>>j&1)==1) continue;
if ((s>>(j+1)&1)==1) continue;
f[i][j][r+1][s|(1<<(j-1))] += f[i][j-1][r][s];
}
for (int r=0;r<=k;r++)
for (int s=0;s<(1<<(n+1));s++)
f[i+1][0][r][(s & ((1<<n)-1) << 1] = f[i][j][r][s];
}
}
是的,你没看错,这便是插头 \(\text{DP}\),它是状压 \(\text{DP}\) 的一个部分。
4.数据结构优化 DP
例6.
题意:给定一个长度为 \(N\) 的序列,每一位有一个目标颜色。初始时每一位都没有颜色。每一次可选择一个区间,将区间里的所有元素都改为其目标颜色。若区间里不同颜色个数为 \(X\),则代价为 \(X^2\)。
数据范围:\(N \le 5 \times 10^4\)。
设 \(f[i]\) 为把 \(1 \sim i\) 全部染色的最小代价。
转移?\(f[i] = \min(f[i],f[j] + (不同颜色的个数)^2)\)。
时间复杂度 \(\mathcal O(n^2)\),显然过不去。
我们考虑,最优解的上界是什么。\(N^2\) 吗?不是。答案是 \(N\),因为我们可以每一个数都单独染色。
于是 \(X^2 \le N\),推出 \(X \le \sqrt{N}\)。
于是用双指针解决。
例7.
题意:Hja 回到老家开始种地,一开始,所有地(共 \(N\) 块)都是荒地。把每块地变成不荒地有一定代价,但一旦改变之后就不再是荒地了。现在 Hja 开始了 \(M\) 年的种地生活,第 \(i\) 年可以在 \(l_i \sim r_i\) 上种地,并且可获得 \(p_i\) 的收益。(注意:种地必须整段种,且必须都是不荒地)Hja 可以选择种或不种每一年的地,问能获得的最大收益。
数据范围:\(N,M \le 1000\)。
有些题不一定非得按照它说的去做。——zhx
由此,我们考虑把左端点为第一关键字,右端点为第二关键字从小到大排序。
有些时候,做题困难时,排个序说不定能好做一些。你看这个题如果有 \(N\) 个东西,那它就有可能是排序。——zhx
所以 \(f[i][j]\) 表示前 \(i\) 次种地,所有开荒的地中最右边的是 \(j\) 的最小代价。
如果种的话,\(f[i + 1][j] = f[i]\);如果种的话,若 \(j < l_{i + 1}\),则 \(f[i + 1][r_{i + 1}] = f[i][j] - sum(l_{j + 1}, r_{j + 1}) + p_{i + 1}\),若 \(l_{i + 1}\le j\le r_{i + 1})\),则 \(f[i + 1][r_{r + 1}] = f[i][j] - sum(j, r_{j + 1}) + p_{i + 1}\),否则 \(f[i + 1][j] = f[i][j] + p_{i + 1}\)。
时间复杂度 \(\mathcal O(NM)\)。
\(\color{red}\texttt{Code:}\)
int f[i][j];//前i年已经搞定 开了荒的地最靠右的是第j块
int main()
{
cin >> n >> m;
for (int i=1;i<=n;i++)
cin >> a[i];//第i块地开荒的代价
//sum[i]代表a[i]的前缀和
for (int i=1;i<=m;i++)
cin >> l[i] >> r[i] >> p[i];
//把种地按照左端点为第一关键字 右端点为第二关键字从小到大排序
for (int i=0;i<m;i++)
for (int j=0;j<=n;j++)// 第i+1年的种不种
{
//不种
f[i+1][j] = max(f[i+1][j], f[i][j]);
//种
if (j < l[i+1]) f[i+1][r[i+1]] = max(f[i+1][r[i+1]], f[i][j] + p[i+1] - (sum[r[i+1]] - sum[l[i+1]-1]));
else if (j <= r[i+1]) f[i+1][r[i+1]] = max(f[i+1][r[i+1]], f[i][j] + p[i+1] - (sum[r[i+1]] - sum[j]));
else f[i+1][j] = max(f[i+1][j], f[i][j] + p[i+1]);
}
}
接下来,难度提升!
\(N,M \le 2 \times 10^5\),\(\mathcal O(NM)\) 过不去了。
这时,我们用滚动数组、线段树优化即可。
5.数位 DP
在 \(l \sim r\)(包含) 中的所有数的各位之和是多少?
要解决这个问题,我们就需要数位 \(\text{DP}\) 了。
\(\color{red}\texttt{Code:}\)
int f[i][0/1] //代表填完第i位之后的数字之和
int g[i][0/1] //代表填完第i位之后的方案数
int dp(int x_)//要求 0~x_的 各位数字之和
{//O(logx)
int n=0;
while (x_ != 0)
{
n++;
x[n] = x_ % 10;
x_ /= 10;
}
memset(f,0,sizeof(f));
memset(g,0,sizeof(g));
f[n+1][1] = 0;g[n+1][1] = 1;
for (int i=n;i>=1;i--)//要填yi的值
for (int j=0;j<2;j++)
if (f[i+1][j]) //从f[i+1][j]进行转移
{
int up=9;
if (j==1) up = x[i];
for (int k=0;k<=up;k++)//要在yi这里填k
{
f[i][(j==1) && (k==up)] += f[i+1][j] + g[i+1][j] * k;
g[i][(j==1) && (k==up)] += g[i+1][j];
}
}
return f[1][0] + f[1][1];
}
int main()
{
cin >> l >> r;
cout << dp(r) - dp(l-1) << "\n";
}
6.总结
暑假是学习 OI 最好的时间。——zhx
做题争取一遍过,自己检查,多给自己时间。——zhx
7.作业
- Day4 题单 在此。
后记
更多内容,请移步至 \(\color{red}\texttt{Luogu ryf2011}\)。

浙公网安备 33010602011771号