一本通提高篇 树形动态规划

9 篇文章 0 订阅
4 篇文章 0 订阅

先写个考试 鸽着~
考试有树 d p dp dp紫题 我回来了
每年联赛基本都有好几道题时树 d p dp dp,这块一定得好好学
U P D : 20201109 UPD:20201109 UPD:20201109期中考砸 回来补博客 q a q qaq qaq 先把例题发上去 后面习题一天补上

一、基本概念

树形动态规划,顾名思义,就是在“树”的数据结构上做动态规划,通过有限次地遍历树,记录相关信息,以求解问题,通常都是建立在图上的。线性的动态规划的顺序有向前向后两种方向,即顺推和逆推;而树形动态规划是建立在树上的,树中的父子关系天然就是个递归(子问题)结构,所以也相应的有两个方向。
①叶 → → 根,即根的子节点传递有用的信息给根,之后由根得出最优解的过程。这种方式 D P DP DP的题目应用比较多。
②根 → → 叶,即需要取所有点作为一次根节点进行求值,此时父节点得到了整棵树的信息,只需要去除这个儿子的 D P DP DP值的影响,然后再转移给这个儿子,这样就能达到根 → → 叶的顺序。
实现方式:树形 D P DP DP是通过记忆化搜索实现的,因此采用的是递归方式。
时间复杂度:树形 D P DP DP的时间复杂度基本上是 O ( n ) O(n) O(n),若有附加维 m m m则是 O ( n m ) O(nm) O(nm)

二、经典问题

1.树的重心

定义 1 : 1: 1:找到一个点,其所有子树中最大的子树结点数最少,那么这个点就是这棵树的重心
定义 2 : 2: 2:以这个点为根,其所有的子树大小都不超过整棵树的一半,那么这个点就是这棵树的重心
S o l u t i o n : Solution: Solution:任选一个结点为根,把无根树变成有根树,然后设 f i f_i fi表示以 i i i为根的子树的结点的个数,则 f [ i ] = ∑ j ∈ s o n [ i ] + 1 f[i]=\sum\limits_{j∈son[i]}+1 f[i]=json[i]+1。结点 i i i的子树中最大的有 m a x j ∈ s o n [ i ] { f [ j ] } max_{j∈son[i]}\{f[j]\} maxjson[i]{f[j]}个结点, i i i的祖宗有 n − f [ i ] n-f[i] nf[i]个结点,就能求出删去结点 i i i后的最大连通块了
实现:只需一次 D F S DFS DFS,在无根树转有根树的同时计算即可。
性质 1 : 1: 1:树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么他们的距离和相等。
性质 2 : 2: 2:把一棵树添加或删除一个子叶,树的重心最多只移动一条边的距离。
性质 3 : 3: 3:把两棵树通过一条边相连得到一颗新的树,那么新的树的重心在原来两个树的重心的路径上。

只求树的重心果题还是挺难找的 看这道 p o j poj poj的题吧

poj1655 Balancing Art

题面
题意:求树的重心,并输出删去重心后的最大子树结点数
我注册了个 p o j poj poj账号哈哈, p o j poj poj好像不支持万能库??
裸题 上代码

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
#define N 20020
#define reg register
inline void read(int &x){
	int s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}
struct node{
	int to,nxt;
}edge[N<<1];
int u,v,n,T,ans,cnt,head[N],siz[N],son[N];
inline void addedge(int u, int v){
	edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
	addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
	siz[u]=1;
	for(reg int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs(v,u);
		siz[u]+=siz[v],son[u]=max(son[u],siz[v]);
	}
	son[u]=max(son[u],n-siz[u]);
	if(son[u]<son[ans])ans=u;
	else if(son[u]==son[ans]&&u<ans)ans=u;
}
int main(){
	read(T);
	while(T--){
		read(n);
		memset(siz,0,sizeof siz);
		memset(son,0,sizeof son);
		memset(head,0,sizeof head);
		cnt=ans=0,son[0]=999999999;
		for(reg int i=1;i<n;i++)read(u),read(v),superadd(u,v);
		dfs(1,0);
		printf("%d %d\n",ans,son[ans]);
	}
}
2.树的直径

这里不得不 d i s s diss diss一下 s b sb sb一本通,直径两个字一笔没提,求法就给一个
定义:一棵树中,任意两个点的最短路径的距离最大值即为树的直径
有两种求法
1. 1. 1.两次 d f s : dfs: dfs:实质是遍历点
2. 2. 2.树形 d p : dp: dp:实质是遍历边

