2025.5.4笔记

如果我有写错或令人不理解的地方,请及时指出,谢谢!!!

树形DP

重心:点到其他所有点的距离之和最小。

\(f[i]\) 表示以 \(i\) 为根的子树的所有点到 \(i\) 的距离之和。

\(i\) 为叶子节点,\(f[i] = 0\)

\(j\)\(i\) 的儿子,则 \(f[i] = \sum (f[j] + size[j])\)

\(size[j]\) 表示以 \(j\) 为根的子树的大小。

点击查看代码
void dfs(int now, int fa)
{
	for (auto x : z[now])
		if (x != fa)
			dfs(x, now);
	f[now] = 0;
	size[now] = 1;
	for (auto x : z[now])
		if (x != fa)
		{
			size[now] += size[x];
			f[now] += f[x] + size[x];
		}
}

可是我们只知道 \(1\) 号点的信息。

所以我们要换根 DP。

如果要把根换到 \(p\)

\(f[p] = f[1] - (f[p] + size[p]) + (n - size[p])\)

也就是原来的根 \(-\) \(p\) 的信息 \(+\)\(p\) 为根的子树外的点的贡献。

点击查看代码
void dfs2(int now, int fa, int sum)
{
	ans = min(ans, f[now] + sum);
	for (auto x : z[now])
		if (x != fa)
			dfs2(x, now, sum + f[now] - (f[x] + size[x]) + (n - size[x]));
}

例1

image

\(f[i]\) 表示以 \(i\) 为根的子树中经过 \(i\) 的路径之和。

\(g[i]\) 表示以 \(i\) 为根的子树中以 \(i\) 为起点的路径之和。

\(g[i] = \sum (g[p] + size[p])\)

\(f[i] = \sum (g[p] + size[p]) \cdot (size[i] - size[p])\)

但是。。。

\(\displaystyle\sum^n_{i = 1}\sum^n_{j = 1}dis(i,j) = \sum^n_{i = 1}\sum^{n}_{j = 1}\sum_{(u, v)}[(u, v)是否在 i\to j 路径上] = \sum_{(u, v)}\sum^n_{i = 1}\sum^n_{j = 1}[(u, v)是否在 i\to j 路径上] =\sum_{(u,v)}(u,v)在几条路径上\)

经过 \((u,v)\) 的肯定一个在子树内,一个在子树外。

考虑分步乘法计数原理。

因此答案为 \(\displaystyle\sum_{i = 1}^n(n - size[i]) \cdot size[i] \cdot 2\)

例2

image

\(g[i]\) 表示 \(i\) 的子树内被标记的点到 \(i\) 的距离的最大值。

\(g[i] =\max\{g[p] + 1\}\)

再记一个次大值 \(h[i]\)

考虑将根从 \(i\) 换到 \(j\)

如果 \(g[i] = g[j] + 1\),则 \(g[i]\) 是由 \(g[j]\) 转移过来的,所以不能再用 \(g[i]\) 了,要用 \(h[i]\);否则就还用 \(g[i]\)

\(maxv\) 表示外面标记的点的走到 \(now\) 的最大距离,则新的 \(maxv = max\{maxv, h[i]/g[i]\} + 1\)

状压DP

状态压缩:把数组表示成数。

因为 DP 的时候又是需要数组来存当前的状态。

image

变化的量:哪里、每个点有没有走过、距离。

每个点有没有走过是一个状态(布尔数组),所以可以装状压。

点击查看代码
double f[1<<maxn][maxn];
//f[s][i] s代表哪些点已经走过的状压数组 i当前走到哪个点了
//f[i][s] 

int main()
{//O(n^2 * 2^n) n = 18
//暴力O(n!) n = 11
	cin >> n;
	for (int i=0;i<n;i++)
		cin >> x[i] >> y[i];
	for (int i=0;i<(1<<n);i++)
		for (int j=0;j<n;j++)
			f[i][j] = 1e+20;
	f[1][0] = 0;
	for (int i=1;i<(1<<n);i++)
		for (int j=0;j<n;j++)//要从f[i][j]这个状态向外转移
			if (i>>j&1)//i的二进制第j位需要是1 才是合法的状态 代表j点走过
				for (int k=0;k<n;k++) //要走到k点去
					if ((i>>k&1)==0)//k点没有走过 可以走
						f[i|(1<<k)][k] = min(f[i|(1<<k)][k], f[i][j] + dis(j,k)); 
		
	return 0;
}

