【算法笔记】树上差分+树形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}\) 暴力修改。

显而易见,这做法受不住树链很长的多次询问 dk

还有一种做法是先树剖然后套一个线段树。

但这里完全不在讨论范围内啊。

所以点差分就出现在了我们的眼前。

我们将一条树链拆成 \(A:(u,lca(u,v)),B:(v,lca(u,v))\) 这两部分。

WsdjyR.png

那么设 \(c[u]\) 表示 \([u]\) 这个节点的增量(差分数组)。

我们有:

Wswrc9.png

我们对于 \(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\)

WsB0y9.png

但是你想想,我们差分之后肯定要还原,还原的时候肯定要 \(\texttt{dfs}\) ,从子树收集信息。

所以父节点的值是会被子树影响的。

所以我们还需要给 \(father(lca(u,v))-d\)

这才是真正的点差分。

WsrSjH.png

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

这是候就有人想,既然边权化点权了,那我们能不能直接跑点差分?

不行。

Ws635t.png

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

Ws7Eex.png

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\) 为根的有根树)

众所周知树的链可能会长成下面这两种样子(蓝色链)

WcpU9s.png

不难发现,对于一个非叶子节点,经过它的链可以向它的子树延伸。

所以我们设 \(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]\) 可能有很多子树,所以我们考虑从所有子树的信息那里转移过来。

画个图感性理解:

WcCIte.png

因为经过 \(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\) 本身的子树(和它自己)。

另一部分就是整棵树抠掉第一部分。

如图:

WgVKSS.png

所以我们要分别用 \(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

相关