求法 1 : 1: 1:两次 d f s dfs dfs ( ( (或两次 b f s bfs bfs ) ) )
做法:先从任意一点 P P P开始,找到距离他最远的点 Q Q Q;再从 Q Q Q找到距离最远的点 W W W Q W QW QW即为树的直径

证明:①若 P P P是直径上的一点,则 Q Q Q是直径的端点, Q W QW QW就是直径
②若 P P P不在直径上,反证法:
假设 Q W QW QW不是直径, A B AB AB是直径,则 Q Q Q一定不在直径上
( 1 ) (1) (1) A B AB AB P Q PQ PQ有交点时,如图(为了方便把 A B AB AB P Q PQ PQ画成直线)
在这里插入图片描述

我们选的 Q Q Q是距离 P P P最远的点,则 P Q > P O + O A PQ>PO+OA PQ>PO+OA
两边同时减去 P O PO PO O Q > O A OQ>OA OQ>OA
两边同时加上 O B OB OB O Q + O B > A B OQ+OB>AB OQ+OB>AB,与 A B AB AB是直径矛盾
( 2 ) (2) (2) A B AB AB P Q PQ PQ没有交点时,如图
在这里插入图片描述 P Q > N P + N M + N B PQ>NP+NM+NB PQ>NP+NM+NB,两边同时减去 N P NP NP,得 N Q > N M + M B NQ>NM+MB NQ>NM+MB
两边同时加上 A M + M N AM+MN AM+MN,得 A M + M N + N Q > A M + 2 N M + M B > A M + M B = A B AM+MN+NQ>AM+2NM+MB>AM+MB=AB AM+MN+NQ>AM+2NM+MB>AM+MB=AB
A B AB AB为直径矛盾
Q Q Q在直径上,且为直径的端点,所以 Q W QW QW为直径
来个板子题

FZOJ 3398 树的直径

题目描述
树的直径:树上两点之间的最大距离。
给出一个树,让你求树的直径。
输入
一个数n表示节点数,以下(n-1)行每行两个数x,y表示x与y间有边。
输出
一个整数,树的直径。
样例输入
10
2 8
7 2
2 1
1 10
2 3
3 4
4 9
3 5
3 6
样例输出
5
提示
40% n<=2000
100% n<=200000

上代码

#include<bits/stdc++.h>
using namespace std;
#define N 200020
#define reg register
inline void read(int &x){
	int s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}
int n,u,v,p,q,cnt,dep[N],head[N];
struct node{
	int to,nxt;
}edge[N<<1];
inline void addedge(int u, int v){
	edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
	addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
	dep[u]=dep[fa]+1;
	for(reg int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs(v,u);
	}
	if(dep[u]>dep[q])q=u;
}
int main(){
	read(n);
	for(reg int i=1;i<n;i++)read(u),read(v),superadd(u,v);
	dep[0]=-1;dfs(1,0);
	p=q,q=0;
	memset(dep,0,sizeof dep);
	dep[0]=-1;dfs(p,0);
	printf("%d\n",dep[q]);
}

求法 2 : 2: 2:树形 d p dp dp
这个方法也可以适用于有权值的树
在这里插入图片描述

一棵有根树的最长连,可能出现红色或黄色两种情况的直径
所以要解决这个问题,我们要求出每个结点为根的子树中的最长链,取其中的最大值为该树的直径
d p 1 [ u ] dp1[u] dp1[u]表示以 u u u为根的子树中, u u u到叶子结点的距离最大值; d p 2 [ u ] dp2[u] dp2[u]表示以 u u u为根的子树中, u u u到叶子节点的距离次大值
当遍历到 i i i的儿子 j j j时,
①若 d p 1 [ j ] + d i s [ i ] [ j ] > d p 1 [ i ] dp1[j]+dis[i][j]>dp1[i] dp1[j]+dis[i][j]>dp1[i],则 d p 2 [ i ] = d p 1 [ i ] , d p 1 [ i ] = d p 1 [ j ] + d i s [ i ] [ j ] dp2[i]=dp1[i],dp1[i]=dp1[j]+dis[i][j] dp2[i]=dp1[i],dp1[i]=dp1[j]+dis[i][j]
②若 d p 2 [ i ] < d p 1 [ j ] + d i s [ i ] [ j ] < d p 1 [ i ] dp2[i]<dp1[j]+dis[i][j]<dp1[i] dp2[i]<dp1[j]+dis[i][j]<dp1[i],则 d p 2 [ i ] = d p 1 [ j ] + d i s [ i ] [ j ] dp2[i]=dp1[j]+dis[i][j] dp2[i]=dp1[j]+dis[i][j]
最后扫描所有的结点,找最大的 d p 1 [ i ] + d p 2 [ i ] dp1[i]+dp2[i] dp1[i]+dp2[i]的值
上代码
就不找例题了 好累 哭哭