复杂度是 \(\mathcal O(n^22^n)\)

\(n \leq 18\)

暴力的复杂度是 \(n!\)

\(n\leq 10\)

例3

image

一行一行去放:

\(f[i][j][s]\) 表示 \(1\sim i\) 行已放完,放了 \(j\) 个国王,第 \(i\) 行哪些格子有国王。

点击查看代码
#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];
					}
}

一个格子一个格子去放:

\(f[i][j][k][s]\) 表示放到 \((i,j)\),放了 \(k\) 个国王,下面红线(轮廓线)上方每个位置放没放国王的方案数。

image

其实是插头 DP。

点击查看代码
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];
		}
} 

这样更快,但是更麻烦。

例4

Luogu 炮兵阵地

状压两行。

\(n\) 比较大,所以状压 \(m\)

例5

Luogu Corn Fields G

如果直接整行整行地转移会比较慢。

插头会比较快。

例6

image

\(f[i]\) 表示把 \(1\sim i\) 全部染好的最小代价。

枚举一个断点 \(j\)\(f[i] = \min\{f[j] + (j + 1\sim i 有多少种不同的颜色)^2\}\)

复杂度 \(=\) 状态 \(\times\) 转移。

状态已经无法优化,所以优化转移。

这个平方非常关键。

最优解有一个上界。

最优解 \(\leq n\)

每次只选一个位置染色。

因此选择的区间不同颜色的数量必须 \(\leq \sqrt{n}\)

可能有好几个区间有相同种类数的颜色。

所以要保持种类数不变的情况下让区间尽量大。

维护从 \(i\) 开始向前走颜色数量有 \(1,2,\dots,\sqrt{n}\) 个走最远能到哪。

这样就不用枚举每一个位置了。

于是,维护 \(\sqrt{n}\) 个双指针。

例7

image

\(f[i][s]\) 表示前 \(i\) 年,每一块地是不是荒地,能获得的最大价值。

开荒的顺序对答案无影响。

所以可以按某种顺序排序。

image

从小到大排序。

\(f[i]\) 表示前 \(i\) 次种地。

如果这段区间有被开荒过,那一定是前缀。

所以只需要知道前缀的终点就行。

所以 \(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}\leq j\leq r_{i + 1})\),则 \(f[i + 1][r_{r + 1}] = f[i][j] - sum(j + 1, r_{j + 1}) + p_{i + 1}\),否则 \(f[i + 1][j] = f[i][j] + p_{i + 1}\)

点击查看代码
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]);
		} 
}

复杂度还是太高了。

先滚动数组。

可以用线段树分三种情况优化。

例8

image

正难则反。

\(f[l][r]\) 表示 \(l\sim r\) 在原序列中形成几个区间。

image

如果把 \(i\) 加入区间时, \(i - 1\)\(i + 1\) 都没有被加入,就是 \(+1\)

如果有一个加入,则不变;

如果都加入,个数 \(-1\)

\(r\) 这一维滚动掉。

考虑从 \(r\)\(r + 1\)

image

\(r + 1 < a_{i - 1},a_{i + 1}\),所有的 \(1\leq l\leq r + 1\) 都要 \(+1\)

当只小于其中一个时(假设是 \(a_{i - 1}\)),则所有的 \(l\leq a_{i + 1}\) 都不变,所有的 \(l > a_{i + 1}\)\(+1\)

当大于所有的时,假设 \(a_{i - 1} < a_{i + 1}\),则所有的 \(l \leq a_{i - 1}\)\(-1\),所有的 \(a_{i - 1} < l \leq a_{i + 1}\) 都不变,所有的 \(a_{i + 1} < l \leq r + 1\)\(+1\)

用线段树维护区间加和 \(\leq 2\) 的个数。

具体:维护最小值、次小值、最小值的个数、次小值的个数。

数位DP

统计 \(l\sim r\) 各位数字之和。

点击查看代码
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";
}
posted @ 2025-05-04 10:35  SigmaToT  阅读(58)  评论(1)    收藏  举报