树形|换根 DP总结
树形dp
树形 DP,即在树上进行的 DP。由于树固有的递归性质,树形 DP 一般都是递归进行的。
基础
树形 DP 的一般过程。
没有上司的舞会
某大学有 n n n 个职员,编号为 1 − N 1 - N 1−N。他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 a i a_i ai,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
思路
我们设
f
(
i
,
0
/
1
)
f(i,0/1)
f(i,0/1) 代表以
i
i
i 为根的子树的最优解(第二维的值
0
0
0 代表
i
i
i 不参加舞会的情况,
1
1
1 代表
i
i
i 参加舞会的情况)。
对于每个状态,都存在两种决策(其中下面的
x
x
x 都是
i
i
i 的儿子):
- 上司参加舞会,下属不参加
- 上司不参加舞会,下属可参加或不参加
根据两种状态写出dp递推式
F
(
i
,
1
)
=
Σ
F
(
x
,
0
)
+
a
i
F(i,1)=\Sigma F(x,0)+a_i
F(i,1)=ΣF(x,0)+ai
F
(
i
,
0
)
=
Σ
m
a
x
(
F
(
x
,
1
)
,
F
(
x
,
0
)
)
F(i,0)=\Sigma max(F(x,1),F(x,0))
F(i,0)=Σmax(F(x,1),F(x,0))
代码
略
树上背包
树上的背包问题,背包与树形dp结合
选课
现在有
n
n
n 门课程,第
i
i
i 门课程的学分为
a
i
a_i
ai,每门课程有零门或一门先修课,有先修课的课程需要先学完其先修课,才能学习该课程。
一位学生要学习
m
m
m 门课程,求其能获得的最多学分数。
每门课最多只有一门先选课的特点,与有根树中一个点最多只有一个父亲节点的特性类似。
因此可以想到根据这一性质建树,从而所有的课程组成了一个森林结构。
新增一门0学分的课程,作为无前提课程的前提课程,整个森林变为以0为根的树。
设 f ( u , i , j ) f(u,i,j) f(u,i,j) 表示以 u u u 号点为根的子树中,已经遍历了 u u u 号点的前 i i i 棵子树,选了 j j j 门课程的最大学分。
转移的过程结合了树形 DP 和 背包 DP 的特点,我们枚举
u
u
u 点的每个子结点
v
v
v,同时枚举以
v
v
v 为根的子树选了几门课程,将子树的结果合并到
u
u
u 上。
以
x
x
x 为根的子树大小为
s
i
z
x
siz_x
sizx 可以记因此有转移方程:
F
(
u
,
i
,
j
)
=
m
a
x
(
F
(
u
,
i
−
1
,
j
−
k
)
+
F
(
v
,
S
v
,
k
)
)
F(u,i,j)=max(F(u,i-1,j-k)+F(v,S_v,k))
F(u,i,j)=max(F(u,i−1,j−k)+F(v,Sv,k))
第二维可以滚动数组优化掉,倒序枚举j。
复杂度为O(nm)
代码
#include <algorithm>
#include <cstdio>
#include <vector>
using namespace std;
int f[305][305], s[305], n, m;
vector<int> e[305];
int dfs(int u) {
int p = 1;
f[u][1] = s[u];
for (auto v : e[u]) {
int siz = dfs(v);
for (int i = min(p, m + 1); i; i--)
for (int j = 1; j <= siz && i + j <= m + 1; j++)
f[u][i + j] = max(f[u][i + j], f[u][i] + f[v][j]);
p += siz;
}
return p;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
int k;
scanf("%d%d", &k, &s[i]);
e[k].push_back(i);
}
dfs(0);
printf("%d", f[0][m + 1]);
return 0;
}
换根dp
树形 DP 中的换根 DP 问题又被称为二次扫描,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。
STA-Station
思路
不妨令 u 为当前结点,v 为当前结点的子结点。首先需要用
s
i
s_i
si 来表示以 i 为根的子树中的结点个数,并且有
s
u
=
1
+
Σ
s
v
s_u=1+\Sigma s_v
su=1+Σsv。显然需要一次 DFS 来计算所有的
S
i
S_i
Si,这次的 DFS 就是预处理,我们得到了以某个结点为根时其子树中的结点总数。
令
f
u
f_u
fu 为以
u
u
u 为根时,所有结点的深度之和。
f
v
⬅
f
u
f_v⬅f_u
fv⬅fu 可以体现换根,即以
u
u
u 为根转移到以
v
v
v 为根。显然在换根的转移过程中,以
v
v
v 为根或以
u
u
u 为根会导致其子树中的结点的深度产生改变。具体表现为:
- 所有在 v v v 的子树上的结点深度都减少了一,那么总深度和就减少了 s v s_v sv;
- 所有不在 v v v 的子树上的结点深度都增加了一,那么总深度和就增加了 n − s v n-s_v n−sv;
由此地递推方程:
f
v
=
f
u
+
n
−
s
v
=
f
u
+
n
−
2
×
s
v
f_v=f_u+n-s_v=f_u+n-2 × s_v
fv=fu+n−sv=fu+n−2×sv
在第二次 DFS 遍历整棵树并状态转移 f v = f u + n − 2 ∗ s v f_v=f_u+n-2*s_v fv=fu+n−2∗sv,那么就能求出以每个结点为根时的深度和了。
代码
#include <bits/stdc++.h>
using namespace std;
int head[1000010 << 1], tot;
long long n, size[1000010], dep[1000010];
long long f[1000010];
struct node {
int to, next;
} e[1000010 << 1];
void add(int u, int v) { // 建图
e[++tot] = node{v, head[u]};
head[u] = tot;
}
void dfs(int u, int fa) { // 预处理dfs
size[u] = 1;
dep[u] = dep[fa] + 1;
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (v != fa) {
dfs(v, u);
size[u] += size[v];
}
}
}
void get_ans(int u, int fa) { // 第二次dfs换根dp
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (v != fa) {
f[v] = f[u] - size[v] * 2 + n;
get_ans(v, u);
}
}
}
int main() {
scanf("%lld", &n);
int u, v;
for (int i = 1; i <= n - 1; i++) {
scanf("%d %d", &u, &v);
add(u, v);
add(v, u);
}
dfs(1, 1);
for (int i = 1; i <= n; i++) f[1] += dep[i];
get_ans(1, 1);
long long int ans = -1;
int id;
for (int i = 1; i <= n; i++) {
if (f[i] > ans) {
ans = f[i];
id = i;
}
}
printf("%d\n", id);
}
树的直径与重心
树的直径
给定一棵树,
树中每条边都有一个权值,
树中两点之间的距离定义为连接两点的路径边权之和。
树中最远的两个节点之间的距离被称为树的直径,
连接这两个点的路径被称为树的最长链。
————————————————————————————————
树的直径求法:双dfs或树形dp
双dfs
考虑贪心策略,
对于树上的一个随机的点
W
W
W ,
我们找到离他最远的
P
P
P ,
找到离
P
P
P 距离最远的点
Q
Q
Q ,
P
Q
PQ
PQ 的距离即为我们要求的直径。
如图,假设五号点为
W
W
W ,
找到离他距离最远的点
4
(
P
)
4(P)
4(P),
再找到距离
P
P
P 最远的点
6
(
Q
)
6(Q)
6(Q) ,
P
Q
PQ
PQ 的距离即为直径。
代码
#include<bits/stdc++.h>
#define N 200005
using namespace std;
int n,m;
struct edge{
int to,nxt,w;
}e[N];
int tot;
int ans,pos;
int head[N],dis[N];
void add(int u,int v,int w){
e[++tot]={v,head[u],w},head[u]=tot;
}
void dfs(int me,int dad){
if(ans<dis[me])ans=dis[me],pos=me;
for(int i=head[me];i;i=e[i].nxt){
int son=e[i].to;
if(son==dad)continue;
dis[son]=dis[son]+e[i].w;
dfs(son,me);
}
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
add(u,v,1),add(v,u,1);
}
dis[1]=0;
dfs(1,0);//第一次dfs,随便找一个点。
ans=0;dis[pos]=0,dfs(pos,0);//第二次dfs
cout<<ans<<endl;
}
树形dp
考虑
f
[
i
]
f[i]
f[i] 以
i
i
i 为根的子树中和从
i
i
i 出发的最长长度
考虑以
u
u
u 为根的子树
f
[
u
]
=
m
a
x
(
f
[
u
]
,
f
[
v
]
+
e
[
i
]
.
w
)
f[u] = max(f[u],f[v]+e[i].w)
f[u]=max(f[u],f[v]+e[i].w)
最长长度就是两条链的和。
或者说,我们原来找到了一条 f [ u ] f[u] f[u] 的链
现在我们新找到了一条由 v v v 节点继承的链
这两条链长度之和的最大值即为
a
n
s
ans
ans
所以
a
n
s
=
m
a
x
(
a
n
s
,
f
[
u
]
+
f
[
v
]
+
e
[
i
]
.
w
)
ans=max(ans,f[u]+f[v]+e[i].w)
ans=max(ans,f[u]+f[v]+e[i].w)
PS: 要写在 f [ u ] f[u] f[u] 的转移之前
代码
void dfs(int me,int dad)
{
f[me]=0;
for(int i=head[me];~i;i=e[i].nxt)
{
int son=e[i].to;
if(son==dad)continue;
dfs(son,me);
ans=max(ans,f[me]+f[son]+e[i].w);
f[me]=max(f[me],f[son]+e[i].w);
}
}
因为树形 d p dp dp 的做法不需要依赖于 x + y > x ( y ∈ R + ) x+y>x(y \in R^+) x+y>x(y∈R+) 的性质,所以边权可以为负。
树的重心
考虑一个点,以它为根的树中,最大的子树节点数最少,我们把这个点称为树的重心。
例:下图中重心为
1
1
1 和
2
2
2 。
树形dp
求解树的重心的时候,我们通常会采用树形 d p dp dp
我们用 s [ i ] s[i] s[i] 代表以 i i i 为根的子树节点数
f [ i ] f[i] f[i] 代表以 i i i 为根的子树中最大的子树节点个数
显然,
f
[
u
]
=
m
a
x
(
f
[
u
]
,
s
[
v
]
)
f[u]=max(f[u],s[v])
f[u]=max(f[u],s[v])
但是我们求重心的时候,是以
u
u
u 为根。
2
2
2 号节点的父亲变为儿子
所以最后统计
f
[
u
]
f[u]
f[u] 的时候,还要记得统计
n
−
s
[
u
]
n-s[u]
n−s[u] (即以原来父亲为根的子树的节点数)
代码
void dfs(int me,int dad)
{
s[me]=1,f[me]=0;
for(int i=head[me];i;i=e[i].nxt)
{
int son=e[i].to;
if(son==dad)continue;
dfs(son,dad);
s[me]+=s[son];
f[me]=max(f[me],s[son]);
}
f[me]=max(f[me],n-s[me]);
}
性质总结
- 以重心为根,所有的子树的大小都不超过整个树大小的一半。
- 树的重心最多有两个。
- 树的重心到其他节点的距离是最小的。
- 把一个树添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
CSP-S 2019 树的重心
#include<bits/stdc++.h>
using namespace std;
#define rd(x) cin>>x
#define ll long long
#define pb push_back
#define print(x) cout<<x<<endl;
const int N = 3e5 + 7;
int n, rt, s[N], g[N], u, v, z[N];
vector<int> e[N];
ll ans, c1[N], c2[N];
inline void add(ll *c, int x, int k) {
++x;
while (x <= n + 1) c[x] += k, x += x & -x;
}
inline ll ask(ll *c, int x) {
++x;
ll k = 0;
while (x) k += c[x], x -= x & -x;
return k;
}
void dfs1(int x, int f) {
s[x] = 1, g[x] = 0;
bool fg = 1;
for (int i = 0; i < e[x].size(); i++) {
int y = e[x][i];
if (y == f) continue;
dfs1(y, x);
s[x] += s[y];
g[x] = max(g[x], s[y]);
if (s[y] > (n >> 1)) fg = 0;
}
if (n - s[x] > (n >> 1)) fg = 0;
if (fg) rt = x;
}
void dfs2(int x, int f) {
add(c1, s[f], -1);
add(c1, n - s[x], 1);
if (x ^ rt) {
ans += x * ask(c1, n - 2 * g[x]);
ans -= x * ask(c1, n - 2 * s[x] - 1);
ans += x * ask(c2, n - 2 * g[x]);
ans -= x * ask(c2, n - 2 * s[x] - 1);
if (!z[x] && z[f]) z[x] = 1;
ans += rt * (s[x] <= n - 2 * s[z[x] ? v : u]);
}
add(c2, s[x], 1);
for (int i = 0; i < e[x].size(); i++) {
int y = e[x][i];
if (y == f) continue;
dfs2(y, x);
}
add(c1, s[f], 1);
add(c1, n - s[x], -1);
if (x ^ rt) {
ans -= x * ask(c2, n - 2 * g[x]);
ans += x * ask(c2, n - 2 * s[x] - 1);
}
}
inline void solve() {
rd(n);
for (int i = 1; i <= n; i++) e[i].clear();
for (int i = 1, x, y; i < n; i++) rd(x), rd(y), e[x].pb(y), e[y].pb(x);
ans = 0;
dfs1(1, 0);
dfs1(rt, 0);
u = v = 0;
for (int i = 0; i < e[rt].size(); i++) {
int x = e[rt][i];
if (s[x] > s[v]) v = x;
if (s[v] > s[u]) swap(u, v);
}
for (int i = 1; i <= n + 1; i++) c1[i] = c2[i] = 0;
for (int i = 0; i <= n; i++) add(c1, s[i], 1), z[i] = 0;
z[u] = 1;
dfs2(rt, 0);
print(ans);
}
int main() {
int T;
rd(T);
while (T--) solve();
return 0;
}
主要是统计 x x x 为重心的次数