void dfs(int u, int fa){
	for(reg int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs(v,u);
		if(dp1[u]<dp1[v]+edge[i].val)
			dp2[u]=dp1[u],dp1[u]=dp1[v]+edge[i].val;
		else if(dp2[u]<dp1[v]+edge[i].val)
			dp2[u]=dp1[v]+edge[i].val;
		ans=max(ans,dp1[u]+dp2[u]);
	}
}

三、一本通题目

行 知识点就这么多够了 来上题吧
书里的例题分为五个题型
1. 1. 1.由根分成左子树和右子树两部分的情况
2. 2. 2.背包类树形 D P DP DP
3. 3. 3.求树的最长链问题
4. 4. 4.覆盖一棵树上所有边
5. 5. 5.覆盖一棵树上的所有点
就一个一个按顺序来了

题型 1. 1. 1.二叉苹果树

题面

#include<bits/stdc++.h>
using namespace std;
#define N 110
#define reg register
inline void read(int &x){
	int s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}
int n,q,u,v,w,cnt,a[N],head[N],ls[N],rs[N],dp[N][N];
struct node{
	int to,nxt,val;
}edge[N<<1];
inline void addedge(int u, int v, int w){
	edge[++cnt].to=v,edge[cnt].val=w,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v, int w){
	addedge(u,v,w),addedge(v,u,w);
}
void build(int u, int fa){
	int g=0;
	for(reg int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		if(!g)ls[u]=v,a[v]=edge[i].val,g++;
		else rs[u]=v,a[v]=edge[i].val;
		build(v,u);
	}
}
int dfs(int u, int num){
	if(!num)return 0;
	if(!ls[u]&&!rs[u])return a[u];
	if(dp[u][num])return dp[u][num];
	for(reg int k=0;k<num;k++)
		dp[u][num]=max(dp[u][num],dfs(ls[u],k)+dfs(rs[u],num-k-1)+a[u]);
	return dp[u][num];
}
int main(){
	read(n),read(q);
	for(reg int i=1;i<n;i++)read(u),read(v),read(w),superadd(u,v,w);
	build(1,0);
	printf("%d\n",dfs(1,q+1));
}
题型 2. 2. 2.选课

题面
S o l u t i o n : Solution: Solution: d p + dp+ dp+背包
首先,肯定要从没有先修课的课开始选,所以要从跟 0 0 0相连的边开始选。
故直接将 0 0 0设为根就可以了,结点数变为 n + 1 n+1 n+1
d p [ u ] [ t ] dp[u][t] dp[u][t]表示在 x x x为根的子树中选 t t t门课能够获得的最高学分
这就转化成了一个背包问题
倒序枚举空间 ( ( (即选课总门数 ) ) ) 正序枚举物品 ( ( (即传递给子树的选课门数 ) ) )
状态转移方程: d p [ u ] [ t ] = m a x j = 0 t { d p [ u ] [ t − j ] + d p [ v ] [ j ] } dp[u][t]=max_{j=0}^{t}\{dp[u][t-j]+dp[v][j]\} dp[u][t]=maxj=0t{dp[u][tj]+dp[v][j]}
上代码↓

#include<bits/stdc++.h>
using namespace std;
#define N 110
inline void read(int &x){
	int s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}
int n,m,u,cnt,head[N],val[N],dp[N][N];
struct node{
	int to,nxt;
}edge[N<<1];
inline void addedge(int u, int v){
	edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
	addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
	dp[u][1]=val[u];
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs(v,u);
		for(int t=m;t;t--)
			for(int j=0;j<t;j++)
				dp[u][t]=max(dp[u][t],dp[u][t-j]+dp[v][j]);
	}
}

int main(){
	read(n),read(m);
	for(int i=1;i<=n;i++)
		read(u),read(val[i]),superadd(u,i);
	m++;
	dfs(0,-1);
	printf("%d\n",dp[0][m]);
}
题型 3. 3. 3.数字转换

