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\) 的最小值。

例3.例图

举例:一棵 \(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\) 号点出发,求所有点都走一遍的最优方案,使得距离之和最小。

首先,我们通过瞪眼法得出两个性质:

  1. 最优方案中,每个点只会走一次;
  2. 两点之间,线段最短(废话,数学没学吗)。

\(\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.作业

后记

更多内容,请移步至 \(\color{red}\texttt{Luogu ryf2011}\)

posted @ 2025-05-04 15:26  cnblogs2011ryf  阅读(32)  评论(0)    收藏  举报