You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

955 lines
33 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 换根$DP$
换根$DP$,又叫二次扫描,是树形$DP$的一种。
其相比于一般的树形$DP$具有以下特点:
- ① 以树上的不同点作为根,其解不同
- ② 故为求解答案,不能单求某点的信息,需要求解每个节点的信息
- ③ 故无法通过一次搜索完成答案的求解,因为一次搜索只能得到一个节点的答案
难度也就要比一般的树形$DP$高一点。
### 题单
#### **[$P3478$ $STA-Station$](https://www.luogu.com.cn/problem/P3478)**
> **题意**:给定一个$n$个点的无根树,问以树上哪个节点为根时,其所有节点的深度和最大?
**深度**:节点到根的简单路径上边的数量
> **关键词:换根$DP$模板题**
如果我们假设某个节点为根,将无根树化为有根树,在搜索回溯时统计子树的深度和,则可以用一次搜索算出以该节点为根时的深度和,其时间复杂度为 $O(N)$。
但这样求解出的答案只是以该节点为根的,并不是最优解。
如果要暴力求解出最优解,则我们可以枚举所有的节点为根,然后分别跑一次搜索,这样的时间复杂度会达到$O(N^2)$,显然不可接受。
所以我们考虑在第二次搜索时就完成所有节点答案的统计——
- ① 我们假设第一次搜索时的根节点为$1$号节点,则此时只有$1$号节点的答案是已知的。同时第一次搜索可以统计出所有子树的大小。
- ② 第二次搜索依旧从$1$号节点出发,若$1$号节点与节点$x$相连,则我们考虑能否通过$1$号节点的答案去推出节点$x$的答案。
- ③ 我们假设此时将根节点换成节点$x$,则其子树由两部分构成,第一部分是其原子树,第二部分则是$1$号节点的其他子树(如下图)。
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401090958197.png)
- ④ 根从$1$号节点变为节点$x$的过程中,我们可以发现第一部分的深度降低了$1$,第二部分的深度则上升了$1$,而这两部分节点的数量在第一次搜索时就得到了。
故得到递推公式:
$$f[v]=f[u]-siz[v]+(siz[1]-siz[v]),fa[v]=u$$
简化一下就是
$$f[v]=f[u]+siz[1]-2\times siz[v]=f[u]+n-2\times siz[v]$$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010, M = N << 1;
#define int long long
#define endl "\n"
// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int n; // n个节点
int depth[N]; // depth[i]:在以1号节点为根的树中,i号节点的深度是多少
int sz[N]; // sz[i]:i号节点为根的子树中有多少个节点
int f[N]; // DP结果数组,f[i]记录整个树以i为根时,可以获取到的深度和是多少
// 第一次dfs
void dfs1(int u, int fa) {
sz[u] = 1; // u为根的子树,最起码有u一个节点
depth[u] = depth[fa] + 1; // u节点的深度是它父节点深度+1
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
dfs1(v, u); // 深搜v节点,填充 sz[v],depth[v]
sz[u] += sz[v]; // 在完成了sz[v]和depth[v]的填充工作后,利用儿子更新父亲的sz[u]+=sz[v];
}
}
// 第二次dfs
void dfs2(int u, int fa) {
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
f[v] = f[u] + n - 2 * sz[v];
dfs2(v, u);
}
}
signed main() {
memset(h, -1, sizeof h); // 初始化链式前向星
cin >> n;
for (int i = 1; i < n; i++) { // n-1条边
int a, b;
cin >> a >> b;
add(a, b), add(b, a); // 换根DP无向图
}
// 1、第一次dfs,以1号节点为根它的父节点不存在传入0
dfs1(1, 0);
// 2、换根
for (int i = 1; i <= n; i++) f[1] += depth[i]; // DP初始化以1号节点为根时所有节点的深度和
dfs2(1, 0); // 从1号节点开始深度进行换根
// 3、找答案
int ans = 0, id = 0;
for (int i = 1; i <= n; i++) // 遍历每个节点
if (ans < f[i]) ans = f[i], id = i; // ans记录最大的深度值,id记录以哪个节点为根时取得最大值
// 输出以哪个节点为根时,深度和最大
cout << id << endl;
}
```
**总结与进阶**
由此我们可以看出换根$DP$的套路:
- 指定某个节点为根节点。
- 第一次搜索完成预处理(如子树大小等),同时得到该节点的解。
- 第二次搜索进行换根的动态规划,由已知解的节点推出相连节点的解。
#### [$P1364$ 医院设置](https://www.luogu.com.cn/problem/P1364)
**一、$O(N^3)$算法**
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010;
const int INF = 0x3f3f3f3f;
int g[150][150];
int w[N];
int main() {
int n;
cin >> n;
// 地图初始化
memset(g, 0x3f, sizeof g);
for (int i = 1; i <= n; i++) g[i][i] = 0;
for (int i = 1; i <= n; i++) {
int a, b;
cin >> w[i] >> a >> b;
g[i][a] = g[a][i] = 1; // 左链接,右链接,二叉树,和一般的不一样
g[i][b] = g[b][i] = 1;
}
// floyd
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (g[i][j] > g[i][k] + g[k][j]) g[i][j] = g[i][k] + g[k][j];
int ans = INF;
for (int i = 1; i <= n; i++) {
int s = 0;
for (int j = 1; j <= n; j++) s += w[j] * g[i][j];
ans = min(ans, s);
}
printf("%d", ans);
return 0;
}
```
**二、$O(N^2)$算法**
$n$ 的值很小,最多可以有 $O(n^3)$ 的时间复杂度。
那么就可以枚举每一个节点,计算它的 **最小距离和** ,再统计答案。
**最小距离和** 怎么计算呢?容易想到的是枚举所有节点,算出两个节点之间的距离,再乘上这个节点的价值。
这样就需要求出节点之间的距离。先枚举起点,然后算出每个节点到这个起点间的距离。我用的是一个朴素的 $dfs$,在搜索的过程中累加距离,每搜索到一个节点,就储存这个节点与起点间的距离。
而累加距离也很容易实现,在从一个节点遍历到下一个节点时,$step$ 增加 $1$
代码就很好实现了,时间复杂度也不高,$O(n^2)$。
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = N << 1;
const int INF = 0x3f3f3f3f;
int n;
int x[N]; // 点权权值数组
int st[N]; // st 数组存是否遍历过这个节点
int dis[N][N]; // 存节点间的距离
// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
void dfs(int root, int u, int step) { // root 表示根,u:当前走到哪个节点,step:u点时走了几步
st[u] = 1; // u走过了,防止回头路
dis[root][u] = step, dis[u][root] = step; // root<->u之间的路径长度
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (st[v]) continue;
dfs(root, v, step + 1);
}
}
int main() {
// 初始化链式前向星
memset(h, -1, sizeof h);
cin >> n;
for (int i = 1; i <= n; i++) {
int a, b;
cin >> x[i] >> a >> b;
if (a) add(i, a), add(a, i); // 存图
if (b) add(i, b), add(b, i);
}
for (int i = 1; i <= n; i++) {
memset(st, 0, sizeof st);
dfs(i, i, 0); // 搜索
}
int ans = INF;
for (int i = 1; i <= n; i++) {
int s = 0;
for (int j = 1; j <= n; j++)
s = s + x[j] * dis[i][j]; // 累加距离
ans = min(ans, s);
}
cout << ans << endl;
return 0;
}
```
**三、$O(N)$算法**
如果$n=1e6$,那么就要考虑换根$dp$了
我们考虑相邻的医院是否存在转换关系,设其中一个医院为$u$(父节点),另一个为$v$(子节点)
如果把$u$点的医院改为$v$点,则发现:
如图:以$5$为根时:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401100756011.png)
以$4$为根时:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401100756839.png)
以$v$为根的子树的集合的所有人少走$1$步,但是另一集合的所有人要多走一步
设$sz[i]$表示以$i$为根节点的集合人的总数,$f[i]$表示在$i$点设置医院的代价,则可转换成:
$$\large f[v]=f[u]+(sz[1]-sz[v])-sz[v]=f[u]+sz[1]-2\times sz[v]$$
> **注** 其中$sz[1]$表示全部人的数量,一般也写做$n$
**思路**
先算出$1$个点的代价,之后$dp$换根直接转换
**$Code$**
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = N << 1;
const int INF = 0x3f3f3f3f;
// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int c[N];
int f[N], sz[N];
int ans = INF;
// 第一次dfs,获取在以1为根的树中:
// 1、每个节点分别有多少个子节点填充sz[]数组
// 2、获取到f[1],f[1]表示在1点设置医院的代价
// 获取到上面这一组+一个数据才能进行dfs2进行换根
void dfs1(int u, int fa, int step) {
sz[u] = c[u]; // 这个挺绝啊~,与一般的统计子树节点个数不同,这里把人数,也就是点权值,也看做是一个节子点,想想也是这个道理
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
dfs1(v, u, step + 1); // 填充深搜v节点为根的子树
sz[u] += sz[v]; // 在完成了v节点的数据统计后用v节点的sz[v]结果累加到sz[u]
}
f[1] += step * c[u]; // 累加步数*人数 = 1点的总代价,预处理出1点的总代价
}
// 第二次dfs,开始dp换根
void dfs2(int u, int fa) {
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
f[v] = f[u] + sz[1] - sz[v] * 2; // 经典的递推式
dfs2(v, u); // 继续深搜
}
}
int main() {
// 初始化链式前向星
memset(h, -1, sizeof h);
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> c[i];
int a, b;
cin >> a >> b;
if (a) add(a, i), add(i, a); // 是一个二叉树结构,与左右节点相链接,但有可能不存在左或右节点,不存在时,a或b为0
if (b) add(b, i), add(i, b);
}
// 1、准备动作
dfs1(1, 0, 0);
// 2、换根dp
dfs2(1, 0);
// 输出答案
for (int i = 1; i <= n; i++) ans = min(ans, f[i]);
cout << ans << endl;
return 0;
}
```
#### [$P2986$ 伟大的奶牛聚集](https://www.luogu.com.cn/problem/P2986)
**题目描述**
$Bessie$ 正在计划一年一度的奶牛大集会,来自全国各地的奶牛将来参加这一次集会。当然,她会选择最方便的地点来举办这次集会。
每个奶牛居住在 $N$ 个农场中的一个,这些农场由 $N-1$ 条道路连接,并且从任意一个农场都能够到达另外一个农场。道路 $i$ 连接农场 $A_i$ $B_i$,长度为 $L_i$。集会可以在 $N$ 个农场中的任意一个举行。另外,每个牛棚中居住着 $C_i$ 只奶牛。
在选择集会的地点的时候,Bessie 希望最大化方便的程度(也就是最小化不方便程度)。比如选择第 $X$ 个农场作为集会地点,它的不方便程度是其它牛棚中每只奶牛去参加集会所走的路程之和(比如,农场 $i$ 到达农场 $X$ 的距离是 $20$,那么总路程就是 $C_i\times 20$)。帮助 $Bessie$ 找出最方便的地点来举行大集会。
**题目分析**
这还分析个啥啊,这不就是上一道题的医院选址吗?
**$Code$**
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = N << 1;
#define int long long
#define endl "\n"
// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int c[N]; // 点权数组
int sz[N]; // sz[i]:在以1号节点为根时,i号节点的子节点数量
int dis[N]; // dis[i]:表示i距离起点的长度
int f[N]; // f[i]:把奶牛大集会的地点设为i时的最小代价
int ans = 1e18;
// 第一次dfs,获取在以1为根的树中:
// 1、每个节点分别有多少个子节点,填充sz[]数组
// 2、获取到f[1],f[1]表示在1点设置医院的代价
// 获取到上面这一组+一个数据,才能进行dfs2进行换根
void dfs1(int u, int fa) {
sz[u] = c[u]; // 这个和医院选址是一样的,点权就是子节点个数
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
dis[v] = dis[u] + w[i]; // 每个点到根节点的距离,这个和医院选址是不一样的,那个是一步+1,用step记录即可,这个还有边权
dfs1(v, u); // 深搜
sz[u] += sz[v]; // u为根的子树奶牛数量
}
f[1] += dis[u] * c[u]; // 累加 距离*人数=1点的总代价
}
// 第二次dfs,开始dp换根
void dfs2(int u, int fa) {
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
f[v] = f[u] + (sz[1] - sz[v] * 2) * w[i];
dfs2(v, u);
}
}
signed main() {
// 初始化链式前向星
memset(h, -1, sizeof h);
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> c[i];
for (int i = 1; i < n; i++) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
// 1、准备动作
dfs1(1, 0);
// 2、换根dp
dfs2(1, 0);
// 输出答案
for (int i = 1; i <= n; i++) ans = min(ans, f[i]);
cout << ans << endl;
}
```
#### [$CF1187E$ $Tree$ $Painting$](https://www.luogu.com.cn/problem/CF1187E)
https://www.luogu.com.cn/blog/defKaeru/solution-cf1187e
**题意**
给定一棵有 $n$ 个结点的无根树,所有结点都是白色的。
第一次操作可以 <font size=5 color='red'><b>随意</b></font> 使一个结点染成黑色,之后每次操作可以使一个与黑色结点相邻的白色结点变成黑色。
**每次操作可以获得的权值为**: 被染成黑色的白色结点所在的白色连通块的结点数量。
求可以获得的最大权值。
**难点解析**
最初时,我对这个**权值** 的认识不够深入,没有明白为什么根不同权值就会不同呢?后来仔细思考,发现是自己傻了,因为根不同,每个节点到根的距离就会不同,而权值的计算办法,其实是类似于哈夫曼树,父子隶属关系的不同最终的权值是不一样的,我们可以再画一下上面的图进行深入理解:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401160950270.png)
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401160947763.png)
**总结**:这个理解能力,目前看来有两种办法:
- ① 多动手画图理解,尝试换根试一下。
- ② 多做题,做的多了就一下明白它说什么了。
**题解**
不难发现只要选定了第一个被染色的结点,答案也就确定了, 也就是 选了谁是根最重要,其它的选择顺序不重要。
所以有一个朴素做法就是以枚举每个结点为根,都做一次树形$dp$。
以某一结点为根,记 $f_i$ 表示以 $i$ 为根的子树中,首先把 $i$ 染成黑色的答案。
方程就是
$\displaystyle \large f[u]=sz[u]+\sum_{v \in son[u]} f[v]$
其中
$sz[u]$ 表示以 $u$ 为根的子树大小,也就是染色时的白色连通块大小。
**时间复杂度**
$O(n^2)$ ,稳稳地暴毙,然后就会自然而然地想到换根$dp$。
**换根$dp$**
先考虑以任意一点为根,不妨记为 $1$ ,求出 $f$ 数组。
然后记 $g[i]$ 表示以 $i$ 结点为根时的答案,尝试通过$1$号节点的计算已知值,进行换根,利用数学变换加快运算速度。
显然,由于1号节点是根它没有向上走的路径所以它的向下所有获取的价值就是总价值也就是 $g[1] =f[1]$
然后考虑 $g$ 数组从 **父亲到儿子** 的转移。
以样例为例:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401151619931.png)
我们假设当前以 $1$ 号为根,求出了 $f$ 数组,也就是知道了 $g[1]=f[1]$ ,然后要求出 $g[2]$ 。
考虑一下答案的组成。
首先考虑 $2$ 号结点的孩子的贡献,也就是图中蓝圈内的部分。这部分显然不会改变,贡献就是 $f[2] sz[2]$ 。
然后考虑父亲方向,也就是图中红圈部分对 $g[2]$ 的贡献。
那么除了以 $2$ 号结点,与 $1$ 号结点相邻的其他子树都会对答案产生贡献,也就是说,我们只需要用以 $1$ 号结点为根时的权值减去以
$2$ 为根的子树的贡献即可,也就是 $g[1]-f[2]-sz[2]$ 。
不要忘了加上
<EFBFBD>
n ,也就是初始的白色连通块大小。
综合一下上述两种方向的贡献,可以得到:
<EFBFBD>
2
=
(
<EFBFBD>
2
<EFBFBD>
<EFBFBD>
<EFBFBD>
2
)
+
(
<EFBFBD>
1
<EFBFBD>
2
<EFBFBD>
<EFBFBD>
<EFBFBD>
2
)
+
<EFBFBD>
=
<EFBFBD>
1
+
<EFBFBD>
<EFBFBD>
<EFBFBD>
<EFBFBD>
2
×
2
g
2
=(f
2
siz
2
)+(g
1
f
2
siz
2
)+n=g
1
+nsiz
2
×2 。
推广到所有结点,就可以得到:
<EFBFBD>
<EFBFBD>
=
<EFBFBD>
<EFBFBD>
<EFBFBD>
<EFBFBD>
<EFBFBD>
<EFBFBD>
<EFBFBD>
+
<EFBFBD>
<EFBFBD>
<EFBFBD>
<EFBFBD>
<EFBFBD>
×
2
g
u
=g
father
u
+nsiz
u
×2
然后跑两遍 dfs 就愉快的解决啦。
#### [$CF1324F$.$Maximum$ $White$ $Subtree$](https://www.luogu.com.cn/problem/CF1324F)
**题目大意**
- 给定一棵 $n$ 个节点无根树,每个节点 $u$ 有一个颜色 $a_u$,若 $a_u$ 为 $0$ 则 $u$ 是黑点,若 $a_u$ 为 $1$ 则 $u$ 是白点。
- 对于每个节点 $u$,选出一个**包含** $u$ 的连通子图,设子图中白点个数为 $cnt_1$,黑点个数为 $cnt_2$,请最大化 $cnt_1 - cnt_2$。并输出这个值。
- $1 \leq n \leq 2 \times 10^5$$0 \leq a_u \leq 1$。
**思路分析**
这题要求的是求出对任何一个节点$v$,求出包含这个节点的子树$cnt_1cnt_2$的最大值。
**暴力想法**
首先思考下暴力写法应该如何写。
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401101430095.png)
对于所有可能的路径的贡献值的累加,且贡献值需大于等于$0$。
>- 白的比黑的多,有分, 这时我们选上这棵子树
>- 黑的比白的多,没分, 这时我们放弃这棵子树
不妨设$f[u]$代表$u$结点的最大值。故
$$\large f[u]=c[u]+\sum_{v \in son_u}max(0,f[v])$$
假如用暴力写法,就是对于每个结点$u$,暴力搜索所有的相邻结点,利用$dfs$暴力搜索。也就是以每个结点为棵出发,枚举$n$次$dfs$,但是结点最大为$210^5$ 这个暴力算法显然会超时,考虑如何优化。
**算法优化**
 对于从下往上的贡献,可以利用从下往上的$dfs$树形$dp$进行获取,难求的是刨去以$v$为根的子树的贡献值,也就是向上走的那部分。
设$u$为节点$v$的父节点,$f[v]$代表从下往上以$v$为根的 **白点数减去黑点数** 的 **最大值**,$g[v]$代表最终的最大值。
根据刨去以$v$为根的子树的贡献值这个思想,可以发现:
$$\large add=g[u]max(0,f[v])$$
> **注**$fa[v]=u$
就是刨去以$v$为根的子树的贡献值。写出状态转移方程:
$$\large g[v] =
\left\{\begin{matrix}
f[v] & if \ v = root \\
f[v]+max(0,g[u]-max(0,f[v]))& if \ v \neq root
\end{matrix}\right.
$$
因此思路:
- ① 从下往上树形$dp$,计算$f[v]$
- ② 从上往下换根$dp$,计算$g[v]$
**$Code$**
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10, M = N << 1;
// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int f[N];
int g[N];
int c[N]; // 颜色
int n; // 节点数量
// 以1号节点为根跑一遍dfs,填充每个节点的cnt1-cnt2的最大值
void dfs1(int u, int fa) {
f[u] = c[u]; // 1白色-1黑色正好与 cnt1-cnt2一致,初始值加上了老头子自己的养老钱
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
dfs1(v, u);
f[u] += max(0, f[v]); // 如果我儿子给我,那我就拿着;如果我儿子不给我钱,或者管我要钱,我就不理它!
}
}
// 换根dp
void dfs2(int u, int fa) {
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
int val = g[u] - max(f[v], 0);
g[v] = f[v] + max(val, 0);
dfs2(v, u);
}
}
int main() {
// 初始化链式前向星
memset(h, -1, sizeof h);
cin >> n;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
c[i] = (x ? x : -1); // 白色c[i]=1黑色c[i]=-1
}
for (int i = 1; i < n; i++) {
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
// 第一次dfs
dfs1(1, 0);
// 它们两个是一个意思
g[1] = f[1];
// 换根dp
dfs2(1, 0);
// 输出答案
for (int i = 1; i <= n; i++) printf("%d ", g[i]);
return 0;
}
```
#### [$P3047$ $Nearby$ $Cows$ $G$](https://www.luogu.com.cn/problem/P3047)
**题目大意**
给你一棵 $n$ 个点的树,点带权,对于每个节点求出距离它不超过 $k$ 的所有节点权值和。
对于树中的某个节点而言,距离它不超过$k$的节点主要来源于两方面:
- 一个是该节点的子节点中距离该节点不超过距离$k$的节点的权值和
- 一个是该节点向上沿着父节点方向不超过距离$k$的点的权值和
对于子节点方向的节点的权值和,可以通过普通的树形$DP$计算出来。
**1、状态表示**
$f[i][j]$表示以$i$为根节点的子树中,距离$i$不超过$j$的子节点的权值和。
**2、状态转移**
$$\large f[u][j]=val[u]+\sum_{v \in son[u]}f[v][j1] \ j \in [1,k]$$
到节点$u$不超过距离$k$,即距离$v=son[u]$不超过$k1$,然后加在一起即可。同时$u$节点本身也有贡献,因为$u$节点本身是不超过距离$0$的节点。
> **理解**:父亲的生活费=$\sum$(每个儿子给的生活费)+自己的社保金
```cpp {.line-numbers}
void dfs1(int u, int fa) {
// 初始化:当遍历到u节点时,u的拆分状态中,最起码包含了自己的点权值
for (int i = 0; i <= k; i++) f[u][i] = val[u];
// 枚举u的每一个子节点
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue; // 如果是u的父亲,那么就跳过,保证只访问u的孩子
// 先递归,// 递归填充v节点的信息
dfs1(v, u);
// 再利用子节点信息更新父节点信息
for (int j = 1; j <= k; j++) f[u][j] += f[v][j - 1];
}
}
```
**3、换根$DP$**
这个题目本身是个无根树,如果我们认为规定编号为$1$的节点是根的话,那么对于祖宗节点$1$来说,$f[1][k]$就是距离$1$节点不超过距离$k$的节点的权值和。因为祖宗节点是没有父亲节点的,所以我们就不需要考虑沿着父节点方向的节点权值和。
令:$g[u][j]$表示所有到$u$节点的不超过距离$j$的节点的权值和。根据刚刚的分析:
$$\large g[1][j]=f[1][j]\ j \in [1,k]$$
这个就是我们换根$DP$的 **初始化**。
> **注**:我们完全可以去把每个点都当作根,然后暴力跑出答案,但是这个暴力做法的时间复杂度是$O(n^2)$的,会超时。
所以当我们将祖宗节点从节点$1$换为另一个节点的时候,我们只能通过数学上的关系来计算出$g$数组元素的值。这个也是换根$DP$的意义。
我们看下面的图:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202401151412377.png)
红色框是非常好理解的,以$v$为根的子树,在最远距离为$k$的限制下,写成$f[v][k]$。上面的部分,我们可以写成$g[u][k-1]$。因为到$v$不超过$k$的距离,即距离它的父亲节点不超过$k1$的距离。
但是这么写对吗?
答案是不对的,$g[u][k-1]$和$f[v][k]$是有重复部分的。我们需要减去这段重复的部分,那么关键问题是重复部分如何表示?
重复部分肯定是出现在了红色框中,红色框中到$u$不超过距离$k1$,即距离$u$不超过$k-2$,同时重复部分又恰好是节点$v$的子节点,所以这部分可以表示为:$f[v][k-2]$。
所以最终的结果就是:
$$\large g[v][k]=f[v][k]+g[u][k1]f[v][k2]$$
> **解释**
> ① 换根$DP$时,由父推子,也就是用$g[u][?] \rightarrow g[v][??]$
> ② 由于$v$需要向上,通过$u$去寻找点权和,而$v \rightarrow u$已经用去了$1$步,一共$k$步,现在就剩下了$k-1$步。
> ③ $Q$:那为什么不是$f[u][k-1]$,而是$g[u][k-1]$呢?
> 因为$u$不光有向下的,还有向上的啊!我们现在不光要向下的,还要向上的,当然是$g[u][k-1]$啦!
> ④ 但是简单的这么整是不行的:$g[u][k-1]$与$f[v][k]$是存在交集的,如果简单加上就会造成一部分被算了两次!那么,是哪部分被算了两次呢?
> 答:对于$u$节点而言,$g[u][k-1]$与$f[v][k]$的交集,需要先走$1$步进入红框,这样,就用去了$1$步,也就是$f[v][k-2]$就是重复的部分,利用容斥原理去掉就行了,也就是$g[v][k]=f[v][k]+g[u][k1]f[v][k2]$
细心的同学发现,这面的状态转移方程是有边界问题的:$k-2$是不是一定大于等于$0$呢?
如果$k-2<=0$咋办?会不会造成代码$RE$或者$WA$?
也就是说,上述方程成立的条件是$k\geq 2$的。
所以我们还得想一想$\leq 1$的时候。
如果$k=0$$g[v][0]$其实就是$val[v]$,因为不超过距离$0$的点只有本身。
如果$k=1$,那么$g[v][1]$其实就是$f[v][1]+val[u]$,因为沿着父节点方向距离$v$不超过$1$的点,只有父节点,而树中,父节点是唯一的。沿着子节点方向,其实就是$v$的各个子节点,而这些子节点可以统统用$f[v][1]$表示。
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = N << 1;
const int K = 25;
// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int f[N][K]; // f[i][j]:如果根是1号节点时i号节点最远走j步可以获取到的所有点权和
int g[N][K];
int val[N]; // 点权数组
int n, k;
void dfs1(int u, int fa) {
// 初始化当遍历到u节点时u的拆分状态中最起码包含了自己的点权值
for (int i = 0; i <= k; i++) f[u][i] = val[u];
// 枚举u的每一个子节点
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue; // 如果是u的父亲那么就跳过,保证只访问u的孩子
// 先递归,// 递归填充v节点的信息
dfs1(v, u);
// 再利用子节点信息更新父节点信息
for (int j = 1; j <= k; j++) f[u][j] += f[v][j - 1];
}
}
// 换根dp
void dfs2(int u, int fa) {
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == fa) continue;
g[v][0] = val[v]; // 走0步,只有自己一个点
g[v][1] = f[v][1] + val[u]; // 走1步,包含自己下面子树一层+父节点
// 如果走2步及以上最多k步以内
for (int j = 2; j <= k; j++) g[v][j] = f[v][j] + g[u][j - 1] - f[v][j - 2];
// 再递归,利用父更新子
dfs2(v, u);
}
}
int main() {
// 初始化链式前向星
memset(h, -1, sizeof h);
cin >> n >> k;
for (int i = 1; i < n; i++) { // n-1条边
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
for (int i = 1; i <= n; i++) cin >> val[i]; // 点权
// 1、自底向上
dfs1(1, 0);
// 2、换根dp
for (int i = 0; i <= k; i++) g[1][i] = f[1][i];
dfs2(1, 0);
// 输出结果
for (int i = 1; i <= n; i++) cout << g[i][k] << endl;
return 0;
}
```
####[$CF708C$ $Centroids$](https://www.luogu.com.cn/problem/CF708C)
#### [$P6419$ $Kamp$](https://www.luogu.com.cn/problem/P6419)
**题目大意**
一颗树 $n$ 个点,$n-1$ 条边,经过每条边都要花费一定的时间,任意两个点都是联通的。
$K$ 个人(分布在 $K$ 个不同的点)要集中到一个点举行聚会。
聚会结束后需要一辆车从举行聚会的这点出发,把这 $K$ 个人分别送回去。
请你回答,对于 $i=1 \sim n$ ,如果在第 $i$ 个点举行聚会,司机最少需要多少时间把 $K$ 个人都送回家。
**思路**
换根$dp$的特征感觉还是非常明显的,就是答案 **可被移动**
具体点就是可以在$log_n$或者是$n$的时间内通过计算来把一个节点的答案转化得到另一个节点的答案
这题就很明显,虽然要分类讨论,还有其他的挺多的限制和要求...主要是分类讨论很烦人。。
- 第一遍$dfs$直接把从每个点出发经过它子树内所有终点并返回的路径长度算出来
这个是最基础的,还有一些其它要算的我后面会说
- 第二遍$dfs$就需要算出来全局的答案了,就是从一个点出发,**经过全图所有点并返回的路径长**
如果有仔细一点,推过样例的同学就会发现,答案要求的不是这个,而是 **不返回的最短路径**
就是车车把最后一个人送回家后,离开停下,到此为止它开的最短路径
这个路径和我们统计的路径有什么区别呢?
很明显,有一个容易证明的贪心,这个最短路径,就是我们上面算出来的路径减去这个点到离这个点最远的点路径长
(因为不用返回嘛)
那么,很明显,如果我们只是统计一个子树的根节点到这个子树内的终点的最短路径,这个在第一个dfs内就可以直接完成
问题就在于,怎么计算整棵树范围的,也就是全图的答案。
假如这个子树的根节点就是这棵树的根,那很明显这个最短路径就是我们要的最短路径
那我们在第一遍dfs的时候所用的根就是我们第二遍换根dp的起点
那我们现在要考虑的就是,已知一个节点作为跟的时候的答案,怎么把这个答案转移到其子节点上?也就是如何换根
要分类讨论了
首先我们以1号节点为根
f[u] 从点u出发把所有在u的子树里的人送回家并返回u的距离
ans[u]从点u出发把所有人出发并返回u的距离
dis[u] u出发,在u的子树内,距离u最远的那个人的家到u的距离
sdis[u] u出发,在u的子树内,距离u次远的那个人家到u的距离并且这个人的家和dis[u]不在同一个子树内(这个东西的作用后面会说到)
up[u] 不在u的子树内,距离u最远的那个人的家到u的距离
siz[u] uu的子树内有多少个人的家
f[u],siz[u],dis[u],sdis[u]在第一遍dfs内就可以求出来
关键的是up[u]和ans[u]的计算
fau的父亲节点,wufa之间的路径长度
ans[u]的计算规则
1.当siz[u]==0时,ans[u]=ans[fa]+w*2
2.当siz[u]==k(就是所有的重点都在这个子树内)时,ans[u]=f[u]
3.其他的情况,ans[u]=fa[u]
up[u]的计算规则就更复杂一些
1.当siz[u]==0up[u]=up[fa]+w
2.当siz[u]==kup[u]=0
3.其他情况
  有两种,一种是最长的就是up[fa]+w
  一种是最长的要经过它的兄弟节点
  第二种还有两种情况。。
  如果dis[u]>up[u]就是上面说的第二种情况的一种但是这个dis[u]实际上是经过了u的那肯定是不能要的
  但是它的兄弟节点的可能却没有被考虑到
  所以对每一个节点就要记录一个最长路和一个不和最长路相交的次长路也就是dis[u]和sdis[u]
那这样最后的答案就是ans[i]-max(up[i],dis[i])
有一说一真的没有紫题难度。。不知道为什么是紫题
#### [$P3647$ $APIO2014$ 连珠线](https://www.luogu.com.cn/problem/P3647)
#### [$POJ3585$ $Accumulation$ $Degree$](http://poj.org/problem?id=3585)
https://blog.csdn.net/qq_34493840/article/details/90575293
#### [$CF708C$ $Centroids$](https://www.luogu.com.cn/problem/CF708C)
https://www.cnblogs.com/DongPD/p/17498336.html
#### [Eg3: AT Educational DP Contest V-Subtree](https://dmoj.ca/problem/dpv)
#### [$AcWing$ $1073$. 树的中心](https://www.cnblogs.com/littlehb/p/15786805.html)
#### [$AcWing$ $1148$ 秘密的牛奶运输](https://www.cnblogs.com/littlehb/p/16054005.html)