题面
S o l u t i o n : Solution: Solution:把可以转化的数和约数和连上边,这样就组成了一棵树。题中要求的最大变换即为树的直径,就用到了上面树的直径的例题。
这题还要判定一个数只能转化为比他小的约数和,所以我们要判断一个边是否能连上,间接地遍历了边集,所以用树形 d p dp dp求直径更为简洁
( ( (也可以用两次 d f s dfs dfs,但是上面写了两次 d f s dfs dfs的题,这道就用树形 d p dp dp ) ) )
P S : PS: PS:如果懒得建图一定要注意遍历顺序是从大数到小数!
上代码↓

#include<bits/stdc++.h>
using namespace std;
#define reg register
#define N 50005
int n,ans,a[N],dis1[N],dis2[N];
void dp(){
	for(reg int i=n;i;i--){
		if(a[i]<i){
			if(dis1[i]+1>dis1[a[i]])
				dis2[a[i]]=dis1[a[i]],dis1[a[i]]=dis1[i]+1;
			else if(dis1[i]+1>dis2[a[i]])dis2[a[i]]=dis1[i]+1;
		}
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		for(int j=2;j<=n/i;j++)a[i*j]+=i;
	}
	dp();
	for(reg int i=1;i<=n;i++)
		if(dis1[i]+dis2[i]>ans)ans=dis1[i]+dis2[i];
	cout<<ans<<endl;
}
题型 4. 4. 4.战略游戏

题面
不懂一本通上说的什么树的最大独立集,就把他改成覆盖树上所有点了…
S o l u t i o n : Solution: Solution:
d p [ u ] [ 0 ] dp[u][0] dp[u][0]表示 u u u点不放士兵时,以 u u u为根的子树最少需要的士兵数
d p [ u ] [ 1 ] dp[u][1] dp[u][1]表示 u u u点不放士兵时,以 u u u为根的子树最少需要的士兵数
那么当 u u u点不放士兵时,因为士兵要看到所有的路,所以所有 u u u的儿子结点都要放士兵
d p [ u ] [ 0 ] = ∑ v ∈ s o n [ u ] d p [ v ] [ 1 ] dp[u][0]=\sum\limits_{v∈son[u]}{dp[v][1]} dp[u][0]=vson[u]dp[v][1]
u u u点放士兵时, u u u的儿子结点放不放士兵都可以,那么加上 d p [ u ] [ 1 ] dp[u][1] dp[u][1] d p [ u ] [ 0 ] dp[u][0] dp[u][0]中最小的就可以了
d p [ u ] [ 1 ] = ∑ v ∈ s o n [ u ] m i n { d p [ v ] [ 0 ] , d p [ v ] [ 1 ] } dp[u][1]=\sum\limits_{v∈son[u]}{min\{dp[v][0],dp[v][1]\}} dp[u][1]=vson[u]min{dp[v][0],dp[v][1]}
上代码↓

#include<bits/stdc++.h>
using namespace std;
#define N 1515
#define reg register
inline void read(int &x){
	int s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}
int u,v,n,m,cnt,head[N],dp[N][2];
struct node{
	int nxt,to;
}edge[N<<1];
inline void addedge(int u, int v){
	edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
	addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
	dp[u][1]=1;
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs(v,u);
		dp[u][0]+=dp[v][1];
		dp[u][1]+=min(dp[v][0],dp[v][1]);
	}
}
int main(){
	read(n);
	for(reg int i=1;i<=n;i++){
		read(u),read(m);
		for(reg int j=1;j<=m;j++)read(v),superadd(u,v);
	}
	dfs(0,-1);
	printf("%d\n",min(dp[0][0],dp[0][1]));
}
题型 5. 5. 5.皇宫看守

题面
S o l u t i o n : Solution: Solution:
这道题和上道题的区别是,上道题是覆盖所有边,这道题是覆盖所有点。
覆盖所有边要用两个状态,而覆盖所有点需要用三个状态
① ① 在父结点安排警卫
d p [ u ] [ 0 ] dp[u][0] dp[u][0]表示 u u u结点在父亲结点可以被看到时,以 u u u为根的子树最少需要安排的士兵数
② ② 在当前结点安排警卫
d p [ u ] [ 1 ] dp[u][1] dp[u][1]表示 u u u结点安排警卫时,以 u u u为根的子树需要安排的最少士兵数
③ ③ 在子结点安排警卫
d p [ u ] [ 2 ] dp[u][2] dp[u][2]表示 u u u结点能被它的至少一个子节点看到时,以 u u u为根的子树最少需要安排的士兵数

