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

\(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

记 \(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 的时候又是需要数组来存当前的状态。

变化的量:哪里、每个点有没有走过、距离。
每个点有没有走过是一个状态(布尔数组),所以可以装状压。
点击查看代码
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

一行一行去放:
\(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\) 个国王,下面红线(轮廓线)上方每个位置放没放国王的方案数。

其实是插头 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
状压两行。
\(n\) 比较大,所以状压 \(m\)。
例5
如果直接整行整行地转移会比较慢。
插头会比较快。
例6

令 \(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

\(f[i][s]\) 表示前 \(i\) 年,每一块地是不是荒地,能获得的最大价值。
开荒的顺序对答案无影响。
所以可以按某种顺序排序。

从小到大排序。
\(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

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

如果把 \(i\) 加入区间时, \(i - 1\) 和 \(i + 1\) 都没有被加入,就是 \(+1\);
如果有一个加入,则不变;
如果都加入,个数 \(-1\)。
把 \(r\) 这一维滚动掉。
考虑从 \(r\) 到 \(r + 1\)。

当 \(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";
}

浙公网安备 33010602011771号