6.1训练总结树形dp

6.1训练总结

今天学习了树形dp和换根dp,这两个dp主要就是把普通的dp搬到树上去,再把根换掉,就是这么多。那我就先从普通的树形dp开始。

树形dp

第一道题目,我们就先从没有上司的舞会开始。

没有上司的舞会

传送门

题目大概就是每一个员工都有一个快乐值,但是如果他的上司去了他就不能去了,求最大值。

那我们不妨可以设一个 f i , 0 / 1 f_{i,0/1} fi,0/1 数组所代表的意思就是第 i i i 位员工去或者不去舞会,那么状态就只能从他的下一级推过来。如果去,下一级就不去,如果不去,那么下一级可以去也可以不去。这里很重要,因为万一你的下下级快乐值很高,所以如果你的下一级去了就错了。就是如下图所示:就是如果你的老板不去但是领导2去了,那你的主管2就不能去了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

#include<bits/stdc++.h>
using namespace std;
int r[6005],f[6005][2], x, y, n, head[6500], nex[6500], ver[6005],tot;
bool fa[6005];
void add(int x, int y)//!链式前向星建树
{
	ver[++tot]=y;
	nex[tot]=head[x];
	head[x]=tot;
}
void dfs(int x)
{
	f[x][0]=0;//!初始化
	f[x][1]=r[x];
	for(int i=head[x];i;i=nex[i])//!根开始遍历
	{
		int y=ver[i];
		dfs(y);//!先把子结点的数值计算出来
		f[x][0]+=max(f[y][0],f[y][1]);//当前节点不去
		f[x][1]+=f[y][0];//去
	}
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>r[i];
	}
	for(int i=1;i<n;i++)
	{
		int x, y;
		cin>>x>>y;
		add(y,x);//加边
		fa[x]=1;
	}
	for(int i=1;i<=n;i++)
	{
		if(!fa[i])
		{
			dfs(i);
			cout<<max(f[i][0],f[i][1])<<endl;
		}
	}
	return 0;
}

那我们下一题来讲二叉苹果树

二叉苹果树

传送门

#include <iostream>
#include <cstdio>
using namespace std;
struct node
{
    int t;
    int apple;
    int next;
};
node e[2*101];
int dp[101][101];
int head[101],n,q,tot=0;
void add(int x,int y,int z)  //邻接表存数
{
    e[++tot].t=y;
    e[tot].apple=z;
    e[tot].next=head[x];
    head[x]=tot;
}
void dfs(int f,int fa,int apple)
{
    int son[101]={0},cnt=0; //son[1]表示f的左儿子在第几条边,son[2]表示f的右儿子在第几条边
    bool flag=false;
    for(int xun=head[f];xun;xun=e[xun].next)
    {
        if(e[xun].t!=fa)
        {
        	flag=true;
            son[++cnt]=xun;
            dfs(e[xun].t,f,e[xun].apple);
        }
    }
    if(!flag)
    {
        return;
    }
    for(int i=1;i<=q;i++) //DP部分
    {
        for(int j=0;j<=i;j++)
        {
        	int t1=0;
        	if(j-1>=0) t1+=e[son[1]].apple;  //j-1>=0表示分配给了左儿子与i节点的一条相连的树枝
        	if(i-j-1>=0) t1+=e[son[2]].apple;//i-j-1>=0表示分配给了右儿子与i节点的一条相连的树枝
        	if(j!=0)
         		dp[f][i]=max(dp[f][i],dp[e[son[1]].t][j-1]+t1+dp[e[son[2]].t][i-j-1]);  //j!=0,表示两个儿子都分配了
         	else //j==0,表示只分配给了右儿子树枝
         		dp[f][i]=max(dp[f][i],dp[e[son[2]].t][i-j-1]+t1);
        }
    }
}
int main()
{
    scanf("%d %d",&n,&q);
    for(int i=1;i<=n-1;i++)
    {
        int x,y,z;
        scanf("%d %d %d",&x,&y,&z);
        add(x,y,z);
        add(y,x,z);
    }
    dfs(1,0,0);
    printf("%d",dp[1][q]); 
    return 0;
}

剩下的不多说了,代码如下。

