树形DP

树形dp

summary

树形dp的主要实现形式是dfs,在dfs中dp,主要的实现形式是\(dp[i] [j] [0/1]\),i 是以 i 为根的子树,j 是表示在以 i 为根的子树中选择 j 个子节点,0表示这个节点不选,1表示选择这个节点。有的时候 j 或0/1这一维可以压掉

选择节点/边类

\[dp[i][0]=dp[j][1]\\ dp[i][1]=max/min(dp[j][0],dp[j][1]) \]

没有上司的舞会

三种状态

1、上司去,而剩下的这个上司的职员就不会去。
2、不去,而那个职员去
3、上司不去,而那个职员也不去。

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
const int N =6010;
int n,st,r[N],f[N][2];//0不 1 go 
vector< int >g[N];
bool vis[N];
void dfs(int x,int fa){
	f[x][0]=0;f[x][1]=r[x];
	int siz=g[x].size();
	for(int i=0;i<siz;i++){
		int y=g[x][i];
		if(y==fa)continue;
		dfs(y,x);
		f[x][0]+=max(f[y][0],f[y][1]);
		f[x][1]+=f[y][0];
	}
}
int main(){
	n=read();
	for(int i=1;i<=n;i++)r[i]=read();
	for(int i=1,x,y;i<n;i++){
		x=read();
		y=read();
		vis[x]=1;
		g[y].push_back(x);
	}
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			st=i,dfs(i,0);break;
		}
	} 
	printf("%d\n",max(f[st][0],f[st][1]));
	return 0;
}

最大子树和

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
#define N 16050
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
int f[N];
int n,ans;
vector< int >g[N];
void dfs(int x,int fa){
	for(int i=0;i<g[x].size();i++){
		int y=g[x][i];
		if(y==fa)continue;
		dfs(y,x);
		f[x]+=max(f[y],0);
	}
	ans=max(ans,f[x]);
}
int main(){
	n=read();
	for(int i=1;i<=n;i++)f[i]=read();
	for(int i=1,x,y;i<n;i++){
		x=read();y=read();
		g[x].push_back(y);
		g[y].push_back(x);
	}
	dfs(1,0);
	printf("%d\n",ans);
	return 0;
}

战略游戏

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
const int N = 2000;
int n,ans,f[N][2];
vector< int >g[N]; 
bool vis[N];
void dfs(int x,int fa){
	f[x][1]=1;f[x][0]=0;
	for(int i=0;i<g[x].size();i++){
		int y=g[x][i];
		if(y==fa)continue;
		dfs(y,x);
		f[x][0]+=f[y][1];
		f[x][1]+=min(f[y][1],f[y][0]);
	}
}
int main(){
	n=read();
	for(int i=1,x,y,num;i<=n;i++){
		x=read();num=read();
		while(num--){
			y=read();
			g[x].push_back(y);
			g[y].push_back(x);
		}
	} 
	dfs(0,-1); 
	printf("%d",min(f[0][0],f[0][1]));
	return 0;
}

消防局的设立

贪心
树上的最小点覆盖
按照反方向的bfs序(从叶子到根)来进行贪心.每检查一个结点,覆盖他的爷爷,然后对爷爷的儿孙打标记

dfs序
2 4 5 7 6 8 3
bfs序:是一种层次遍历算法
2 3 4 5 6 7 8
用queue维护bfs序的同时维护一个栈——得到反向bfs序

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<vector>
#include<stack>
#define N 10005
using namespace std;
int n,m;
int fa[N],cnt,ans=0,dad;
bool vis[N];
vector< int > g[N];
queue <int> q; 
stack <int> s;
void dfs(int x,int dis){
	if (dis>2) return;
    vis[x]=1;
    for(int i=0;i<g[x].size();i++)
        dfs(g[x][i],dis+1);
}
int main(){
	scanf("%d",&n);
	for(int i=2,x;i<=n;i++){
		scanf("%d",&fa[i]);
		g[i].push_back(fa[i]);
		g[fa[i]].push_back(i);
	}
	q.push(1);
    s.push(1);
    while (!q.empty()){
        int x=q.front();
        q.pop();
        for (int i=0;i<g[x].size();i++) {
            int y=g[x][i];
            if (y==fa[x]) continue;
            q.push(y);
            s.push(y); 
        }
    }
	while(!s.empty()){
		int x=s.top();s.pop();
		if(!vis[x]){
			++ans;
			dfs(fa[fa[x]],0);
		}
	}
	printf("%d",ans);
	return 0;
}