对于 d p [ u ] [ 0 ] , dp[u][0], dp[u][0]表示 u u u结点能被父亲结点看到,这时 u u u结点不用安排警卫,子结点要么安排警卫,要么被它的子结点看到,所以有
d p [ u ] [ 0 ] = ∑ v ∈ s o n [ u ] m i n { d p [ v ] [ 1 ] , d p [ v ] [ 2 ] } dp[u][0]=\sum\limits_{v∈son[u]}{min\{dp[v][1],dp[v][2]\}} dp[u][0]=vson[u]min{dp[v][1],dp[v][2]}
( P S . (PS. (PS.一本通又双叒叕写错了… ) ) )

对于 d p [ u ] [ 1 ] , dp[u][1], dp[u][1],表示 u u u结点安排了警卫, u u u的儿子 v v v可以安排警卫,也可以被子结点看守,还可以被它的父结点 u u u看守,所以有
d p [ u ] [ 1 ] = c o s t [ u ] + ∑ v ∈ s o n [ u ] m i n { d p [ v ] [ 0 ] , d p [ v ] [ 1 ] , d p [ v ] [ 2 ] } dp[u][1]=cost[u]+\sum\limits_{v∈son[u]}{min\{dp[v][0],dp[v][1],dp[v][2]\}} dp[u][1]=cost[u]+vson[u]min{dp[v][0],dp[v][1],dp[v][2]}
P S . PS. PS.不要忘记加上自身的花费 c o s t [ u ] cost[u] cost[u]

对于 d p [ u ] [ 1 ] , dp[u][1], dp[u][1],表示 u u u能被至少一个子结点看到,此时 u u u结点没有被安排警卫,子结点需要安排警卫或被它的后代看到,所以我们要比较这两个方案哪个更优
但问题来了,如果所有的子结点都是被后代看到更优,那么就没有结点看到 u u u结点了,这就是为什么我加粗了至少一个,这时候我们就要判断用哪个子结点安排警卫最优
所以当所有的 d p [ v ] [ 1 ] > d p [ v ] [ 2 ] dp[v][1]>dp[v][2] dp[v][1]>dp[v][2]时,维护一个 r = m i n v ∈ s o n [ u ] { d p [ v ] [ 1 ] − d p [ v ] [ 2 ] } r=min_{v∈son[u]}\{dp[v][1]-dp[v][2]\} r=minvson[u]{dp[v][1]dp[v][2]},最后总结果加上 r r r的意义是使一个子结点安排警卫来看守 u u u结点
所以有
d p [ u ] [ 2 ] = r + ∑ v ∈ s o n [ u ] m i n { d p [ v ] [ 1 ] , d p [ v ] [ 2 ] } dp[u][2]=r+\sum\limits_{v∈son[u]}{min\{dp[v][1],dp[v][2]\}} dp[u][2]=r+vson[u]min{dp[v][1],dp[v][2]}
其中 r = m i n { d p [ v ] [ 1 ] − m i n { d p [ v ] [ 1 ] , d p [ v ] [ 2 ] } } r=min\{dp[v][1]-min\{dp[v][1],dp[v][2]\}\} r=min{dp[v][1]min{dp[v][1],dp[v][2]}}
P S . PS. PS.这样写 r r r的意思是,如果子结点有安排警卫的,那么 r = 0 r=0 r=0表示不用补子结点
如果都没有 那么 r = d p [ v ] [ 1 ] − d p [ v ] [ 2 ] r=dp[v][1]-dp[v][2] r=dp[v][1]dp[v][2]表示让 v v v结点从被子结点看到改为自己安排警卫
我这块理解了很长时间 所以就多码点字 哈哈
上代码↓

#include<bits/stdc++.h>
using namespace std;
#define N 1515
#define reg register
inline void read(int &x){
	int s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}
int n,m,u,v,cnt,head[N],dp[N][3],c[N];
struct node{
	int to,nxt;
}edge[N<<1];
inline void addedge(int u, int v){
	edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
	addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
	int r=2e9;dp[u][1]=c[u];
	for(reg int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs(v,u);
		dp[u][0]+=min(dp[v][1],dp[v][2]);
		dp[u][1]+=min(dp[v][0],min(dp[v][1],dp[v][2]));
		dp[u][2]+=min(dp[v][1],dp[v][2]),r=min(r,dp[v][1]-min(dp[v][1],dp[v][2]));
	}
	dp[u][2]+=r;
}
int main(){
	read(n);
	for(reg int i=1;i<=n;i++){
		read(u),read(c[u]),read(m);
		for(reg int j=1;j<=m;j++)read(v),superadd(u,v);
	}
	dfs(1,0);
	printf("%d\n",min(dp[1][1],dp[1][2]));
}