Barn Painting G
#include<bits/stdc++.h>
#define int long long
#define Mod 1000000007
using namespace std;
int nex[200005], head[200005], f[200005][4], c[200005], n, m, tot, ver[200005], vis[200005];
void add(int x, int y)
{
    ver[++tot]=y;
	nex[tot]=head[x];
	head[x]=tot;
}
void dfs(int x)
{
    vis[x]=1;
    if(c[x])
    {
        f[x][c[x]]=1;
    }
    else
    {
        f[x][1]=1;
        f[x][2]=1;
        f[x][3]=1;
    }
    for(int i=head[x];i;i=nex[i])
    {
        int y=ver[i];
        if(!vis[y])
        {
            dfs(y);
            f[x][1]=f[x][1]*((f[y][2]+f[y][3])%Mod)%Mod;
            f[x][2]=f[x][2]*((f[y][1]+f[y][3])%Mod)%Mod;
            f[x][3]=f[x][3]*((f[y][2]+f[y][1])%Mod)%Mod;
        }
    }
}
signed main()
{
    cin>>n>>m;
    for(int i=1;i<n;i++)
    {
        int x, y;
        cin>>x>>y;
        add(x,y);
        add(y,x); 
    }
    for(int i=1;i<=m;i++)
    {
        int x, y;
        cin>>x>>y;
        c[x]=y;
    }
    dfs(1);
    cout<<(f[1][1]+f[1][2]+f[1][3])%Mod<<endl;
    return 0;
}

接下来讲换根dp

换根dp

换根dp就是在求着求着根会发生变化,但是有一大部分的父子关系是不变的,所以我们只要先处理,再做就可以了。

选课
#include<bits/stdc++.h>
using namespace std;
struct node
{
	int nex, to;
}e[505];
int head[505];
int tot;
int n, m;
int dp[505][505];//!表示以这个点为根节点,用了j的容量所得到的最大价值
void add(int x, int y)//!链式前向星加边
{
	e[++tot].nex=head[x];
	e[tot].to=y;
	head[x]=tot;
}
void dfs(int x)
{
	for(int i=head[x];i;i=e[i].nex)//!先确保子结点有数据
	{
		dfs(e[i].to);
	}
	for(int i=head[x];i;i=e[i].nex)//!遍历所有的点
	{
		for(int j=m;j>0;--j)//!与01背包类似
		{
			for(int k=0;k<j;k++)//!这里是遍历所有的课程
			{
				int y=e[i].to;
				dp[x][j]=max(dp[x][j],dp[x][j-k]+dp[y][k]);
			}
		}
	}
}
int main()
{
	cin>>n>>m;
	++m;
	for(int i=1;i<=n;i++)
	{
		int x, y;
		cin>>x>>y;
		dp[i][1]=y;
		add(x,i);
	}
	dfs(0);
	cout<<dp[0][m]<<endl;
	return 0;
}
树的直径

那我们直接做树的直径这道题

这道题目运用了一个性质,从根节点所到达的最远的点肯定在直径中,就不证明了。

那么就有 2 2 2 种可能,用 2 2 2 遍dfs,但是遇到负权值可以死了。第二种就是树形dp。

#include<bits/stdc++.h>
using namespace std;
int const maxn = 100005;
struct edge
{
	int v,next,val;
}e[maxn];
bool vis[maxn];
int n,tot,ans,point;
int head[maxn];
void add(int a,int b,int w)//!类似与链式前向星的头部
{
	e[tot].v=a;
	e[tot].val=w;
	e[tot].next=head[b];
	head[b]=tot++;
}
void dfs(int u,int s)//!u表示这个点,x记录答案
{
	vis[u]=1;//!代表你已经来过了,不能重复
	if(s>ans)
	{
		ans=s;
		point=u;
	}
	for(int i=head[u];i>0;i=e[i].next)
	{
		int v=e[i].v;
		if(vis[v])//!不能走过
			continue;
		dfs(v,s+e[i].val);
	}
}
int main()
{
	tot=1;//!很重要,不加就会错
	int n;
	cin>>n;
	for(int i=1;i<n;i++)
	{
		int u, v, w;
		cin>>u>>v>>w;
		add(u,v,w);
		add(v,u,w);
	}
	ans=0;
	memset(vis,0,sizeof(vis));
	dfs(1,0);
	ans=0;
	memset(vis,0,sizeof(vis));
	dfs(point,0);
	cout<<ans<<endl;
	return 0;
}
树的重心
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e4+5;
const int INF=1e9+7;
int head[maxn<<1];
int s[maxn<<1],dp[maxn];
int cnt;
int N;
struct edge
{
    int next,to;
}e[maxn<<1];
void add(int u,int v)
{
    e[++cnt].to=v;
    e[cnt].next=head[u];
    head[u]=cnt;
}
void dfs(int x,int y)
{
    s[x]=1;
    for(int i=head[x];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(v==y) continue;
        dfs(v,x);
        s[x]+=s[v];
        dp[x]=max(dp[x],s[v]);
    }
}
int main()
{
    cnt=0;
    memset(head,-1,sizeof(head));
    scanf("%d",&N);
    for(int i=1;i<N;i++)
    {
        int u,v;
		scanf("%d%d",&u,&v);
        add(u,v);
        add(v,u);
    }
    dfs(1,0);
    int minn=INF,pos;
    for(int i=1;i<=N;i++)
    {
        int maxn=max(dp[i],N-s[i]);
        if(maxn<minn)
        {
            minn=maxn;
            pos=i;
        }
    }
    printf("%d\n",pos);
    return 0;
}