保安站岗

(以下全部都是对于要覆盖任意一个以x为根的子树来说的)

(其中我们设y节点为y的儿子,fa为x的父亲)

1.x节点被自己覆盖,即选择x点来覆盖x点—— f[x] [0]

2.x节点被儿子y覆盖,即选择y点来覆盖x点 —— f[x] [1]

3.x节点被父亲fa覆盖,即选择fa点来覆盖x点 —— f[x] [2]

\[对于三种状态的转移方程\\ f[x][0]=\sum(f[y][0],f[y][1],f[y][2]);\\ 选了自己,那么儿子肯定被覆盖,无脑取min\\ f[x][1]=\sum(f[y][0],f[y][1]);\\ 儿子覆盖自己,那么儿子肯定不能从父亲被覆盖。然后注意!!!\\ 如果一直取f[y][1]最后就到叶子也没人覆盖x,所以最后要特判,加上min(f[y][0]-f[y][1])\\ f[x][2]=\sum(f[y][0],f[y][1]);\\ 父亲覆盖自己,显然覆盖不了自己的儿子,那么要么y被y的儿子覆盖,要么被y自己覆盖\\ \]

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
const int N = 3055;
int n,m,w[N],f[N][3],add=0x3f3f3f3f;
vector<int >g[N]; 
inline void dfs(int x,int from){
	int sz=g[x].size();
	bool add=true;
	int minn=0x3f3f3f3f;
	for(int i=0;i<sz;++i){
		int y=g[x][i];
		if(y==from)continue;
		dfs(y,x);
		int t=min(f[y][1],f[y][0]);
		f[x][0]+=min(t,f[y][2]);
		f[x][2]+=t;
		f[x][1]+=t;
		minn=min(minn,f[y][0]-f[y][1]);
		if(f[y][0]<=f[y][1])add=false;
	}
	f[x][0] += w[x];
	if(add)	f[x][1] += minn; 
}
int main(){
	n=read();
	for(int i=1,x,y;i<=n;i++){
		x=read();w[x]=read();m=read();
		while(m--){
			y=read();
			g[x].push_back(y);
			g[y].push_back(x);
		}
	}
	dfs(1,0);
	printf("%d\n",min(f[1][0],f[1][1]));
	return 0;
 } 

联合权值

树形dp

距离差2的话,要么是祖父和儿子,要么是两个儿子之间

维护联合权值最大值只需记录每个点儿子的最大值,次大值

儿子之间联合权值之和等于权值和的平方减去权值的平方和( son中w总和,平方一下,再减去son[i]与son[i](自己配自己)这样不合法的情况即可 )

儿子和祖父就直接 w相乘再乘2

