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;
}
好了,总结就到这里,再见。