【算法笔记】树上差分+树形DP
「妖怪が鍛えたこの楼観剣に 斬れぬものなど、あんまり無い!」
树上差分
树上差分分两种,点差分和边差分。
不管是哪一种,都是基于最基础的差分的。
只不过我们的操作对象从序列变成了树链。
点差分
假设说给你一棵树 \(T\)。
且 \(\forall u \in T\) 都有一个权值 \(val[u]\)
现在有 \(q\) 次操作,每一次操作 \(\operatorname{change}(u,v,d)\) 需要你修改 \(u \to v\) 的路径上的所有点的权值。
即令 \(\forall val[u]+d,(u\in \delta(u,v))\) 。
怎么做?
正常想法是 \(\texttt{dfs}\) 暴力修改。
显而易见,这做法受不住树链很长的多次询问 。
还有一种做法是先树剖然后套一个线段树。
但这里完全不在讨论范围内啊。
所以点差分就出现在了我们的眼前。
我们将一条树链拆成 \(A:(u,lca(u,v)),B:(v,lca(u,v))\) 这两部分。
那么设 \(c[u]\) 表示 \([u]\) 这个节点的增量(差分数组)。
我们有:
我们对于 \(A,B\) 的端点分别差分一下。
所以 \(c[u]=d,c[v]=d,c[lca]=-2\times d\)。
但是这个 \(\texttt{LCA}\) 本身就在树链 \(\delta(u,v)\) 上。
所以它自己也要加 \(d\) ,我们就给他加回去。
那么 \(c[u]=d,c[v]=d,c[lca]=-d\)。
但是你想想,我们差分之后肯定要还原,还原的时候肯定要 \(\texttt{dfs}\) ,从子树收集信息。
所以父节点的值是会被子树影响的。
所以我们还需要给 \(father(lca(u,v))-d\)。
这才是真正的点差分。
void dfs(int u,int fa){
for(register int i=e[u].head;i;i=e[i].Next){
int v=e[i].ver;
if(v==fa) continue;
dfs(v,u),c[u]+=c[v];//收集子树信息
}
a[u]+=c[u];//还原
return;
} //在所有修改结束之后还原序列。
注意,差分的所有修改一定在查询之前,要不然你需要还原,修改多次。
还不如树剖+线段树。
边差分
点差分明白了,边差分也就十分类似。
之前做 P3627 [APIO2009]抢掠计划的时候。
用了一个思想叫“点权化边权”。
在这里我们反过来,“边权化点权”。
考虑任意的一条树边 \((u,v)\) ,一定满足它连接的是父亲和儿子(废话)。
那么这个边的“指向”就有唯一性。
所以我们把每一条树边的权值压到它指向的“儿子节点”。
特别的,因为树根没有父亲,所以它的权值为 \(0\)
这是候就有人想,既然边权化点权了,那我们能不能直接跑点差分?
不行。
(\(u\) 打错成 \(x\)了)
你仔细看看,lca的权值可是 \((lca,root)\) 的权值哦。
我哪里需要更新这条边?
所以相当于是在跑一个去掉 \(\texttt{LCA}\) 的点差分。
于是我们就不需要考虑 \(\texttt{LCA}\) 的权值和它对 \(father({\texttt{LCA}})\) 的影响。
直接 \(c[u]+d,c[v]+d,c[lca]-2\times d\) 即可。
然后也是利用子树上传信息还原序列。
树形DP
正常的线性DP都是在一个序列上进行决策和转移的。
那如果转到树上呢?
当然可以使用拓扑排序进行DP,不过这里我们讨论利用 \(\texttt{dfs}\) 实现的树形DP。
我们一般从叶子节点上推到根节点,也就是在递归之后利用子树信息更新父节点。
不过调用 \(\texttt{dfs}\) 的时候还是写 dfs(root)
所以说 \(f\) 的第一维一般是当前访问的节点(方便决策和讨论转移)。
那么这里以最经典的“没有上司的舞会”为例讨论一下树形DP。
description
solution
题中极度明确的说到了:从属关系构成一棵树。
而且父亲节点是否参加会影响儿子节点是否参加。
那么我们就从这两个“树形”限制入手。
先设 \(f[u]\) 表示节点 \(u\) 的最大权值。
诶,不对啊,这状态有问题吧,节点 \(u\) 的最大权值是什么,怎么来的?
很明显状态并没有设计完整,我们只考虑了“从属关系”这一条件,也就是考虑当前节点的位置,便于判断父亲节点。
所以利用第二个条件再加一维。
设 \(f[u][0/1]\) 表示 \(u\) 参加或者不参加舞会的最大快乐值。
嗯?还是不对,你这状态有个鬼用啊,参加不就是 \(r[u]\),不参加不就是 \(0\) 吗?
你怎么用这个节点的决策更新它的父亲(儿子)的状态?
你怎么转移?
所以这个状态设计的还不够完善,为了想办法“决策”,
我们设 \(f[u][0/1]\) 表示以 \(u\) 为根的子树, \(u\) 参加或者不参加舞会取得的最大权值。
那么根据状态设计,我们分 \(f[u][0]\) 和 \(f[u][1]\) 两种情况来讨论。
如果 \(u\) 不参加舞会,那么它的儿子们都可以参加舞会。
但是我们注意到 \(r_i\) 可能是负数,所以要在儿子参加和不参加里面取个最大值然后再求和。
\(f[u][0]=\sum^{v=1}_{v\le \text{sonsize(u)}} \max(f[v][1],f[v][0])\)
然后如果 \(u\) 参加了舞会,那么它的儿子节点就都不会参加,但是它自己要参加。
所以 \(f[u][1]=\sum^{v=1}_{v\le \text{sonsize(u)}}f[v][0]+r_u\)
然后我们DP完了之后就在 \(f[root][0]\) 和 \(f[root][1]\) 之间取个最大值即可。
Code:
#include
using namespace std;
const int si=6e3+10;
struct Tree{
int ver,head,Next;
}e[si*2];
int root=0,cnt=0;
void add(int u,int v){
e[++cnt].ver=v,e[cnt].Next=e[u].head;
e[u].head=cnt;
}
int r[si];
int f[si][2];
bool nrt[si];
void dp(int u,int fa){
f[u][0]=0;
f[u][1]=r[u];//注意这里不要放到里面,会多加
for(register int i=e[u].head;i;i=e[i].Next){
int v=e[i].ver;
if(v==fa) continue;
dp(v,u);
f[u][0]+=max(f[v][1],f[v][0]);
f[u][1]+=f[v][0];
}
}
int n;
int main(){
scanf("%d",&n);
for(register int i=1;i<=n;++i){
scanf("%d",&r[i]);
}
for(register int i=1;i
这道题告诉我们,
树形DP的常见“状态”可以是子树的权值(信息),
可以是以一个节点为根进行决策的权值。
树形依赖背包
这种问题一般都是将背包的转移转化到树上。
而且父亲儿子的关系会影响决策。
有两种通用方法,一种是多叉树转二叉树,一种是直接用多叉树。
这里只介绍后者。
像 P1272 重建道路,
P2014 [CTSC1997]选课
和 P1270 “访问”美术馆 都是十分经典的树形背包。
这里以最板子的选课一题讲解。
不难发现这里实际上就是把01背包转到了树上。
那么我们设 \(f[u][i]\) 表示考虑以 \(u\) 为根的子树当中考虑选修 \(i\) 门课所能得到的最大值。
对于每一个儿子你仍旧是选或者不选,把选修多少门课看做背包的空间,你会发现,为了得到子树的信息,我们要用的就是儿子的状态而不是权值。
因为对于儿子节点你也有选多少的限制,所以需要枚举。
方程就是: \(f[u][i]=max(f[u][i],f[u][i-j]+f[v][j]),(i\in [1,m],j\in[0,i))\)
\(v\) 是 \(u\) 的儿子,\(j\) 用于枚举儿子 \(v\) 的情况,\(m\) 是题里给的限制(空间)。
就和01背包类似,我们需要倒序循环 \(i\)。
这部分的代码如下:
void dp(int u,int fa){
f[u][1]=val[u];
for(register int i=e[u].head;i;i=e[i].Next){
int v=e[i].ver;
if(v==fa) continue;
dfs(v,u);
for(register int j=m;j>=1;--j){
for(register int k=0;k
然后你发现其实我们也可以直接把所有儿子提出来,做类似线性01背包的处理。
也就是说可以把儿子的权值直接合并到儿子的状态当中然后01背包。
这种代码如下:
Code
void dp(int u,int fa){
f[u][1]=val[u];
for(register int i=e[u].head;i;i=e[i].Next){
int v=e[i].ver;
if(v==fa) continue;
for(register int j=m;j>=0;--j){
f[v][j]=f[u][j]+val[v];
}
dfs(v,u);
for(register int j=m;j>=0;--j){
f[u][j]=max(f[u][j],f[v][j-1]);
}
}
}
很类似分组背包(
树的直径
-
定义:
给定一颗树 \(T=\{V,E\}\),树上的每一条边都会有一个权值。
如果说有一条路径 \(\delta(u,v)\) 满足 \(\text{dis}(u,v)\) (\(u\to v\) 的距离)是所有路径当中最大的。
那么 \(\delta(u,v)\) 就是树的最长链。
\(\text{dis}(u,v)\) 就是树的直径。
-
求法
(为了方便我们一般假设它是以 \(1\) 为根的有根树)
众所周知树的链可能会长成下面这两种样子(蓝色链)
不难发现,对于一个非叶子节点,经过它的链可以向它的子树延伸。
所以我们设 \(d[u]\) 表示从 \(u\) 出发,走向 \(u\) 的所有子树,所能到达最远节点和 \(u\) 距离。
设 \(u\) 一共有 \(k\) 个儿子节点。
显然,\(d[u]=\max\{d[son_{u,i}]\ + w(u,son_{u,i})\}(i \in [1,k])\)
这里用树形DP的思想,使用 \(\texttt{dfs}\) 从子树向上收集信息即可。
设 \(f[u]\) 表示经过 \(u\) 的最长链的长度。
那么直径 \(D\) 就是 \(\max(f[i]),(i\in[1,|V|])\)。
考虑怎么求 \(f[u]\)。
因为 \(f[u]\) 可能有很多子树,所以我们考虑从所有子树的信息那里转移过来。
画个图感性理解:
因为经过 \(u\) 的最长链(可以理解为以它为根)肯定是由两部分(两个子树的信息)合成的
那么根据 \(f\) 的定义很容易有:
\(f[u]=\max\{ d[son_{u,a}] + d[son_{u,b}] + w(son_{u,a})+w(son_{u,b})\}\)
且 \(a,b \in [1,size(son(u))]\)
但是这样子的话我们就必须两两枚举 \(a,b\)。
复杂度稍微有点大,咋办?
想想我们更新 \(d[u]\) 的过程,在取 \(\max\) 的时候它就会保留一部分信息了。
这部分信息就是 \(\max \{d[v]+w(u,v)\},(v\in son(u),v\,has \,been \, visited)\) (这个 \(v\) 不一定循环到了所有的儿子)
所以我们可以直接用 \(u\) 的下一个循环到的儿子 \(x\) 的信息来更新 \(f[u]\)
所以 \(f[u]=\max\{d[u]+d[x]+w(u,x)\}\)。
那么就能够在 \(\text{O}(n)\) 的复杂度下求解。
Code:
void dp(int u,int fa){
for(register int i=e[u].head;i;i=e[i].Next){
int v=e[i].ver;
if(v==fa) continue;
dp(v,u),f[u]=max(f[u],d[u]+d[v]+e[i].w);
d[u]=max(d[u],d[v]+e[i].w);
}
}
//也可以不用 f 直接写 res
树的重心
-
定义:
树若以某点为根,使得该树最大子树的结点数最小,那么这个点则为该树的重心。
一棵树可能有多个重心。
-
性质:
1、树上所有的点到树的重心的距离之和是最短的,如果有多个重心,那么总距离相等。
2、插入或删除一个点,树的重心的位置最多移动一个单位。
3、若添加一条边连接2棵树,那么新树的重心一定在原来两棵树的重心的路径上。
这里还是从定义先入手。
设 \(f[u]\) 表示以 \(u\) 为根时最大子树的大小。
怎么求?
不难发现状态里有“最大子树”四个字。
所以我们先 \(\texttt{dfs}\) 一遍,求出以每一个节点 \(u\) 为根的子树大小 \(siz[u]\)
那么考虑怎么转移。
你想,假设我们以一个节点 \(u\) 为根,那么这棵树一定可以分成两部分。
一部分是 \(u\) 本身的子树(和它自己)。
另一部分就是整棵树抠掉第一部分。
如图:
所以我们要分别用 \(u\) 的子树们的信息和上面那一部分的信息来更新 \(f[u]\)。
那么首先 \(f[u]=max\{size[v]\},(v\in Son(u))\)。
然后 \(f[u]=max\{n-size[u]\}\)。
最后在 \(f[u]\) 里面找最小值即可。
Code:
void dfs(int u,int fa){
siz[u]=1;
for(register int i=e[u].head;i;i=e[i].Next){
int v=e[i].ver;
if(v==fa) continue;
dfs(v,u);
siz[u]+=siz[v];
}
}
void dp(int u,int fa){
for(register int i=e[u].head;i;i=e[i].Next){
int v=e[i].ver;
if(v==fa) continue;
dp(v,u);
f[u]=max(f[u],siz[v]);
}
f[u]=max(f[u],n-siz[u]);
}
如果你要把这两个合到一起,再用一个 ans
替代 f[u]
也是可以的。
换根DP
none