后面就是练习题了

加分二叉树

题面
S o l u t i o n : Solution: Solution:数据范围这么小忍不住用区间 d p dp dp

#include<bits/stdc++.h>
using namespace std;
#define N 50
typedef long long ll;
int n,root[N][N];
ll dp[N][N],ans;
inline void read(ll &x){
	ll s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}
void print(int l, int r){
	if(l>r)return ;
	printf("%d ",root[l][r]);
	if(l==r)return ;
	print(l,root[l][r]-1),print(root[l][r]+1,r);
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)read(dp[i][i]),root[i][i]=i;
	for(int l=1;l<n;l++){
		for(int i=1,j=l+1;j<=n;i++,j++){
			dp[i][j]=dp[i+1][j]+dp[i][i];
			root[i][j]=i;
			for(int k=i+1;k<j;k++){
				ll now=dp[i][k-1]*dp[k+1][j]+dp[k][k];
				if(dp[i][j]<now)dp[i][j]=now,root[i][j]=k;
			}	
		}
	}
	printf("%lld\n",dp[1][n]);
	print(1,n);
}
旅游规划

题面
S o l u t i o n : Solution: Solution:这道题可能有多条直径,而且要求出所有在直径上的点,所以用树形 d p dp dp求树的直径
还是往常的直径的长度 l e n = m a x { d i s 1 [ i ] len=max\{dis1[i] len=max{dis1[i] d i s [ 2 ] } dis[2]\} dis[2]}
但是问题来了,如果一个点 u u u在直径上,但他只有两条边,一条连向父亲 f a fa fa,一条连向儿子 v v v,那么他的 d i s 2 dis2 dis2值会为 0 0 0,但实际上他也在直径上
这个问题的出现的原因是因为我们把 d f s dfs dfs设成单向,不让一个点去重复遍历他的父亲结点,因为他遍历不了父亲结点,所以才导致 d i s 2 dis2 dis2值为 0 0 0,故我们需要求出此节点在父亲结点方向链长的最大值
如何解决这个问题呢
我们设置两个数组 s o n 1 [ i ] = v son1[i]=v son1[i]=v表示 i i i点的 d i s 1 [ i ] dis1[i] dis1[i]值是从 v v v点转移的, m o v e [ i ] move[i] move[i]表示 i i i点在父亲结点方向的链长最大值
分两种情况
1. 1. 1.如果 s o n [ f a ] = u son[fa]=u son[fa]=u:说明 u u u就在 f a fa fa求得 d i s 1 dis1 dis1值的链中,转移 m o v e [ u ] move[u] move[u]不能从 d i s 1 dis1 dis1中转移。那么又有两种情况, f a fa fa d i s 2 dis2 dis2和没有 d i s 2 dis2 dis2,所以 m o v e [ u ] = m a x { d i s 2 [ f a ] , m o v e [ f a ] } move[u]=max\{dis2[fa],move[fa]\} move[u]=max{dis2[fa],move[fa]}
2. 2. 2.如果 s o n [ f a ] ≠ u son[fa]\neq u son[fa]=u:这时候转移可以从 d i s 1 dis1 dis1转移了,所以 m o v e [ u ] = m a x { d i s 1 [ f a ] , m o a v [ f a ] } + 1 move[u]=max\{dis1[fa],moav[fa]\}+1 move[u]=max{dis1[fa],moav[fa]}+1
最后判断 m [ i ] + d i s 1 [ i ] = l e n m[i]+dis1[i]=len m[i]+dis1[i]=len d i s 1 [ i ] + d i s 2 [ i ] = l e n dis1[i]+dis2[i]=len dis1[i]+dis2[i]=len时说明 i i i在直径上
上代码

#include<bits/stdc++.h>
using namespace std;
#define N 200020
#define reg register
inline void read(int &x){
	int s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}