树上背包

背包我们很熟悉了,树上的呢,我们其实大概的东西不用变的,只要我们一个一个遍历每一个点即可。

树上删边

题目大意非常简单,就是要删多少个边才能保证每一颗子树的节点至少为 p p p​ 。

其实这道题你可以做一个反向思考,不要考虑怎么删边,而是考虑加边,因为加的边越多剪的边越少。

其他的我写在代码注释里面了。

#include <bits/stdc++.h>
using namespace std;
const int MAXN=10000;
const int INF=1e9;
vector<int>t[MAXN];
int n,p,dp[MAXN][MAXN];//dp[i][j]记录了以i为根节点的子树留下j个节点所要删去的最小边数
bool son[MAXN];
void dfs(int u, int fa) 
{
    dp[u][1]=t[u].size();//做一个初始化,就是说只留下一个点要删去其他所有的边,但是一般来说需要-1,也就是父亲不能剪,但是不知道为什么我的代码-1就不对。
    for(int i=0;i<t[u].size();i++) 
	{
        int v=t[u][i];
        if(v!=fa) 
		{
            dfs(v, u);
			for(int j=p;j>1;j--) 
			{
				for(int k=1;k<j;k++) 
				{
					dp[u][j]=min(dp[u][j],dp[u][j-k]+dp[v][k]-2);//这里如果你上面-1,这里也-1就够了,但是我的2不对,所以只能-2
				}
			}
        }
    }
}
int main() 
{
    cin>>n>>p;
    memset(son,false, sizeof(son));
    for(int i=0;i<n-1;i++) 
	{
        int u, v;
        cin>>u>>v;
        t[u].push_back(v);
        t[v].push_back(u);
        son[v]=true;//记录有没有父亲,找根的。
    }
    int root=1;
    while(son[root]) 找根
	{
        root++;
    }
    for(int i=1;i<=n;i++) 
	{
        for(int j=2;j<=p;j++) 
		{
            dp[i][j]=INF;
        }
    }
    dfs(root,0);
    int ans=INF;
    for(int i=1;i<=n;i++) 
	{
        ans=min(ans,dp[i][p]);
    }
    cout<<ans<<endl;
    return 0;
}
The more, The Better
#include<bits/stdc++.h>
using namespace std;
int dp[205][205],a[205],n,m;
vector<int> vec[205];
void dfs(int r)
{
    for(int i=0;i<vec[r].size();i++)//!每一个结点都要遍历
    {
        int x=vec[r][i];
        dfs(x);
        for(int j=m;j>1;j--)//!与背包相似
            for(int k=1;k<j;k++)//!略有不同,需要反过来
                dp[r][j]=max(dp[r][j],dp[r][j-k]+dp[x][k]);
    }
}
int main()
{
    while(cin>>n>>m,n||m)
    {
        memset(dp,0,sizeof dp);
        for(int i=0;i<=n;i++)
            vec[i].clear();
        for(int i=1,x;i<=n;i++)
        {
            cin>>x>>dp[i][1];
            vec[x].push_back(i);
        }
        m++;
        dfs(0);
        cout<<dp[0][m]<<endl;
    }
    return 0;
}

好了,总结就到这里,再见。

  • 27
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值