树形dp
树形dp
例题一 没有上司的舞会
做法
过于经典,不多赘述
#include<bits/stdc++.h>
using namespace std;
const int maxn=6*1e3+5;
int f[maxn][2],num[maxn],n,x,y,root;
vector<int>d[maxn];
void dfs(int now,int last){
f[now][0]=f[now][1]=0;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
dfs(Next,now);
f[now][0]+=max(f[Next][1],f[Next][0]);
f[now][1]+=f[Next][0];
}
f[now][1]+=num[now];
return ;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>num[i];
for(int i=1;i<n;i++){
cin>>x>>y;
root+=x;
d[y].push_back(x);
}
root=(1+n)*n/2-root;
dfs(root,root);
cout<<max(f[root][0],f[root][1]);
return 0;
}
例题二 最大利润
题目大意
John邀请了你在火车站开饭店,但不允许同时在两个相连接的火车站开。
任意两个火车站有且只有一条路径,每个火车站最多有50个和它相连接的火车站。
告诉你每个火车站的利润,问你可以获得的最大利润为多少。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int f[maxn][3],num[maxn],n,x,y;
vector<int>d[maxn];
void dfs(int now,int last){
f[now][0]=f[now][1]=0;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
dfs(Next,now);
f[now][0]+=max(f[Next][0],f[Next][1]);
f[now][1]+=f[Next][0];
}
f[now][1]+=num[now];
return ;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>num[i];
for(int i=1;i<n;i++){
cin>>x>>y;
d[x].push_back(y);
d[y].push_back(x);
}
dfs(1,1);
cout<<max(f[1][0],f[1][1]);
return 0;
}
例题三 P - Independent Set
题目大意
一棵树有n个结点,你要给每个点染上黑色或者白色,但是任意相邻两个结点不能同时染成黑色。
问有多少种不同的染色方案,答案模1000000007。
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5,p=1e9+7;
int n,x,y,f[maxn][3];
vector<int>d[maxn];
void dfs(int now,int last){
f[now][0]=f[now][1]=1;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
dfs(Next,now);
f[now][1]=(f[now][1]*f[Next][0])%p;
f[now][0]=f[now][0]*(f[Next][1]+f[Next][0])%p;
}
return ;
}
signed main(){
cin>>n;
for(int i=1;i<n;i++){
cin>>x>>y;
d[x].push_back(y);
d[y].push_back(x);
}
dfs(1,1);
cout<<(f[1][0]+f[1][1])%p;
return 0;
}
例题四 移动信号
做法
记f[now][0]为now建
记f[now][1]为now不建,没信号
记f[now][2]为now不建,有信号
考虑f[now][2],首先直接从min(f[next][0],f[next][2])
但有可能转移过来的全部都是f[next][2],此时now就没有信号
我们就要强制一个next将f[next][0]转移过来
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4+5,oo=1e9+7;
int f[maxn][3],n,x,y;
vector<int>d[maxn];
void dfs(int now,int last){
f[now][0]=1;
f[now][1]=0;
f[now][2]=0;
bool flag=false;
int sum=0;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
dfs(Next,now);
f[now][0]+=min(min(f[Next][2],f[Next][1]),f[Next][0]);
f[now][1]+=min(f[Next][0],f[Next][2]);
f[now][2]+=min(f[Next][2],f[Next][0]);
if(f[Next][2]>=f[Next][0])flag=true;
}
if(!flag)sum=f[now][2];
else return ;
f[now][2]=oo;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
f[now][2]=min(f[now][2],sum-f[Next][2]+f[Next][0]);
}
return ;
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
cin>>x>>y;
d[x].push_back(y);
d[y].push_back(x);
}
dfs(1,1);
// for(int i=1;i<=n;i++)cout<<i<<" "<<f[i][0]<<" "<<f[i][1]<<" "<<f[i][2]<<endl;
cout<<min(f[1][2],f[1][0]);
return 0;
}
例题五 选边
题目大意
一棵树有n个结点,编号1至n。有n-1条边,第i条边是:u[i],v[i],w[i]表示结点u[i]与结点v[i]有一条权值为w[i]的无向边。
你需要从这n-1条边当中选取若干条边(可以不选),使得被选中的边的权值权值总和最大,但是有一些限制:
对于第i个点来说,与点i相连的所有边当中最多只能有d[i]条边被选中。
做法
记f[now][0]表示在now子树符合规则的选边的最大权值和,其中now结点不与其父亲结点的连边。
记f[now][1]表示在now子树符合规则的选边的最大权值和,其中now结点与其父亲结点的连边,即now最多只能与d[now]-1个儿子连边。
先让 f[now][0] = sum(f[son][0],son是now儿子),然后尝试通过选中now-->son这条边,调整它的最大值,
因为可能存在儿子son,使得w[now][son] + f[son][1] > f[son][0],这样会使得f[now][0]增加w[now][son] + f[son][1] - f[son][0],我们称满足这些条件的
son叫做有用儿子,把w[now][son] + f[son][1] - f[son][0]叫做增大量。我们可以把所有这些有用儿子的增大量全部收集起来,保存在数组g[0...cnt-1],
然后对g从大到小排序,把最大的cnt的增加量累加到f[now][[0]即可得到最优的f[now][0]。
f[now][1]的值和f[now][0]几乎一样,只是有用儿子的增大量最多只能取cnt-1个增加量。
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=3*1e5+5;
struct edge{
int to,v;
};
bool cmp(int x,int y){
return x>y;
}
int f[maxn][3],g[maxn],cnt=0,a[maxn],n,x,y,z;
vector<edge>d[maxn];
void dfs(int now,int last){
f[now][0]=f[now][1]=0;
int sum=0;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i].to;
if(Next==last)continue;
dfs(Next,now);
sum+=f[Next][0];
}
int cnt=0;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i].to;
if(Next==last)continue;
if(a[Next]>0&&d[now][i].v+f[Next][1]>f[Next][0])g[++cnt]=d[now][i].v+f[Next][1]-f[Next][0];
}
sort(g+1,g+cnt+1,cmp);
f[now][0]=f[now][1]=sum;
for(int i=1;i<=min(cnt,a[now]);i++){
f[now][0]+=g[i];
if(i==a[now])continue;
else f[now][1]+=g[i];
}
return ;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<n;i++){
cin>>x>>y>>z;
d[x].push_back({y,z});
d[y].push_back({x,z});
}
dfs(1,1);
// for(int i=1;i<=n;i++)cout<<i<<" "<<f[i][0]<<" "<<f[i][1]<<endl;
cout<<f[1][0];
return 0;
}
例题六 交通违法
题目大意
禅城区有N条双向道路和N个路口,路口的编号从1至N。每条道路连接两个路口。
两个路口之间不会有重边。保证任意两个路口都是相互到达的。
现在觉得在一些路口装上摄像头,检测路面的违法情况。
装在路口的摄像头可以监测到所有连接到这个路口的道路。
现在的问题是:最少需要在多少个路口安装摄像头才能监测所有的道路?
做法
容易看出是基环树。
通过dfs1容易找出换上相邻的两个点X和Y。
那么为了使得X与Y之间道路被监控到,那么X要装摄像头或者Y要装摄像头(当然,两个都装也是可以的)。
假设X安装了摄像头,那么可以删掉X与Y之间的那条边,就构成一棵树了,从X开始DFS2,就是普通的树型DP了,此时最优解是f[X][1]。
假设Y安装了摄像头,那么可以删掉X与Y之间的那条边,就构成一棵树了,从Y开始DFS2,就是普通的树型DP了,此时最优解是f[Y][1]。
答案等于min(f[X][1],f[Y][1])
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
bool flag=true,vis[maxn]={false};
int st,en,cnt=0,n,m,x,f[maxn][3];
vector<int>d[maxn];
void dfs1(int now,int last){
if(!flag||vis[now]){
if(flag){
st=last;
en=now;
flag=false;
}
return ;
}
vis[now]=true;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
dfs1(Next,now);
}
vis[now]=false;
return ;
}
bool cmp(int A,int B){
return A==st&&B==en||A==en&&B==st;
}
void dfs2(int now,int last){
// printf("come to this:now is %d,last is %d\n",now,last);
f[now][0]=1;
f[now][1]=0;
// int minn=1e9+7;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last||cmp(Next,now))continue;
// printf("i is %d ,now is to Next:%d ,now is %d;last is %d\n",i,Next,now,last);
dfs2(Next,now);
f[now][0]+=min(f[Next][0],f[Next][1]);
f[now][1]+=f[Next][0];
// minn=min(minn,-min(f[Next][0],f[Next][1])+f[Next][1]);
}
// if(minn!=1e9+7)
// f[now][1]+=minn;
return ;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>m;
for(int j=1;j<=m;j++){
cin>>x;
d[i].push_back(x);
}
}
// for(int i=1;i<=n;i++){
// cin>>m>>x;
// d[m].push_back(x);
// d[x].push_back(m);
// }
dfs1(1,1);
// cout<<cmp(1,3)<<" "<<cmp(2,3)<<" "<<cmp(3,1)<<endl;
// cout<<st<<" "<<en<<endl;
dfs2(st,st);
int ans1=f[st][0];
// for(int i=1;i<=n;i++)cout<<i<<" "<<f[i][0]<<" "<<f[i][1]<<endl;
dfs2(en,en);
int ans2=f[en][0];
// for(int i=1;i<=n;i++)cout<<i<<" "<<f[i][0]<<" "<<f[i][1]<<endl;
cout<<min(ans1,ans2);
return 0;
}
树形背包合并
例一 选课
方法一:O(n*m^2)
//n*m^2
#include<bits/stdc++.h>
using namespace std;
const int maxn=305;
vector<int>d[maxn];
int n,m,x,y,f[maxn][maxn],a[maxn];
void dfs(int now,int last){
f[now][1]=a[now];
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
dfs(Next,now);
for(int j=m;j>=1;j--){
for(int k=1;k<=j;k++){
f[now][j]=max(f[now][j],f[now][k]+f[Next][j-k]);
}
}
}
return ;
}
int main(){
cin>>n>>m;
m++;
for(int i=1;i<=n;i++){
cin>>x>>y;
a[i]=y;
d[x].push_back(i);
}
dfs(0,0);
cout<<f[0][m];
return 0;
}
方法二:O(n*m)
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3+5;
vector<int>d[maxn];
int f[maxn][maxn];
int a[maxn];
int n,m,x,y;
int dfs(int now,int last){
f[now][1]=a[now];
int cnt=1;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
int cntv=dfs(Next,now);
for(int j=min(m,cnt+cntv);j>=1;j--){
for(int k=max(1,j-cntv);k<=min(j,cnt);k++){
f[now][j]=max(f[now][j],f[now][k]+f[Next][j-k]);
}
}
cnt+=cntv;
}
return cnt;
}
int main(){
cin>>n>>m;
m++;
for(int i=1;i<=n;i++){
cin>>x>>y;
a[i]=y;
d[x].push_back(i);
}
dfs(0,0);
cout<<f[0][m];
return 0;
}
例题二 第K大
题目大意
一棵n个结点的树,根是1号点,第i个结点的权值是x[i]。
有Q个问题,第i个问题的格式给出: V[i], K[i],表示询问以V[i]结点为根的子树内,权值第K[i]大的结点的权值是多少。
输入格式
第一行,n和Q。2<=n<=1e5, 1<=Q<=1e5。
第二行,n个整数,第i个整数是x[i], 0 <=x[i]<=1e9。
接下来有n-1行,第i行有两个整数:u[i]和v[i],表示结点u[i]和v[i]之间有一条边。
接下来有Q行,每行描述一个询问,第i行给出V[i]和K[i], 1<=V[i]<=n, 1<=K[i]<=20, 数据保证以V[i]为根的子树的结点总数不少于K[i]。
做法
归并排序
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
vector<int>d[maxn];
int f[maxn][25],a[maxn],q,n,x,y,tmp[25];
void dfs(int now,int last){
// cout<<now<<" "<<last<<endl;
f[now][1]=a[now];
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
dfs(Next,now);
int st1=1,st2=1;
for(int j=1;j<=20;j++){
if(f[now][st1]>f[Next][st2])tmp[j]=f[now][st1],st1++;
else tmp[j]=f[Next][st2],st2++;
}
for(int j=1;j<=20;j++)f[now][j]=tmp[j];
}
return ;
}
int main(){
// memset(f,0,sizeof f);
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
for(int i=1;i<n;i++){
scanf("%d%d",&x,&y);
d[x].push_back(y);
d[y].push_back(x);
}
dfs(1,1);
// for(int i=1;i<=n;i++){
// for(int j=1;j<=20;j++)cout<<f[i][j]<<" ";
// cout<<endl;
// }
while(q--){
scanf("%d%d",&x,&y);
printf("%d\n",f[x][y]);
}
return 0;
}
例题三 修复道路
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=155,maxm=155;
int f[maxn][maxm],siz[maxn],n,p,x,y;
vector<int>d[maxn];
void dfs(int now,int last){
siz[now]=1;
// f[now][0]=1;
// if(now==1)f[now][0]--;
f[now][1]=0;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
dfs(Next,now);
siz[now]+=siz[Next];
for(int j=min(p,siz[now]);j>=1;j--){
f[now][j]++;//每多一颗子树,想不变,要多砍一颗
for(int k=max(j-siz[Next],1);k<=min(siz[now],j);k++){
f[now][j]=min(f[now][j],f[Next][j-k]+f[now][k]);
}
}
}
// for(int j=min(p,siz[now]-1);j>=1;j--)f[now][j]++;
f[now][siz[now]]=0;
return ;
}
int main(){
cin>>n>>p;
memset(f,0x3f,sizeof f);
int root=(1+n)*n/2;
for(int i=1;i<n;i++){
cin>>x>>y;
d[x].push_back(y);
root-=y;
}
memset(f,0x3f,sizeof f);
dfs(root,root);
// for(int i=1;i<=n;i++){
// cout<<i<<" ";
// for(int j=0;j<=p;j++)cout<<f[i][j]<<" ";
// cout<<endl;
// }
int minn=1e9;
for(int i=1;i<=n;i++)minn=min(minn,f[i][p]+((i==root)?0:1));
cout<<minn;
return 0;
}
例题四 无向图染色
做法
因可能有多棵树,增加一个虚拟0号结点作为新树的根。
虚拟点的代价无穷大,保证虚拟点不会变成黑色开心结点
本题状态的描述很重要,优雅的分类会使得状态的设计以及状态转移简单很多。
f[i][j][0]表示以i结点为根的子树范围内,已经有j个黑色开心结点,且结点i是白色,的最少费用。
f[i][j][1]表示以i结点根的子树范围内,已经有j个黑色开心结点,且结点i是黑色开心结点,的最少费用。
一、f[i][j][0]的转移:
设结点i共有k个儿子,son[1]...son[k],前k-1个子树有x个黑色开心结点的最小费用记为g[k-1][x]。
可以用f[son[k]][y] + g[k-1][x] 去尝试更新 f[i][x+y][0]
g[k-1]如何求?类似背包合并
记i的前k-2子树有p个黑色开心结点的最小费用记为g[k-2][p]。
可以用 g[k-2][p] + f[son[k-1]][q]去更新 g[k-2][p+q]
二、f[i][j][1]的转移:
1、设结点i共有k个儿子,son[1]...son[k]。
(1)g[k-1][x][0]表示i的前k-1颗子树有x个黑色开心结点,且当前i结点是白色,的最小费用
(2)g[k-1][x][1]表示i的前k-1颗子树有x个黑色开心结点,且当前i已经是黑色开心结点,的最小费用
2、使用g[k-1][x][0]更新f[i][***][1]的情况:
(1)把结点i和结点son[k]一起染成黑色,那么可以用g[k-1][x][0] + f[son[k]][y][0] + money[i] + money[son[k]]
去尝试更新f[now][x+y+2][1],因为本轮染色增加了两个开心结点:结点i和结点son[k]。
(2)把结点i染成黑色,那么可以用g[k-1][x][0] + f[son[k]][y][1] + money[i]
去尝试更新f[now][x+y+1][1],因为本轮染色只增加了1个开心结点:结点i。结点son[k]已经早就是黑色开心结点了
3、使用g[k-1][x][1]更新f[i][***][1]的情况:
(1)使用g[k-1][x][1] + f[son[k]][y][0]去尝试更新f[i][x+y][1],即i的子结点son[k]是白色的情况
(2)使用g[k-1][x][1] + f[son[k]][y][0] + money[son[k]]去尝试更新f[i][x+y+1][1],即把原本是白色的son[k]现在染成黑色的情况
(3)使用g[k-1][x][1] + f[son[k]][y][1]尝试更新f[i][x+y][1]
4、如何计算 g[k][***][0]
(1)可以使用g[k-1][x][0]+f[son[k]][y][0]尝试更新g[k][x+y][0]
(2)可以使用g[k-1][x][0]+f[son[k]][y][1]尝试更新g[k][x+y][0]
5、如何计算 g[k][***][1]
(1) 使用g[k-1][x][1] + f[son[k]][y][0]去尝试更新f[i][x+y][1]
(2) 使用g[k-1][x][1] + f[son[k]][y][0] + money[son[k]]去尝试更新f[i][x+y+1][1],结点son[k]本次会变成黑色开心结点
(3) 使用g[k-1][x][1] + f[son[k]][y][1]去尝试更新f[i][x+y][1]
(4) 使用g[k-1][x][0] + f[son[k]][y][0] + money[i] + money[son[k]]去尝试更新g[k][x+y+2][1]。结点i和结点son[k]一起染成黑色,
因为本轮染色增加了两个开心结点:结点i和结点son[k]。
(5) 使用 g[k-1][x][0] + f[son[k]][y][1] + money[i] 去尝试更新g[k][x+y+1][1]。
因为本轮把结点i染成黑色,为本轮染色只增加了1个开心结点:结点i。
求出f[root][0...n][0]之后,需要回答询问。
注意:答案可能不连续,例如:只有1个黑色开心是不可行,
但是两个黑色开心结点可能是可行的,3个黑色可能是不行的,但4个可以。
而且f[root][i][0]没有单调性,例如可能f[root][3][0] > f[root][4][0]
例如下图,假如是两棵树的森林,如果想要3个黑色开心结点,只能是取{1,2,3},有可能cost[3]非常非常大。
但是如果想要4个黑色开心结点,可以只取{1,2,4,5},可能这4个结点的cost很小。
解决方法一:对于询问Ai,暴力找一个最大的下标i,使得f[root][i][0] <= Ai即可。这方法较慢,需要扫描一遍。
解决方法二:维护单调栈,假如3个黑色开心点需要100元,而4个黑色开心结点只需要50元,那么3个黑色开心点没必要保留。
然后就可以二分答案回答询问了。
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e3+5;
bool vis[maxn];
vector<int>d[maxn];
int f[maxn][maxn][3],a[maxn],siz[maxn],n,m,x,y,q,tmp[maxn][3],B[maxn],A[maxn];
void dfs1(int now,int last){
vis[now]=true;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
dfs1(Next,now);
}
return ;
}
void MIN(int a,int &b){
if(a<b)b=a;
return ;
}
void dfs(int now,int last){
f[now][0][0]=0;
siz[now]=1;
// printf("into %d\n",now);
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
dfs(Next,now);
// printf("now is %d ,next is %d\n",now,Next);
for(int j=0;j<=siz[now];j++){
tmp[j][0]=f[now][j][0];
tmp[j][1]=f[now][j][1];
}
for(int j=0;j<=siz[now];j++){
for(int k=0;k<=siz[Next];k++){
MIN(tmp[j][0]+f[Next][k][0],f[now][j+k][0]);
MIN(tmp[j][0]+f[Next][k][1],f[now][j+k][0]);
MIN(tmp[j][1]+f[Next][k][1],f[now][j+k][1]);
MIN(tmp[j][1]+f[Next][k][0],f[now][j+k][1]);
MIN(tmp[j][0]+f[Next][k][0]+a[Next]+a[now],f[now][j+k+2][1]);
MIN(tmp[j][1]+f[Next][k][0]+a[Next],f[now][j+k+1][1]);
MIN(tmp[j][0]+f[Next][k][1]+a[now],f[now][j+k+1][1]);
}
}
siz[now]+=siz[Next];
}
return ;
}
signed main(){
scanf("%lld%lld",&n,&m);
memset(f,0x3f,sizeof f);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int i=1;i<=m;i++){
scanf("%lld%lld",&x,&y);
d[x].push_back(y);
d[y].push_back(x);
}
for(int i=1;i<=n;i++){
if(vis[i])continue;
dfs1(i,0);
d[0].push_back(i);
d[i].push_back(0);
}
a[0]=1e17;
dfs(0,0);
// for(int i=0;i<=n;i++){
//// cout<<i<<" "<<siz[i]<<endl;
// for(int j=0;j<=siz[i];j++){
// cout<<i<<" "<<j<<" "<<f[i][j][0]<<" "<<f[i][j][1]<<endl;
//
// }
// }
// cout<<"af";
int Top=0;
for(int i=0;i<=siz[0];i++){
// cout<<i<<"ans"<<f[0][i][0]<<"\n";
if(f[0][i][0]>455743088879883039)continue;
while(Top>0){
if(B[Top]>f[0][i][0])Top--;
else break;
}
B[++Top]=f[0][i][0];
A[Top]=i;
// for(int j=0;j<=Top;j++)cout<<A[j]<<" "<<B[j]<<endl;
// cout<<endl;
}
scanf("%lld",&q);
while(q--){
scanf("%lld",&x);
int l=0,r=Top+1;
while(l+1<r){
int mid=(l+r)/2;
if(B[mid]<=x)l=mid;
else r=mid;
}
printf("%lld\n",A[l]);
}
return 0;
}
换根dp
例题一 大集会
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
int siz[maxn],a[maxn],f[maxn],tot_size,minn=1e17,n,x,y,z;
struct node{
int to,v;
};
vector<node>d[maxn];
void dfs(int now,int last,int length){
siz[now]=a[now];
f[1]+=length*a[now];
for(int i=0;i<d[now].size();i++){
int Next=d[now][i].to;
if(Next==last)continue;
dfs(Next,now,length+d[now][i].v);
siz[now]+=siz[Next];
}
return ;
}
void dfs2(int now,int last,int path){
f[now]=f[last]+path*tot_size-2*path*siz[now];
minn=min(minn,f[now]);
for(int i=0;i<d[now].size();i++){
int Next=d[now][i].to;
if(Next==last)continue;
dfs2(Next,now,d[now][i].v);
}
return ;
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<n;i++){
cin>>x>>y>>z;
d[x].push_back({y,z});
d[y].push_back({x,z});
}
dfs(1,1,0);
// cout<<"af";
tot_size=siz[1];
dfs2(1,1,0);
cout<<minn;
return 0;
}
例题二 蚂蚁聚会
题目大意
有n个蚁巢,这n个蚁巢形成一颗树形结构,第i个蚁巢有a[i]只蚂蚁。现在蚂蚁们想举行一个大型的聚会。
但是这些蚂蚁比较懒惰,都不想走太远,每只蚂蚁最多只愿意走X步(每一步就是走一条边)。
它们要计算:如果选择第i个蚁巢作为举行聚会的地点,可以有多少只蚂蚁参加聚会?记该数量为p[i]。
你的任务就是帮助计算: p[1]、p[2]、p[3]、....p[n]。
做法
换根DP
一、定义f[i][j]表示i子树内,跟i结点之间有j条的所有结点上的所有蚂蚁的总数量。
先选1号点为根,dfs1(1,-1)一遍,可以求出f数组,时间复杂度O(nx)
二、定义g[i][j]表示若以i根整棵树的根,跟i结点之间有j条的所有结点上的所有蚂蚁的总数量。
显然g[1][0...x] = f[1][0...x]
若2是1号结点的儿子结点,如何由g[1][0...x]推出g[2][0...x]?
如何求g[2][i]?
首先在2子树内,走i步到达2号点的蚂蚁,肯定应该算到g[2][i],故先赋值g[2][i] = f[2][i]
还有一部分蚂蚁是从1号结点向下走到2号点的,是哪些蚂蚁呢?
1、这些蚂蚁不在2号子树内
2、这些蚂蚁是走i-1步到达1号点,再从1号点走一步到达2号点的
3、这部分蚂蚁数量等于g[1][i-1] - f[2][i-2]
4、故g[2][i] = f[2][i] + (g[1][i-1] - f[2][i-2])
5、边界处理。g[2][0] = a[2], g[2][1] = f[2][1] + g[2][0]
6、由g[1][0...x]计算完g[2][0...x],然后再递归2号点,所以遇到叶子结点时,可以直接返回了
三、求答案
对于每个点i,输出sum(g[i][0...x])即可。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int g[maxn][25],f[maxn][25],a[maxn];
vector<int>d[maxn];
int n,x,y,X;
void dfs1(int now,int last){
g[now][0]=a[now];
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
dfs1(Next,now);
for(int i=1;i<=X;i++)
g[now][i]+=g[Next][i-1];
}
return ;
}
void dfs2(int now,int last){
f[now][0]=a[now];
if(now!=1)
for(int i=1;i<=X;i++)f[now][i]=f[last][i-1]+g[now][i]-((i==1)?0:g[now][i-2]);
for(int i=0;i<d[now].size();i++){
int Next=d[now][i];
if(Next==last)continue;
dfs2(Next,now);
}
return ;
}
int main(){
scanf("%d%d",&n,&X);
for(int i=1;i<n;i++){
scanf("%d%d",&x,&y);
d[x].push_back(y);
d[y].push_back(x);
}
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
dfs1(1,1);
for(int i=0;i<=X;i++)f[1][i]+=g[1][i];
dfs2(1,1);
// for(int i=1;i<=n;i++){
// for(int j=0;j<=X;j++)cout<<g[i][j]<<" ";
// cout<<endl;
// }
// cout<<endl;
for(int i=1;i<=n;i++){
int ans=0;
for(int j=0;j<=X;j++)ans+=f[i][j];
printf("%d\n",ans);
}
return 0;
}
例题三 回家
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=5*1e5+5;
struct node{
int to,v;
};
int g[maxn][3],a[maxn],b[maxn],tot,n,K,x,y,z;
vector<node>d[maxn];
void MAX(int &a,int b){
if(b>a)a=b;
return ;
}
void dfs1(int now,int last){
g[now][0]=g[now][1]=0;
b[now]=a[now];
for(int i=0;i<d[now].size();i++){
int Next=d[now][i].to;
if(Next==last)continue;
dfs1(Next,now);
b[now]+=b[Next];
if(b[Next]!=0){
tot+=d[now][i].v*2;
if(g[Next][0]+d[now][i].v>g[now][1])g[now][1]=g[Next][0]+d[now][i].v;
if(g[now][1]>g[now][0])swap(g[now][1],g[now][0]);
}
}
// if(d[now].size()==1&&now!=1)g[now][0]=0;
return ;
}
void dfs2(int now,int last){
for(int i=0;i<d[now].size();i++){
int Next=d[now][i].to;
if(Next==last)continue;
if(b[Next]!=0){
if(g[Next][0]+d[now][i].v==g[now][0])MAX(g[Next][1],g[now][1]+d[now][i].v);
else MAX(g[Next][1],g[now][0]+d[now][i].v);
if(g[Next][1]>g[Next][0])swap(g[Next][1],g[Next][0]);
}
else g[Next][0]=g[now][0]-d[now][i].v;
dfs2(Next,now);
}
return ;
}
signed main(){
scanf("%lld%lld",&n,&K);
for(int i=1;i<n;i++){
scanf("%lld%lld%lld",&x,&y,&z);
d[x].push_back({y,z});
d[y].push_back({x,z});
}
for(int i=1;i<=K;i++){
scanf("%lld",&x);
a[x]=1;
}
dfs1(1,1);
dfs2(1,1);
// cout<<tot<<endl;
for(int i=1;i<=n;i++)printf("%lld\n",tot-g[i][0]);
return 0;
}
例题四 巡逻
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
struct node{
int to,v;
};
vector<node>d[maxn];
int vis[maxn],dp[maxn][3],maxx=0,maxxi=0,n,K,x,y,st,en;
void MAX(int &a,int b){
if(a<b)a=b;
return ;
}
void dfs1(int now,int last){
for(int i=0;i<d[now].size();i++){
int Next=d[now][i].to;
if(Next==last||vis[Next])continue;
vis[Next]=vis[now]+d[now][i].v;
if(vis[Next]>maxx)
maxx=vis[Next],maxxi=Next;
dfs1(Next,now);
}
return ;
}
void dfs2(int now,int last){
for(int i=0;i<d[now].size();i++){
int Next=d[now][i].to;
if(vis[Next]!=vis[now]-1)continue;
d[now][i].v=-1;
dfs2(Next,now);
return ;
}
}
void dfs3(int now,int last){
dp[now][0]=0,dp[now][1]=0;
for(int i=0;i<d[now].size();i++){
int Next=d[now][i].to;
if(Next==last)continue;
dfs3(Next,now);
MAX(dp[now][1],dp[Next][0]+d[now][i].v);
if(dp[now][1]>dp[now][0])swap(dp[now][0],dp[now][1]);
}
return ;
}
int main(){
scanf("%d%d",&n,&K);
for(int i=1;i<n;i++){
scanf("%d%d",&x,&y);
d[x].push_back({y,1});
d[y].push_back({x,1});
}
memset(vis,0,sizeof vis);
vis[1]=1;
dfs1(1,1);
st=maxxi;
maxx=0;
memset(vis,0,sizeof vis);
vis[maxxi]=1;
dfs1(maxxi,maxxi);
int ans=maxx-1;
if(K==1){
printf("%d\n",2*(n-1)-ans+1);
return 0;
}
en=maxxi;
dfs2(en,en);
memset(dp,0,sizeof dp);
dfs3(en,en);
int ans2=0;
for(int i=1;i<=n;i++)
MAX(ans2,dp[i][0]+dp[i][1]);
printf("%d\n",2*(n-1)-ans-ans2+2);
return 0;
}
写在最后
1、换根dp一般使用两次dfs
2、树形dp的背包先把不优化的打出来,然后看now与next的范围
3、更新值使用这种方式:
void MAX(int &a,int b){
if(a<b)a=b;
return ;
}

浙公网安备 33010602011771号