int n,u,v,ans,cnt,head[N],dis1[N],dis2[N],son1[N],m[N];
struct node{
	int to,nxt;
}edge[N<<1];
inline void addedge(int u, int v){
	edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
	addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
	for(reg int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs(v,u);
		if(dis1[u]<dis1[v]+1)dis2[u]=dis1[u],dis1[u]=dis1[v]+1,son1[u]=v;
		else if(dis2[u]<dis1[v]+1)dis2[u]=dis1[v]+1;
	}
	ans=max(ans,dis1[u]+dis2[u]);
}
void df5(int u, int fa){
	if(u!=1){
		if(son1[fa]==u)m[u]=max(dis2[fa],m[fa])+1;
		else m[u]=max(dis1[fa],m[fa])+1;
	}
	for(reg int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v!=fa)df5(v,u);
	}
}
int main(){
	read(n);
	for(reg int i=1;i<n;i++)read(u),read(v),superadd(++u,++v);
	dfs(1,0);df5(1,0);
	for(reg int i=1;i<=n;i++)if(m[i]+dis1[i]==ans||dis1[i]+dis2[i]==ans)printf("%d\n",i-1);
}
周年纪念晚会

题面
S o l u t i o n : Solution: Solution:和没有上司的舞会一样
d p [ u ] [ 0 ] dp[u][0] dp[u][0]表示 u u u不去, d p [ u ] [ 1 ] dp[u][1] dp[u][1]表示 u u u
那么
d p [ u ] [ 0 ] = ∑ v ∈ s o n [ u ] m i n { d p [ v ] [ 0 ] , d p [ v ] [ 1 ] } dp[u][0]=\sum\limits_{v∈son[u]}min\{dp[v][0],dp[v][1]\} dp[u][0]=vson[u]min{dp[v][0],dp[v][1]}
d p [ u ] [ 1 ] = ∑ v ∈ s o n [ u ] d p [ v ] [ 0 ] dp[u][1]=\sum\limits_{v∈son[u]}dp[v][0] dp[u][1]=vson[u]dp[v][0]
代码↓

#include<bits/stdc++.h>
using namespace std;
#define N 6060
#define reg register
inline void read(int &x){
	int s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}
int n,u,v,cnt,head[N],dp[N][2],a[N];
struct node{
	int to,nxt;
}edge[N<<1];
inline void addedge(int u, int v){
	edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
	addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
	dp[u][1]=a[u];
	for(reg int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs(v,u);
		dp[u][0]+=max(dp[v][0]+dp[v][1]);
		dp[u][1]+=dp[v][0];
	}
}
int main(){
	read(n);
	for(reg int i=1;i<=n;i++)read(a[i]);
	while(1){
		read(u),read(v);
		if(u==0&&v==0)break;
		superadd(u,v);
	}
	dfs(1,0);
	printf("%d\n",max(dp[1][0],dp[1][1]));
}

叶子的颜色 ( C Q O I 2009 (CQOI2009 (CQOI2009叶子的染色 ) ) )

题面
S o l u t i o n : Solution: Solution:首先一个结点有三种状态: 1. 1. 1.黑色 2. 2. 2.白色 3. 3. 3.无色
我们可以发现,叶子结点的染色方案只与上一个有色结点有关
这句话有两个意思
1. 1. 1.无色结点与答案无关,因为它做不出任何贡献
2. 2. 2.根结点与答案无关,因为答案只与叶子结点的上一个有色结点有关
d p [ u ] [ 0 ] dp[u][0] dp[u][0] u u u结点染成黑色时,以 u u u为根的子树需要的最少代价
d p [ u ] [ 1 ] dp[u][1] dp[u][1] u u u结点染成白色时,以 u u u为根的子树需要的最少代价
那么 d p [ u ] [ 0 ] = 1 + ∑ v ∈ s o n [ u ] m i n { d p [ v ] [ 0 ] − 1 , d p [ v ] [ 1 ] } dp[u][0]=1+\sum\limits_{v∈son[u]}min\{dp[v][0]-1,dp[v][1]\} dp[u][0]=1+vson[u]min{dp[v][0]1,dp[v][1]}
d p [ u ] [ 1 ] = 1 + ∑ v ∈ s o n m i n { d p [ v ] [ 0 ] , d p [ v ] [ 1 ] − 1 } dp[u][1]=1+\sum\limits_{v∈son}min\{dp[v][0],dp[v][1]-1\} dp[u][1]=1+vsonmin{dp[v][0],dp[v][1]1}
初始条件
d p [ u ] [ c [ u ] ] = 1 , d p [ u ] [ ! c [ u ] ] = i n f               u ∈ [ 1 , n ] dp[u][c[u]]=1,dp[u][!c[u]]=inf\,\,\,\,\,\,\,\,\,\,\,\,\,u∈[1,n] dp[u][c[u]]=1,dp[u][!c[u]]=infu[1,n]
d p [ u ] [ 0 ] = 1 , d p [ u ] [ 1 ] = 1               u ∈ [ n + 1 , m ] dp[u][0]=1,dp[u][1]=1\,\,\,\,\,\,\,\,\,\,\,\,\,u∈[n+1,m] dp[u][0]=1,dp[u][1]=1u[n+1,m]
上代码↓