#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;
const int N=200005;
const int md=10007;
inline int read() {
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return f*x;
}
typedef long long ll;
inline void plu(ll &x,ll y){x+=y,x>=md&&(x-=md);}
ll n,w[N];
ll ans1,ans2;
int hd[N],to[N<<1],nxt[N<<1],tot;
inline void add(int x,int y) {
	to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
void dfs(int x,int fa,int g) {
	ll fir=0,sec=0,sum1=0,sum2=0;//1是和的平方,2平方的和
	for(int i=hd[x];i;i=nxt[i]) {
		int y=to[i];
		if(y==fa) continue;
		plu(sum1,w[y]);
		plu(sum2,w[y]*w[y]%md);
		if(w[y]>fir) 
			sec=fir,fir=w[y];
		else if(w[y]>sec)
			sec=w[y];
		dfs(y,x,fa);
	}
	ans1=max(ans1,max(fir*sec,w[g]*w[x]));
	plu(ans2,((sum1*sum1%md-sum2+md)%md+(w[x]*w[g]*2)%md)%md);
}
int main() {
	n=read();
	int x,y;
	for(int i=1;i<n;i++) {
		x=read();y=read();
		add(x,y),add(y,x);
	}
	for(int i=1;i<=n;i++) w[i]=read();
	dfs(1,0,0);
	printf("%lld %lld\n",ans1,ans2);
	return 0;
}
	

CF633F The Chocolate Spree

https://www.cnblogs.com/zwfymqz/p/9759346.html

#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;
#define int long long
inline int read() {
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
const int N=100006;
inline void Max(int &x,int y) {if(x<y)x=y;}
int n,ans,w[N];
vector<int> G[N];
inline void add(int x,int y) {
	G[x].push_back(y);G[y].push_back(x);
}
int f[N][2],g[N],d[N],h[N];
//f[x][0]以x为根的子树选两条,1选1条
void dfs(int x,int fa) {
	f[x][0]=f[x][1]=g[x]=d[x]=w[x];
	for(auto y:G[x]) {
		if(y==fa) continue;
		dfs(y,x);
		Max(f[x][0],f[y][0]);
		Max(f[x][0],f[y][1]+f[x][1]);
		Max(f[x][0],d[y]+g[x]);
		Max(f[x][0],d[x]+g[y]);

		Max(f[x][1],f[y][1]);
		Max(f[x][1],d[y]+d[x]);

		Max(g[x],g[y]+w[x]);
		Max(g[x],d[x]+f[y][1]);
		Max(g[x],d[y]+w[x]+h[x]);
		Max(h[x],f[y][1]);
		Max(d[x],w[x]+d[y]);
	}
}
signed main() {
	n=read();
	for(int i=1;i<=n;i++) w[i]=read();
	for(int i=1;i<n;i++) 
		add(read(),read());
	dfs(1,0);
	printf("%lld\n",f[1][0]);
	return 0;
}

树形背包类

\[dp[v][k]=dp[u][k]+val\\ dp[u][k]=max(dp[u][k],dp[v][k−1]) \]

选课

类似于有依赖的背包

设f(i)(j)表示在以 i 为根的子树中,选择 j 个点并且一定选择 i 的最大价值。

这里以0节点为开始的根

把选课前需要学的课与其连边(若没有要学的和0连边)

很明显,f(i)(1)就是其自己学分

转移类似01背包(倒序)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>

#define N 1005
using namespace std;
int n,m,f[N][N];
vector<int>g[N];
int maxx(int x,int y){
    return x>y?x:y;
}
void dfs(int x){
    int siz=g[x].size();
    for(int i=0;i<siz;i++){
        int y=g[x][i];dfs(y);
        for(int j=m+1;j>=1;j--)
            for(int k=0;k<j;k++)
                f[x][j]=maxx(f[x][j],f[y][k]+f[x][j-k]);
    }
}
int main(){
    scanf("%d%d",&n,&m) ;
    for(int i=1,x;i<=n;i++){
        scanf("%d",&x);
        scanf("%d",&f[i][1]);
        g[x].push_back(i);
    }
    dfs(0);
    printf("%d\n",f[0][m+1]);
    return 0;
}

高明的另解

之后模拟赛的t4用另解的思想可过,第一个会TLE

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;
typedef long long ll;
const int N=1005;
inline int read() {
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return f*x;
}
int n,m;
int f[N][N];
int to[N<<1],nxt[N<<1],hd[N],tot,fa[N];
inline void add(int x,int y) {
    to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
int s[N],rt;
int dfn_cnt,rev[N],siz[N];//反dfn序
int dfs(int x) {
    int size=1;
    for(int i=hd[x];i;i=nxt[i]) 
        size+=dfs(to[i]);
    rev[++dfn_cnt]=x;siz[dfn_cnt]=size;
    return size;
}
int main() {
    n=read();m=read();
    for(int i=1;i<=n;i++) {
        int k=read();s[i]=read();
        if(k) add(k,i);
        else add(rt,i);
    }
    for(int i=hd[rt];i;i=nxt[i])
        dfs(to[i]);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            f[i][j]=max(f[i-siz[i]][j],f[i-1][j-1]+s[rev[i]]);
    printf("%d\n",f[n][m]);
    return 0;
}

二叉苹果树

一棵二叉树,边上有苹果,给定需要保留的边数量,求出最多能留住多少苹果。

维护子树大小siz[ ]

为什么是f[u] [i-j-1]而不是f[u] [i-j]

为前文提到了,保留一条边必须保留从根节点到这条边路径上的所有边,那么如果你想从u的子节点v的子树上留边的话,也要留下u,v之间的连边

那么取值范围k为什么要小于等于i-1而不是i呢?

同上,因为要保留u,v连边

对了,别忘了i,j要倒序枚举因为这是01背包

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<utility>
using namespace std;
#define N 105
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
int f[N][N],siz[N];
int n,m;
vector< pair<int,int> >g[N];
void dfs(int x,int fa){
	for(int i=0;i<g[x].size();i++){
		int y=g[x][i].first;
		if(y==fa)continue;
		dfs(y,x);
		siz[x]+=siz[y]+1;
		for(int j=min(siz[x],m);j>0;j--)
			for(int k=min(j-1,siz[y]);k>=0;k--)
				f[x][j]=max(f[x][j],f[x][j-k-1]+f[y][k]+g[x][i].second);
		}
	
}
int main(){
	n=read();m=read();
	for(int i=1,x,y,z;i<n;i++){
		x=read();y=read();z=read();
		g[x].push_back(make_pair(y,z));
		g[y].push_back(make_pair(x,z));
	}
	dfs(1,-1);
	printf("%d\n",f[1][m]);
	return 0;
}

时态同步

模拟:mx【x】 以x为根的到终点的路径长,然后ans统计做差就好

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
typedef long long LL;
const int N = 1000005;
LL ans,mx[N];
int n,s;
vector< pair<int,LL> >g[N]; 
bool vis[N];
void dfs(int x,int fa){
	for(int i=0;i<g[x].size();i++){
		int y=g[x][i].first;
		if(y!=fa){
			dfs(y,x);
			mx[x]=max(mx[x],mx[y]+g[x][i].second);
		}
	}
	for(int i=0;i<g[x].size();i++)
		if(g[x][i].first!=fa)ans+=mx[x]-(g[x][i].second+mx[g[x][i].first]);
}
int main(){
	n=read();s=read(); 
	for(int i=1,x,y,z;i<n;i++){
		x=read();y=read();z=read();
		g[x].push_back(make_pair(y,z));
		g[y].push_back(make_pair(x,z));
	} 
	dfs(s,0); 
	printf("%lld\n",ans);
	return 0;
}

[HAOI2010]软件安装

#include <iostream>
#include <cstdio>
#include <cctype>
#include <vector>
using namespace std;
const int N=105;
const int M=505;
inline int read() {
	int x=0;int f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
int n,m;
int st[N],tp,dfn[N],low[N],dfn_cnt,col[N],co_cnt;
bool vis[N];
int sw[N],w[N],sv[N],v[N],in[N],d[N];
vector<int> G[N];
void tarjan(int x) {
	vis[x]=1;
	st[++tp]=x;
	dfn[x]=low[x]=++dfn_cnt;
	for(auto y:G[x]) {
		if(!dfn[y]) {
			tarjan(y);
			low[x]=min(low[x],low[y]);
		} else if(vis[y])
			low[x]=min(low[x],dfn[y]);
	}
	if(dfn[x]==low[x]) {
		co_cnt++;
		while(st[tp+1]!=x) {
			col[st[tp]]=co_cnt;
			sw[co_cnt]+=w[st[tp]];
			sv[co_cnt]+=v[st[tp]];
			vis[st[tp--]]=0;
		}
	}
}
int hd[N],to[N*N],nxt[N*N],tot;
inline void add(int x,int y) {
	to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
int f[N][M];
void dfs(int x) {
	for(int i=sw[x];i<=m;i++)
		f[x][i]=sv[x];
	for(int i=hd[x];i;i=nxt[i]) {
		dfs(to[i]);
		for(int j=m;j>=sw[x];j--)
			for(int k=0;k<=j-sw[x];k++)
				f[x][j]=max(f[x][j],f[x][j-k]+f[to[i]][k]);
	}
}
int main()
{
	n=read(),m=read();
	for(int i=1;i<=n;i++)
		w[i]=read();
	for(int i=1;i<=n;i++)
		v[i]=read();
	for(int i=1;i<=n;i++) {
		d[i]=read();
		if(d[i]) G[d[i]].push_back(i);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i])
			tarjan(i);

	for(int i=1;i<=n;i++)
		if(d[i]&&col[d[i]]!=col[i])
			add(col[d[i]],col[i]),in[col[i]]++;
	for(int i=1;i<=co_cnt;i++)
		if(!in[i]) add(n+1,i);
	dfs(n+1);
	printf("%d\n",f[n+1][m]);
	return 0;
}

题:

https://www.luogu.com.cn/problem/P1272

https://www.luogu.com.cn/blog/nofind/p3761-tjoi2017-cheng-shi-shu-xing-dp

https://www.luogu.com.cn/problem/P3647

https://www.luogu.com.cn/problem/P1453

https://www.luogu.com.cn/problem/P1273

posted @ 2020-08-15 11:11  ke_xin  阅读(51)  评论(0编辑  收藏  举报