常规树形DP
例题一
题目大意
有一棵树,每个节点都有权值,选点规则是若一个点的父节点被选择后那么该节点就不能被选择,求出选点的最大权值总和。(实际上就是最大独立集的带权情况)
解题思路
每个节点只能有两种状态,选或不选,父子节点的选择会依据树的结构特点互相制约,那么设 d [ u ] [ 0 / 1 ] d[u][0/1] d[u][0/1]代表节点 u u u不选/选得到的最大权值和,那么有:
- 若节点 u u u被选那么其子节点 v v v一定不会被选,即 d [ u ] [ 1 ] = w u + ∑ d [ v ] [ 0 ] d[u][1] = w_u + \sum{d[v][0]} d[u][1]=wu+∑d[v][0]。
- 若节点 u u u不选,其子节点既可以选也可以不选,即 d [ u ] [ 0 ] = ∑ m a x { d [ v ] [ 0 ] , d [ v ] [ 1 ] } d[u][0] = \sum{max\{d[v][0], d[v][1]\}} d[u][0]=∑max{d[v][0],d[v][1]}。
最终的答案从根节点取 m a x { d [ r o o t ] [ 1 ] , d [ r o o t ] [ 0 ] } max\{d[root][1], d[root][0]\} max{d[root][1],d[root][0]}。
#include <bits/stdc++.h>
#include <unordered_map>
using namespace std;
#define ENDL "\n"
#define lowbit(x) (x & (-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<double, double> pdd;
const double eps = 1e-8;
const double pi = acos(-1.0);
const int inf = 0x3f3f3f3f;
const double dinf = 1e300;
const ll INF = 1e18;
const int Mod = 1e9 + 7;
const int maxn = 6e3 + 10;
bitset<maxn> vis;
vector<int> G[maxn];
int a[maxn], d[maxn][2];
void dfs(int fa, int u) {
d[u][1] = a[u];
for (auto v:G[u]) {
if (v == fa) continue;
dfs(u, v);
d[u][1] += d[v][0];
d[u][0] += max(d[v][1], d[v][0]);
}
}
int main() {
//freopen("in.txt", "r", stdin);
//freopen("out.txt","w",stdout);
ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1, u, v; i < n; i++) {
cin >> v >> u;
G[v].push_back(u);
G[u].push_back(v);
}
dfs(0, 1);
cout << max(d[1][0], d[1][1]) << ENDL;
return 0;
}
例题二
题目大意
若一个点被选择那么和它直接相连的边也都被选择,本题就是树的最小点覆盖:从树上选出尽可能少的点使得选出的点能够覆盖到所有的边。
解题思路
这个题也是比较好想的,设 d [ u ] [ 0 / 1 ] d[u][0/1] d[u][0/1]代表节点 u u u不选/选,显然初始化为 d [ u ] [ s ] = s d[u][s] = s d[u][s]=s,那么:
- 若节点 u u u不选,那么其子节点 v v v必须选才能覆盖 u , v u,v u,v这条边, d [ u ] [ 0 ] = ∑ d [ v ] [ 1 ] d[u][0] = \sum{d[v][1]} d[u][0]=∑d[v][1]。
- 若节点 u u u选,其节点 v v v既可以选也可以不选,即 d [ u ] [ 1 ] = ∑ m i n { d [ v ] [ 0 ] , d [ v ] [ 1 ] } d[u][1] = \sum{min\{d[v][0],d[v][1]\}} d[u][1]=∑min{d[v][0],d[v][1]} 。
最终的答案从根节点取 m i n { d [ r o o t ] [ 1 ] , d [ r o o t ] [ 0 ] } min\{d[root][1], d[root][0]\} min{d[root][1],d[root][0]}。
#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn = 1510;
int d[maxn][2];
char s[maxn];
vector<int> G[maxn];
void dfs(int fa, int u) {
d[u][0] = 0, d[u][1] = 1;
for (int i = 0, v; i < G[u].size(); i++) {
v = G[u][i];
if (v != fa) {
dfs(u, v);
d[u][1] += min(d[v][0], d[v][1]);
d[u][0] += d[v][1];
}
}
}
int main() {
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
//ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
while (scanf("%d", &n) != EOF) {
for (int i = 0; i < n; i++) G[i].clear();
memset(d, -1, sizeof d);
for (int i = 1, j, m, u, v; i <= n; i++) {
scanf("%s", s);
j = u = m = 0;
while (isalnum(s[j])) {
u = u * 10 + s[j] - '0';
j++;
}
j += 2;
while (isalnum(s[j])) {
m = m * 10 + s[j] - '0';
j++;
}
for (int k = 0; k < m; k++) {
scanf("%d", &v);
G[u].push_back(v), G[v].push_back(u);
}
}
dfs(-1, 0);
int ans = min(d[0][1], d[0][0]);
printf("%d\n", ans);
}
return 0;
}
例题三
这部分有单独的总结,点击链接跳转。
例题四
题目大意
给出一棵树,每条边都有一个权值(相当于是两点间的距离),对于树上的每个节点,需要计算出树上距离它最远节点的距离。
解题思路
对于节点 u u u,显然它的最远距离只可能来自三个方向:它的父亲方向的最远节点,它的儿子方向的最远节点,它的兄弟方向的最远节点。
最容易求出的是儿子方向的最远距离,但是后两者怎么求?首先考虑求兄弟节点方向上的最远距离,显然就是除了节点 u u u之外节点 f a fa fa儿子的最长链,但是去掉一个节点不好解决,只能求出一个节点所有儿子距离,因此要分两种情况讨论:若节点 u u u方向就是节点 f a fa fa儿子们的最长链,那么兄弟方向上的就是节点 f a fa fa儿子们的次长链,否则兄弟方向上的就是节点 f a fa fa儿子们的最长链。
然后考虑求父亲节点方向的最远距离,只需要自顶向下更新一个来自上方的最远距离,比较特殊的是根节点,他上方的最远距离可以看做是 0 0 0。(先递归后状态转移的可以成为“自底向上”,先状态转移后递归的称为“自顶向下”)又因为对于节点 u u u,其上方既可以看做从兄弟节点过来的,又可以看做从父亲节点上方过来,因此我们在更新子节点时需要将父节点的这两种状态合并取最大值。
设 d [ u ] [ 0 / 1 / 2 ] d[u][0/1/2] d[u][0/1/2]分别代表节点 u u u下的最长链、次长链和来自上方的最长链,最长链和次长链需要单独用一个 d f s dfs dfs更新,我们可以通过判断 d [ v ] [ 0 ] + w ( u , v ) d[v][0] + w_{(u,v)} d[v][0]+w(u,v)和 d [ u ] [ 0 ] , d [ u ] [ 1 ] d[u][0],d[u][1] d[u][0],d[u][1]之间的关系更新最长和次长。
第二次 d f s dfs dfs时,状态转移为:
- 若子节点 v v v的方向为最长链的方向,即 d [ v ] [ 0 ] + w ( u , v ) = = d [ u ] [ 0 ] d[v][0]+w_{(u,v)} == d[u][0] d[v][0]+w(u,v)==d[u][0],那么更新节点 v v v上方的最长链为"父节点 u u u方向上的最大值加上该边的权值"和"父节点 u u u的次长链加上该边的权值"取一个最大值,即: d [ v ] [ 2 ] = m a x ( d [ u ] [ 2 ] + w , d [ u ] [ 1 ] + w ) d[v][2] = max(d[u][2] + w, d[u][1] + w) d[v][2]=max(d[u][2]+w,d[u][1]+w)。
- 若子节点 v v v的方向不是最长链的方向,即 d [ v ] [ 0 ] + w ( u , v ) < d [ u ] [ 0 ] d[v][0]+w_{(u,v)} < d[u][0] d[v][0]+w(u,v)<d[u][0],那么更新节点 v v v上方的最长链为"父节点 u u u方向上的最大值加上该边的权值"和"父节点 u u u的最长链加上该边的权值"取一个最大值,即: d [ v ] [ 2 ] = m a x ( d [ u ] [ 2 ] + w , d [ u ] [ 0 ] + w ) d[v][2] = max(d[u][2] + w, d[u][0] + w) d[v][2]=max(d[u][2]+w,d[u][0]+w)。
最终每个节点 i i i的答案就是 m a x { d [ i ] [ 0 ] , d [ i ] [ 2 ] } max\{d[i][0], d[i][2]\} max{d[i][0],d[i][2]}。
PS:若最长链不唯一,显然次长链长度等于最长链,对答案无影响。
#include <bits/stdc++.h>
#include <unordered_map>
using namespace std;
#define ENDL "\n"
#define lowbit(x) (x & (-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<double, double> pdd;
const double eps = 1e-8;
const double pi = acos(-1.0);
const int inf = 0x3f3f3f3f;
const double dinf = 1e300;
const ll INF = 1e18;
const int Mod = 1e9 + 7;
const int maxn = 1e4 + 10;
struct node {
int next, to, w;
} edge[maxn << 2];
int head[maxn], tot;
int d[maxn][3];
void addEdge(int u, int v, int w) {
++tot;
edge[tot].to = v;
edge[tot].w = w;
edge[tot].next = head[u];
head[u] = tot;
}
void dfs1(int fa, int u) {
for (int i = head[u], v, w; i; i = edge[i].next) {
v = edge[i].to, w = edge[i].w;
if (v != fa) {
dfs1(u, v);
if (d[v][0] + w >= d[u][0]) {
d[u][1] = d[u][0];
d[u][0] = d[v][0] + w;
} else if (d[v][0] + w > d[u][1]) {
d[u][1] = d[v][0] + w;
}
}
}
}
void dfs2(int fa, int u) {
for (int i = head[u], v, w; i; i = edge[i].next) {
v = edge[i].to, w = edge[i].w;
if (v != fa) {
if (d[v][0] + w == d[u][0]) {
d[v][2] = max(d[u][2] + w, d[u][1] + w);
} else {
d[v][2] = max(d[u][2] + w, d[u][0] + w);
}
dfs2(u, v);
}
}
}
int main() {
//freopen("in.txt", "r", stdin);
//freopen("out.txt","w",stdout);
ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
while (cin >> n) {
memset(head, 0, sizeof head);
memset(d, 0, sizeof d);
tot = 0;
for (int i = 2, u, w; i <= n; i++) {
cin >> u >> w;
addEdge(u, i, w), addEdge(i, u, w);
}
dfs1(0, 1);
dfs2(0, 1);
for (int i = 1; i <= n; i++) {
cout << max(d[i][0], d[i][2]) << ENDL;
}
}
return 0;
}
例题五
题目大意
有一棵树,根节点为 1 1 1号节点,现在需要在每个节点上安装一个设备,安装时间忽略不计但每个节点上设备启动需要时间,管理员会从1号节点开始先向下面的所有节点安装设备,每条边都会花费一分钟的时间且每条边最多经过两次。最后他回到1号节点安装设备,求安装完所有设备且都启动成功所需要的最少时间。
解题思路
一个容易想错的贪心:按每个子树上设备的启动时间由大到小地
d
f
s
dfs
dfs。
h
a
c
k
hack
hack这个做法是如下数据:
由此我们发现,和子树的权值和没有关系,因为子树的深度不一,遍历子树还需要时间。正解比较难想:
设 f [ u ] f[u] f[u]代表节点 u u u下的子树设备全部启动需要的时间, g [ u ] g[u] g[u]表示遍历节点 u u u的儿子节点 v v v之前的所有子树的最短时间,可以把 f [ u ] − g [ u ] f[u] - g[u] f[u]−g[u]看做等待时间,对于一个节点的所有子节点,按等待时间从大到小排序,这样进行 d f s dfs dfs显然是最优的,这样排序的正确性证明从叶子节点然后推及到正常节点即可。
那么
f
[
u
]
=
m
a
x
{
w
u
,
f
[
v
]
+
g
[
u
]
+
1
}
f[u] = max\{w_u, f[v]+g[u]+1\}
f[u]=max{wu,f[v]+g[u]+1},这里为什么不是
+
2
+2
+2见如下图片(参考洛谷题解):
而且此题是先深度遍历,然后在回溯前,排序然后状态转移。因为根节点比较特殊,因此答案是
m
a
x
{
w
1
+
g
[
1
]
,
f
[
1
]
}
max\{w_1+g[1], f[1]\}
max{w1+g[1],f[1]}。
#include <bits/stdc++.h>
#include <unordered_map>
using namespace std;
#define ENDL "\n"
#define lowbit(x) (x & (-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<double, double> pdd;
const double eps = 1e-8;
const double pi = acos(-1.0);
const int inf = 0x3f3f3f3f;
const double dinf = 1e300;
const ll INF = 1e18;
const int Mod = 1e9 + 7;
const int maxn = 5e5 + 10;
int a[maxn], f[maxn], g[maxn];
vector<int> G[maxn];
bool cmp(int &p, int &q) {
return f[p] - g[p] > f[q] - g[q];
}
void dfs(int fa, int u) {
f[u] = a[u];
for (auto v:G[u]) {
if (v != fa) dfs(u, v);
}
sort(G[u].begin(), G[u].end(), cmp);
for (auto v:G[u]) {
if (v != fa) {
f[u] = max(f[u], f[v] + g[u] + 1);
g[u] += g[v] + 2;
}
}
}
int main() {
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
G[u].push_back(v), G[v].push_back(u);
}
dfs(0, 1);
int ans = max(a[1] + g[1], f[1]);
cout << ans << endl;
return 0;
}
例题六
2020 CCPC 秦皇岛 K Kingdom’s Power
题目大意
给出一棵根节点为 1 1 1的树,其中树根上面有无穷的士兵,每分钟我们能调取一队士兵走一条边,如果该节点没有被占领那么会占领该节点,问占领所有节点需要的最少时间。
解题思路
首先要能简单证明如下几个关键想法:
-
对于某节点 u u u,它要么被其兄弟子树上的士兵折返回来占领,要么就从根节点下来一队新的士兵占领。
-
一队士兵到达节点 u u u的子节点 v v v后一定会继续向下走,要么停在叶子节点上,要么等到 u u u的整个子树遍历完成后可能前往 u u u的兄弟节点。
-
对于每个节点来说,从根节点下来的士兵的路径长度是唯一的,但是其兄弟子树的士兵往返的路径是变化的,如何最小化折返的路径长度?考虑一个节点占领一棵子树时,最优的做法是除了最长链只走一次外,其他链都会走两次。这启发我们对每个节点按最长链的长度从小到大排序,这样再进行DP的结果是最优的。
-
如果我们要用到第 i i i次操作的那队士兵,那么一定只会在 i + 1 i+1 i+1次操作时可能用到它,因为如果其兄弟节点都不会用到,那么在 i + x ( x > 1 ) i+x(x>1) i+x(x>1)次操作回溯到了距离根更近的父亲节点时更不会用到。
有了以上的思路后,显然我们只需要记录上一次走到叶子节点的那队士兵,然后比较上队士兵到当前节点的距离和当前节点的深度来确定是否新派士兵。一开始一直纠结如何从上次的叶子结点转移到当前的叶子节点,写了几次发现很难维护(对于蒟蒻来说)。然后突然发现每次不管如何,都只能是一队士兵在走,我们可以不单单考虑叶子节点,而是对每个节点来看,求出要用到的士兵走到该节点需要的距离,不断更新维护当前的士兵的位置,这样本题就迎刃而解了!
附上测试的图,答案为
13
13
13:
//
// Created by Happig on 2021/3/3.
//
#include <bits/stdc++.h>
#include <unordered_map>
using namespace std;
#define ENDL "\n"
#define lowbit(x) (x & (-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<double, double> pdd;
const double eps = 1e-8;
const double pi = acos(-1.0);
const int inf = 0x3f3f3f3f;
const double dinf = 1e300;
const ll INF = 1e18;
const int Mod = 1e9 + 7;
const int maxn = 1e6 + 10;
ll ans = 0;
vector<int> G[maxn];
int maxd[maxn], d[maxn];
int pre;
bool cmp(int &p, int &q) {
return maxd[p] < maxd[q];
}
void dfs1(int u, int deep) {
d[u] = maxd[u] = deep;
for (auto v:G[u]) {
dfs1(v, deep + 1);
maxd[u] = max(maxd[u], maxd[v]);
}
sort(G[u].begin(), G[u].end(), cmp);
}
void dfs2(int u, int fa) {
if (u != 1) {
if (!pre) ans += d[u];
else {
if (d[pre] - d[fa] + 1 <= d[u]) ans += d[pre] - d[fa] + 1;
else ans += d[u];
}
pre = u;
}
for (auto v:G[u]) {
dfs2(v, u);
}
}
int main() {
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
//ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t, n, kase = 0;
scanf("%d", &t);
while (t--) {
scanf("%d", &n);
for (int i = 1; i <= n; i++) G[i].clear();
for (int i = 2, u; i <= n; i++) {
scanf("%d", &u);
G[u].push_back(i);
}
dfs1(1, 0);
ans = pre = 0;
dfs2(1, 0);
printf("Case #%d: %lld\n", ++kase, ans);
}
return 0;
}
树上背包
树上背包就是在树上进行背包
题目大意
给出一个节点带权的森林,可以选择 m m m个节点,但是在选择一个节点时它的父节点必须已经被选,求选择的最大权值和。
解题思路
首先把森林的所有根节点和一个虚拟节点0连接起来,节点0必选且可选择
m
+
1
m+1
m+1个节点。对于一个节点下的所有子节点对应的子树,每个子树的选择也是从父到子的,可以将所有子树看做多组物品,每组物品有若干件物品,那么每个节点实际上就是一个分组DP的模型,分组背包是枚举每组的
s
s
s物品进行选择:
对于本题来说,要枚举的是这棵子树下选择多少个节点然后进行状态转移,因此是需要自底向上进行状态转移的。设
d
u
du
du代表节点
u
u
u下子树的节点个数,设
f
(
u
,
i
,
j
)
f(u,i,j)
f(u,i,j)表示以
u
u
u号点为根节点的子树中,已经遍历了前
i
i
i棵子树,选了
j
j
j门课程的最大学分,那么有状态转移方程:
f ( u , i , j ) = m a x j : m → 1 , k : 1 → j { f ( u , i − 1 , j − k ) + f ( v , d v , k ) } f(u,i,j) = max_{j: m \rightarrow 1, k: 1 \rightarrow j}\{f(u,i-1,j-k) + f(v,dv,k)\} f(u,i,j)=maxj:m→1,k:1→j{f(u,i−1,j−k)+f(v,dv,k)}
显然可以滚动掉 i i i的那一维,即:
f ( u , j ) = m a x j : m → 1 , k : 1 → j { f ( u , j − k ) + f ( v , k ) } f(u,j) = max_{j: m \rightarrow 1, k: 1 \rightarrow j}\{f(u,j-k) + f(v,k)\} f(u,j)=maxj:m→1,k:1→j{f(u,j−k)+f(v,k)}
初始化为 f ( u , 1 ) = w u f(u,1) = w_u f(u,1)=wu,因此上面的枚举是以 1 1 1为边界而不是0,最终的答案是 f ( 0 , m ) f(0,m) f(0,m)
#include <bits/stdc++.h>
#include <unordered_map>
using namespace std;
#define ENDL "\n"
#define lowbit(x) (x & (-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<double, double> pdd;
const double eps = 1e-8;
const double pi = acos(-1.0);
const int inf = 0x3f3f3f3f;
const double dinf = 1e300;
const ll INF = 1e18;
const int Mod = 1e9 + 7;
const int maxn = 305;
int n, m, f[305][305], w[305];
vector<int> G[maxn];
int dfs(int u, int fa) {
int du = 1;
f[u][1] = w[u];
for (auto v:G[u]) {
if (v != fa) {
int dv = dfs(v, u);
for (int j = m; j >= 1; j--) {
for (int k = 1; k < j && k <= dv; k++) {
f[u][j] = max(f[u][j], f[u][j - k] + f[v][k]);
}
}
du += dv;
}
}
return du;
}
int main() {
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1, u; i <= n; i++) {
cin >> u >> w[i];
G[u].push_back(i), G[i].push_back(u);
}
m++;
dfs(0, -1);
cout << f[0][m] << ENDL;
return 0;
}
换根DP
换根DP的特征就是没有指定根节点,但是根节点的变换会影响深度和、权值和等因素,通常都是需要两次DFS,第一次预处理,第二次进行DP
例题一
题目大意
给定一个 n n n点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。
解题思路
设 f [ u ] f[u] f[u]表示以 u u u为根节点时得到的深度之和,当根从 u u u换到其子节点 v v v时,会发生:
- 所有在 v v v的子树上的节点深度都减少了1,总深度和就减少了 d [ v ] d[v] d[v]。
- 所有不在 v v v的子树上的节点深度都增加了1,总深度和就增加了 n − d [ v ] n - d[v] n−d[v]
那么状态转移方程就是 f [ v ] = f [ u ] + n − 2 ∗ d [ v ] f[v] = f[u] + n - 2*d[v] f[v]=f[u]+n−2∗d[v]。而我们首先需要找1号节点作为根节点并求出答案,然后进行第二次 d f s dfs dfs即换根DP。
#include <bits/stdc++.h>
#include <unordered_map>
using namespace std;
#define ENDL "\n"
#define lowbit(x) (x & (-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<double, double> pdd;
const double eps = 1e-8;
const double pi = acos(-1.0);
const int inf = 0x3f3f3f3f;
const double dinf = 1e300;
const ll INF = 1e18;
const int Mod = 1e9 + 7;
const int maxn = 1e6 + 10;
int n, d[maxn];
ll ans, f[maxn];
vector<int> G[maxn];
void dfs(int u, int fa, int deep) {
d[u] = 1;
f[1] += deep;
for (auto v:G[u]) {
if (v != fa) {
dfs(v, u, deep + 1);
d[u] += d[v];
}
}
}
void dfs2(int u, int fa) {
ans = max(ans, f[u]);
for (auto v:G[u]) {
if (v != fa) {
f[v] = f[u] + n - 2 * d[v];
dfs2(v, u);
}
}
}
int main() {
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
G[u].push_back(v), G[v].push_back(u);
}
dfs(1, 0, 1);
//cout << f[1] << ENDL;
dfs2(1, 0);
for (int i = 1; i <= n; i++) {
if (ans == f[i]) {
ans = i;
break;
}
}
cout << ans << ENDL;
return 0;
}
例题二
POJ - 3585 Accumulation Degree
题目大意
给出一棵树,树上每条边都有一个能经过的最大流量。根节点的水源可以看做无穷大,求出以哪个点为根时,从该点出发的水源流到叶子节点的流量总和最大。以下图为例, A ( 1 ) = 24 , A ( 2 ) = 11 , A ( 3 ) = 5 , A ( 4 ) = 26 , A ( 5 ) = 10 A(1) = 24, A(2) = 11,A(3) = 5,A(4) = 26, A(5) = 10 A(1)=24,A(2)=11,A(3)=5,A(4)=26,A(5)=10,故答案为 5 5 5。
解题思路
设
d
[
u
]
d[u]
d[u]为节点
u
u
u的子树的流量之和,从节点
u
u
u出发能流到其儿子节点
v
v
v的子树下的叶子节点的流量为
m
i
n
{
w
(
u
,
v
)
,
d
[
v
]
}
min\{w_{(u,v)}, d[v]\}
min{w(u,v),d[v]},那么第一次预处理可以求出1号节点的流量和以及以
1
1
1为根节点下每个节点子树的流量和
d
[
i
]
d[i]
d[i],然后考虑换根,假设已经求出了节点
u
u
u为根的流量,那么当根变为其子节点
v
v
v时:将节点
u
u
u的总流量从
w
(
u
,
v
)
w_{(u,v)}
w(u,v)分成两部分,下面的一部分为
d
[
v
]
d[v]
d[v],上面的一部分为没有流向该边的流量
f
[
u
]
−
m
i
n
(
d
[
v
]
,
w
(
u
,
v
)
)
f[u] - min(d[v],w_{(u,v)})
f[u]−min(d[v],w(u,v))和
w
(
u
,
v
)
w_{(u,v)}
w(u,v)取最小值,然后对这两部分求和,即
f
[
v
]
=
m
i
n
{
f
[
u
]
−
m
i
n
(
d
[
v
]
,
w
(
u
,
v
)
)
,
w
(
u
,
v
)
}
+
d
[
v
]
f[v] = min\{f[u] - min(d[v],w_{(u,v)}), w_{(u,v)}\}+d[v]
f[v]=min{f[u]−min(d[v],w(u,v)),w(u,v)}+d[v]。
但是此题有一个易错点,当考虑树为一条链时,显然根节点只有一个子节点,那么此时
f
[
v
]
=
d
[
v
]
+
w
f[v] = d[v] + w
f[v]=d[v]+w。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
const int maxn = 2e5 + 10;
struct node {
int to, next, w;
} edge[maxn * 2];
int head[maxn], tot;
int ans, f[maxn], d[maxn], cnt[maxn];
void addEdge(int u, int v, int w) {
++tot;
edge[tot] = {v, head[u], w};
head[u] = tot;
++tot;
edge[tot] = {u, head[v], w};
head[v] = tot;
}
void init() {
tot = ans = 0;
memset(cnt, 0, sizeof cnt);
memset(d, 0, sizeof d);
memset(f, 0, sizeof f);
memset(head, 0, sizeof head);
}
void dfs1(int u, int fa) {
for (int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to, w = edge[i].w;
if (v != fa) {
dfs1(v, u);
d[u] += cnt[v] == 1 ? w : min(d[v], w);
}
}
//cout << u << " " << d[u] << endl;
}
void dfs2(int u, int fa) {
ans = max(ans, f[u]);
for (int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to, w = edge[i].w;
if (v != fa) {
if (cnt[u] == 1) f[v] = d[v] + w;
else f[v] = min(f[u] - min(d[v], w), w) + d[v];
dfs2(v, u);
}
}
}
int main() {
//freopen("in.txt","r",stdin);
//freopen("out.txt","w",stdout);
//ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t, n;
scanf("%d", &t);
while (t--) {
scanf("%d", &n);
init();
for (int i = 1, u, v, w; i < n; i++) {
scanf("%d%d%d", &u, &v, &w);
addEdge(u, v, w);
cnt[u]++, cnt[v]++;
}
dfs1(1, 0);
f[1] = d[1];
dfs2(1, 0);
printf("%d\n", ans);
}
return 0;
}