#include<bits/stdc++.h>
using namespace std;
#define N 10010
#define reg register
#define inf 2000000000
inline void read(int &x){
	int s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}
int n,m,u,v,cnt,head[N],dp[N][2],c[N];
struct node{
	int to,nxt;
}edge[N<<1];
inline void addedge(int u, int v){
	edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline void superadd(int u, int v){
	addedge(u,v),addedge(v,u);
}
void dfs(int u, int fa){
	for(reg int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs(v,u);
		dp[u][0]+=min(dp[v][0]-1,dp[v][1]);
		dp[u][1]+=min(dp[v][0],dp[v][1]-1);
	}
}
int main(){
	read(m),read(n);
	for(reg int i=1;i<=n;i++)read(c[i]),dp[i][c[i]]=1,dp[i][!c[i]]=inf;
	for(reg int i=1;i<m;i++)read(u),read(v),superadd(u,v);
	for(reg int i=1+n;i<=m;i++)dp[i][0]=dp[i][1]=1;
	dfs(n+1,0);
	printf("%d\n",min(dp[n+1][0],dp[n+1][1]));
}
骑士

题面
S o l u t i o n : Solution: Solution:模型是没有上司的舞会的模型,只不过把每个骑士和他烦的骑士连上边,是颗有 n n n个结点 n n n条边的基环树
那么我们找到环,把环中的一条边断开,那么就变成了一棵树,再对断开的两个结点分别做根进行树 d p dp dp。因为断开的两个结点也是有条件约束的,就强制在一个结点遍历时不遍历另一个结点。
树中会有很多联通块,对每个联通块都进行一次找环的操作,所有答案加起来就是最后的答案
实现时要注意:
1. 1. 1.边要连单向边,并存一个 f f f数组指向边的反方向,来存储上一个结点的信息
2. 2. 2.环只有一个,联通块不是一个,要对每个联通块都求一遍环
3. a n s 3.ans 3.ans要开 l o n g      l o n g long\,\,\,\,long longlong
上代码↓

#include<bits/stdc++.h>
using namespace std;
#define reg register
#define N 1000100
typedef long long ll;
inline void read(int &x){
	int s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}
ll ans,dp[N][2];
bool vis[N];
int n,u,v,rt,cnt,head[N],c[N],f[N];
struct node{
	int to,nxt;
}edge[N];
inline void addedge(int u, int v){
	edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
void dfs(int u){
	vis[u]=1;
	dp[u][0]=0,dp[u][1]=c[u];
	for(reg int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==rt)continue;
		dfs(v);
		dp[u][0]+=max(dp[v][1],dp[v][0]);
		dp[u][1]+=dp[v][0];
	}
}
inline void judge(int x){
	rt=x,vis[x]=1;
	while(!vis[f[rt]])rt=f[rt],vis[rt]=true;
	dfs(rt);
	ll p=dp[rt][0];
	vis[rt]=true,rt=f[rt];
	dfs(rt);
	ll q=dp[rt][0];
	ans+=max(p,q);
}
int main(){
	read(n);
	for(reg int i=1;i<=n;i++)read(c[i]),read(v),addedge(v,i),f[i]=v;
	for(reg int i=1;i<=n;i++)if(!vis[i])judge(i);
	printf("%lld\n",ans);
}

四、总结

讲个故事
C S P 2019 CSP2019 CSP2019前在家跟同学语音刷题,突然发现好多蓝树 d p dp dp题都变成了绿色,我就跟同学提了嘴是不是出题组为了出树 d p dp dp才把标签弄简单的,结果没想到三个树 d p dp dp

总的来说,树形 d p dp dp是一个能解决许多树上问题的一个优秀高效算法,在联赛中也屡见不鲜。
本人 Q Q : 407694747 QQ:407694747 QQ:407694747,欢迎各位大佬一起来讨论

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值