树上dp_学习笔记
1.AcWing.1073 树的中心(换根dp法):
题意:给你一个树,包含n个结点,和n-1条带权无向边。请你在树中找到一个点,使得该点到树中其他结点的最远距离最近,结果输出所求点到树中其他结点的最远距离。
解法:首先分析题目,其实是要我们把每一个点到其他点的最长距离求出来,再求一个其中最短的就可以了,我们来分析一下每一个点可以再树上怎么走,其实就是向上和向下走。我们用d1[u]表示u结点向下走的最长路径,d2[u]表示u结点向下走的次长路径,p1[u]表示u结点向下走最长路径的儿子节点,up[u]表示u节点向上走的最长路径。
以求得向下走的最长路径d1[u]和次长路径d2[u]。那么问题的关键就在于求向上走的最长路径up[u]了。
这个时候我们把结点v当做当前结点,分析其父亲结点u。如果v是u的p1结点,那么up[v]=max(up[u],d2[u])+edge[i].w;如果v不是u的p1结点,那么up[v]=max(up[u],d1[u])+edge[i].w。另外要注意看自己是不是叶结点(即有没有向下的孩子,如果没有的话d1=d2=0)
#include<bits/stdc++.h> #pragma GCC optimize(2) #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int head[maxn],tot; struct E{ int to,next,w; }edge[maxn<<1]; void add(int u,int v,int w){ edge[tot].to=v; edge[tot].w=w; edge[tot].next=head[u]; head[u]=tot++; } bool is_leaf[maxn]; int n,p1[maxn],d1[maxn],d2[maxn],up[maxn]; int dfs_d(int u,int fa){ d1[u]=d2[u]=-INF; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; int d=dfs_d(v,u)+edge[i].w; if(d>=d1[u]){ d2[u]=d1[u];d1[u]=d; p1[u]=v; } else if(d>d2[u]) d2[u]=d; } if(d1[u]==-INF){ d1[u]=d2[u]=0; is_leaf[u]=true; } return d1[u]; } void dfs_up(int u,int fa){ for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; if(p1[u]==v){ up[v]=max(d2[u],up[u])+edge[i].w; } else up[v]=max(up[u],d1[u])+edge[i].w; dfs_up(v,u); } } int main(){ scanf("%d",&n);mem(head,-1); rep(i,1,n-1){ int u,v,w;scanf("%d%d%d",&u,&v,&w); add(u,v,w);add(v,u,w); } dfs_d(1,-1); dfs_up(1,-1); int res=d1[1]; for(int i=2;i<=n;i++){ if(is_leaf[i]) res=min(res,up[i]); else res=min(res,max(up[i],d1[i])); } cout<<res<<endl; }
2.AcWing.1074 二叉苹果树:
题意:给你一个二叉树,n个点,m个要保留的路径数,然后有n-1条带权无向边。然后需要进行删边,但是最后保留边必须与1联通,问所保留的边权值和最大。
题解:树上有依赖性的背包dp问题。
状态定义:dp[i][j]表示当前结点为i作为root的子树中,保留j条边的最大苹果数量和。
状态转移:dp[node][j]=max(dp[node][j], dp[node][j-k-1]+dp[sub_node][k]+w[sub_node])
#include<bits/stdc++.h> #pragma GCC optimize(2) #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int tot,head[maxn]; struct E{ int to,next,w; }edge[maxn<<1]; void add(int u,int v,int w){ edge[tot].to=v; edge[tot].w=w; edge[tot].next=head[u]; head[u]=tot++; } int n,m,dp[110][110]; void dfs(int u,int fa){ for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to,w=edge[i].w; if(v==fa) continue; dfs(v,u); for(int j=m;j;j--){ for(int k=0;k+1<=j;k++){ dp[u][j]=max(dp[u][j],dp[u][j-k-1]+dp[v][k]+w); } } } } int main(){ scanf("%d%d",&n,&m);mem(head,-1); rep(i,1,n-1){ int u,v,w;scanf("%d%d%d",&u,&v,&w); add(u,v,w);add(v,u,w); } dfs(1,-1); printf("%d\n",dp[1][m]); }
3.AcWing.285 没有上司的舞会
题意:给你n个点,n-1条边(u,v,其中v是u的父亲)。每个点都有自己的权值w[i],每条边最多只能选1个点,问最终选择完的最大权值和是多少
题解:树上dp。
状态定义:dp[i][0]表示以当前结点i作为root的子树中,不选择i;dp[i][1]表示以当前结点i作为root的子树中,选择i
状态转移:
#include<bits/stdc++.h> #pragma GCC optimize(2) #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int n,w[maxn],fa[maxn],root; int dp[maxn][2]; void dfs(int x){ dp[x][1]=w[x]; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; dfs(v); dp[x][0]+=max(dp[v][0],dp[v][1]); dp[x][1]+=dp[v][0]; } } int main(){ scanf("%d",&n);mem(head,-1); rep(i,1,n) scanf("%d",&w[i]); rep(i,1,n-1){ int u,v;scanf("%d%d",&u,&v); add(v,u);fa[u]=v; } root=1; while(fa[root]) root=fa[root]; dfs(root); cout<<max(dp[root][0],dp[root][1])<<endl; }
4.AcWing.323 战略游戏
题意:给你n个点(编号为0 ~ n-1),每条边至少选1个点,问最少能选几个点?
题解:树上dp
状态定义:dp[i][0]表示以当前结点i作为root的子树中,不选择i;dp[i][1]表示以当前结点i作为root的子树中,选择i
状态转移:
#include<bits/stdc++.h> #pragma GCC optimize(2) #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1500+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int n,fa[maxn]; int dp[maxn][2]; void dfs(int x){ dp[x][1]=1; dp[x][0]=0; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; dfs(v); dp[x][0]+=dp[v][1]; dp[x][1]+=min(dp[v][1],dp[v][0]); } } int main(){ while(~scanf("%d",&n)){ mem(head,-1);mem(fa,0);mem(dp,0);tot=0; rep(i,1,n){ int x,t;scanf("%d:(%d)",&x,&t); ++x; rep(i,1,t){ int qwq;scanf("%d",&qwq); ++qwq; fa[qwq]=x;add(x,qwq); } } int root=1; while(fa[root]) root=fa[root]; dfs(root); printf("%d\n",min(dp[root][1],dp[root][0])); } }
5.AcWing 1077皇宫看守
题意:给你n个点,n-1条边,一个点可以看守与其相邻的点。每个点有权值,问最小花费
题解:
#include<bits/stdc++.h> #pragma GCC optimize(2) #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1500+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int n,fa[maxn],w[maxn]; int dp[maxn][3]; void dfs(int x){ dp[x][2]=w[x]; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; dfs(v); dp[x][0]+=min(dp[v][1],dp[v][2]); dp[x][2]+=min({dp[v][0],dp[v][1],dp[v][2]}); } dp[x][1]=INF; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; dp[x][1]=min(dp[x][1],dp[x][0]-min(dp[v][1],dp[v][2])+dp[v][2]); } } int main(){ int n;scanf("%d",&n);mem(head,-1); rep(i,1,n){ int id,cost;scanf("%d%d",&id,&cost); w[id]=cost; int t;scanf("%d",&t); while(t--){ int v;scanf("%d",&v); fa[v]=id;add(id,v); } } int root=1; while(fa[root]) root=fa[root]; dfs(root); cout<<min(dp[root][1],dp[root][2])<<endl; }
6.AcWing 287积蓄程度(换根dp)
题意:给你n个点的树,每条边有流量限制,问以其中一个点最为root的最大流流量。
题解:换根dp的处理方法就是二次扫描,第一次dfs_d扫描处理以每个点作为根,其子树对其造成的影响
第二次扫描运用dp的处理,把父节点减去该子节点的信息,和该子节点作为根其子树的信息联立最后确认该子节点的dp信息。
该题注意特判节点indeg为1的情况。我们先用第一次dfs_d扫描,回溯自下而上得出每个点i的子树对于该点的最大流量贡献d[i]。然后自然而然,dp[root]=d[root],以此为起始,再做一次dfs自上而下,dp[v]=d[v]+min(w,dp[u]-min(d[v],w) ), 当然要考虑父节点如果作为root或者说其indeg=1,那么dp[v]=d[v]+w了.
#include<bits/stdc++.h> #pragma GCC optimize(2) #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=2e5+5; int tot,head[maxn]; struct E{ int to,next,w; }edge[maxn<<1]; void add(int u,int v,int w){ edge[tot].to=v; edge[tot].w=w; edge[tot].next=head[u]; head[u]=tot++; } int d[maxn],dp[maxn],indeg[maxn],root; int dfs_d(int x,int fa){ d[x]=0; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to,w=edge[i].w; if(v==fa) continue; d[x]+=min(w,dfs_d(v,x)); } if(indeg[x]!=1) return d[x]; else return INF; } void dfs(int x,int fa){ for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to,w=edge[i].w; if(v==fa) continue; if(indeg[x]==1&&x==root)dp[v]=d[v]+w; else dp[v]=d[v]+min(w,dp[x]-min(d[v],w)); dfs(v,x); } } int main(){ int T;scanf("%d",&T); while(T--){ int n;scanf("%d",&n);mem(head,-1);tot=0; mem(indeg,0);mem(d,0);mem(dp,0); rep(i,1,n-1){ int u,v,w;scanf("%d%d%d",&u,&v,&w); add(u,v,w);add(v,u,w); ++indeg[u],++indeg[v]; } root=1; dfs_d(root,-1); dp[root]=d[root]; dfs(root,-1); int ans=0; rep(i,1,n) ans=max(ans,dp[i]); cout<<ans<<endl; } } /* 1 4 1 2 5 2 3 2 2 4 2 */
7.洛谷P3478【POI2008】 STA-Station(简单换根dp)
题意:给定一个 nn 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。一个结点的深度之定义为该节点到根的简单路径上边的数量。
题解:以1作为root点,第一次dfs扫描,自下而上存子树上每个点到子树的根i的贡献。第二次扫描自上而下更新儿子节点v的dp值。
状态转移方程为:dp[v]=d[v]+(dp[x]-d[v]-siz[v])+(n-siz[v])
#include<bits/stdc++.h> #define rep(i,a,b) for(int i=a;i<=b;i++) #define mem(a,b) memset(a,b,sizeof(a)) #define endl '\n' #define ll long long using namespace std; const int maxn=1e6+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int n,d[maxn],siz[maxn]; void dfs_d(int x,int fa){ siz[x]=1; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; dfs_d(v,x); siz[x]+=siz[v]; d[x]+=d[v]+siz[v]; } } ll dp[maxn]; void dfs(int x,int fa){ for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; dp[v]=d[v]+(dp[x]-d[v]-siz[v])+(n-siz[v]); dfs(v,x); } } int main(){ scanf("%d",&n);mem(head,-1); rep(i,1,n-1){ int u,v;scanf("%d%d",&u,&v); add(u,v);add(v,u); } dfs_d(1,-1); dp[1]=d[1]; dfs(1,-1); ll ans=0; int num; rep(i,1,n){ ans=max(ans,dp[i]); if(dp[i]==ans) num=i; } cout<<num<<endl; }
8.洛谷P2014[CTSC1997] 选课(可依赖性树上背包dp)
题意:给你n个点,m组依赖关系,和每个点的权值,问可依赖情况最大和
题解:
#include<bits/stdc++.h> #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int n,m,s[maxn],dp[305][305]; void dfs(int u,int fa){ for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to,w=s[v]; if(v==fa) continue; dfs(v,u); for(int j=m;j>0;j--){ for(int k=0;k<j;k++){ dp[u][j]=max(dp[u][j],dp[u][j-k-1]+dp[v][k]+w); } } } } int main(){ scanf("%d%d",&n,&m);mem(head,-1); rep(i,1,n){ int f,wt;scanf("%d%d",&f,&wt); add(f,i);s[i]=wt; } s[0]=0; dfs(0,-1); cout<<dp[0][m]<<endl; }
9.洛谷P1613【跑路】(Floyd+倍增)
题解:用一个bool数组g[x][y][z]来记录x到y是否存在长度为2^z的路程,若存在,则x到y的时间(距离)为1秒,以此建图,求最短路。若x到t存在长度为2^(z-1)的路径,t到y存在长度为2^(z-1)的路径,那么x到y存在长度为2^z的路径。具体实现见代码及注释。
#include<bits/stdc++.h> #pragma GCC optimize(2) #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=55+5; bool g[maxn][maxn][25]; int d[maxn][maxn]; int n,m; int main(){ scanf("%d%d",&n,&m); mem(d,INF); rep(i,1,n) d[i][i]=0; rep(i,1,m){ int u,v;scanf("%d%d",&u,&v); g[u][v][0]=true; d[u][v]=1; } for(int i=1;i<=20;i++){ for(int u=1;u<=n;u++){ for(int v=1;v<=n;v++){ for(int k=1;k<=n;k++){ if(g[u][v][i-1]&&g[v][k][i-1]){ d[u][k]=1; g[u][k][i]=true; } } } } } for(int k=1;k<=n;k++){ for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ if(d[i][j]>d[i][k]+d[k][j]){ d[i][j]=d[i][k]+d[k][j]; } } } } cout<<d[1][n]<<endl; }
*10.洛谷P3177【HAOI2015树上染色】 (树上背包)
题解:dp[x][i]表示以x为根的子树中有i个黑色点的情况。那么可想而知,其黑色点的数目的必定范围必定符合[ 0 , siz[x] ]的区间,所以这个时候利用01背包的性质,其儿子节点为v,儿子节点为根的黑色点数量为l,那么状态转移方程为:dp[x][j] = max (dp[x][j],dp[x][j-l] + dp[v][l] +该边的贡献)。然后我看了一下前面的几道树上背包dp,第一维for循环枚举都是从大到小枚举,第二维for循环枚举大小无所谓,但是这道题来说,从小到大就TLE,从大到小就AC,我也不太清楚原理。最后其实无所谓以哪个节点作为root,那么就假定root=1吧,因为不管root为哪个节点,答案最终都是一样的。
#include<bits/stdc++.h> #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=2000+5; int tot,head[maxn]; struct E{ int to,next,w; }edge[maxn<<1]; void add(int u,int v,int w){ edge[tot].to=v; edge[tot].w=w; edge[tot].next=head[u]; head[u]=tot++; } int n,k,siz[maxn]; ll dp[maxn][maxn]; void dfs(int x,int fa){ dp[x][0]=dp[x][1]=0;siz[x]=1; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; dfs(v,x); siz[x]+=siz[v]; for(int j=min(k,siz[x]);j>=0;j--){ if(dp[x][j]!=-1) dp[x][j]+=dp[v][0]+(ll)siz[v]*(n-k-siz[v])*edge[i].w; for(int l=min(j,siz[v]);l>0;l--){ if(dp[x][j-l]==-1)continue; ll val=(ll)(l*(k-l)+(siz[v]-l)*(n-k-siz[v]+l))*edge[i].w; dp[x][j]=max(dp[x][j],dp[x][j-l]+dp[v][l]+val); } } } } int main(){ scanf("%d%d",&n,&k);mem(head,-1);mem(dp,-1); rep(i,1,n-1){ int u,v,w;scanf("%d%d%d",&u,&v,&w); add(u,v,w);add(v,u,w); } dfs(1,-1); cout<<dp[1][k]<<endl; }
11.洛谷P1270【“访问”美术馆】 (树上依赖性背包dp)
题解:这个题读入用递归读入(学了一手)。dp[i][j]表示以i为子树的根,花费j时间所能得到的最多画的数量。
那么状态转移方程就是 dp[u][i] = max(dp[u][i] , dp[2*u][j]+dp[2*u+1][i-cost[u]-j] ); 其实说通俗点就是 当父亲结点u的时间固定为i,减去那个路费,然后枚举0~i作为左边,剩下作为右边的情况的最大值
#include<bits/stdc++.h> #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int t,cost[maxn],val[maxn],dp[1005][2005]; void input(int u){ int x,y;scanf("%d%d",&x,&y); cost[u]=2*x;val[u]=y; if(!val[u]){ input(2*u); input(2*u+1); } } void dfs(int u){ if(val[u]){ for(int i=0;i/5<=val[u]&&cost[u]+i<=t;i++){ dp[u][cost[u]+i]=i/5; } return ; } dfs(2*u); dfs(2*u+1); for(int i=cost[u];i<=t;i++){ for(int j=0;j+cost[u]<=i;j++){ dp[u][i]=max(dp[u][i],dp[2*u][j]+dp[2*u+1][i-cost[u]-j]); } } } int main(){ scanf("%d",&t);t-=1; input(1); dfs(1); cout<<dp[1][t]<<endl; }
12.洛谷P1131 【时态同步】(树上一维dp)
题解:从底向根不断更新,对于每个点,找出其子结点最大距离,然后用最大距离减去其他子节点距离。然后你需要从底下往上更新这样,因为更新的边越靠近root,那么花费的的代价就越少,就是一个自下而上的回溯过程。状态转移方程是:dp[x] += dp[v] + (dis[x]-(dis[v]+edge[i].w));
#include<bits/stdc++.h> #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e6+5; int tot,head[maxn]; struct E{ int to,next,w; }edge[maxn<<1]; void add(int u,int v,int w){ edge[tot].to=v; edge[tot].w=w; edge[tot].next=head[u]; head[u]=tot++; } int n,root;ll dp[maxn],dis[maxn]; void dfs(int x,int fa){ for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; dfs(v,x); dis[x]=max(dis[x],dis[v]+edge[i].w); } for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; dp[x]+=dp[v]+(dis[x]-(dis[v]+edge[i].w)); } } int main(){ scanf("%d%d",&n,&root);mem(head,-1); rep(i,1,n-1){ int u,v,w;scanf("%d%d%d",&u,&v,&w); add(u,v,w);add(v,u,w); } dfs(root,-1); cout<<dp[root]<<endl; }
13.洛谷P2986 【USACO10MAR Great Cow Gathering G】(简易换根dp+二次扫描dfs)
题解:不知道为什么看到这个题直接脑海里就蹦出换根dp的做法,d[u]表示以u为根的子树中每个点到其的贡献和,up[u]表示u的祖辈们对u的贡献和
然后大概很容易能推出状态转移的 dp方程: up[v]=up[u]+(T-siz[u])*edge[i].w+d[u]-d[v]-siz[v]*edge[i].w+(siz[u]-siz[v])*edge[i].w;
最后就是 ans=min(ans,up[i]+d[i])。注意开long long!
#include<bits/stdc++.h> #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int tot,head[maxn]; struct E{ int to,next;ll w; }edge[maxn<<1]; void add(int u,int v,ll w){ edge[tot].to=v; edge[tot].w=w; edge[tot].next=head[u]; head[u]=tot++; } bool is_leaf[maxn]; int n,a[maxn],siz[maxn]; ll d[maxn],up[maxn],T; void init(int u,int fa){ siz[u]=a[u]; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; init(v,u); siz[u]+=siz[v]; } } ll dfs_d(int u,int fa){ d[u]=0; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; d[u]+=dfs_d(v,u)+siz[v]*edge[i].w; } return d[u]; } void dfs_up(int u,int fa){ for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; up[v]=up[u]+(T-siz[u])*edge[i].w+d[u]-d[v]-siz[v]*edge[i].w+(siz[u]-siz[v])*edge[i].w; dfs_up(v,u); } } int main(){ scanf("%d",&n);mem(head,-1); rep(i,1,n) scanf("%d",&a[i]); rep(i,1,n-1){ int u,v;ll w;scanf("%d%d%lld",&u,&v,&w); add(u,v,w);add(v,u,w); } init(1,-1); T=siz[1]; dfs_d(1,-1); up[1]=0; dfs_up(1,-1); ll ans=inf; rep(i,1,n){ ans=min(ans,d[i]+up[i]); } cout<<ans<<endl; }
14.洛谷P2279【HNOI2003】消防局的设立(树上状态机dp)
题解:https://www.luogu.com.cn/blog/rickole/solution-p2279 这个博主写的很好很清楚
大概就是设立5个状态。我发现这种树上状态机dp转移题通常涉及的关系无非是当前结点u和儿子节点v直接的直接关系,通过状态机的不同state之间进行状态的合理转移,我打算写一篇专门针对树上状态机dp转移的一篇总结性的题解。
#include<bits/stdc++.h> #pragma GCC optimize(2) #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int dp[maxn][5]; void dfs(int x,int fa){ dp[x][0]=1;dp[x][3]=0;dp[x][4]=0; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; dfs(v,x); dp[x][0]+=dp[v][4]; dp[x][3]+=dp[v][2]; dp[x][4]+=dp[v][3]; } if(head[x]==-1){ dp[x][1]=dp[x][2]=1; } else{ dp[x][1]=dp[x][2]=INF; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; int F1=dp[v][0]; int F2=dp[v][1]; for(int j=head[x];j!=-1;j=edge[j].next){ int t=edge[j].to; if(t==fa||t==v) continue; F1+=dp[t][3]; F2+=dp[t][2]; } dp[x][1]=min(dp[x][1],F1); dp[x][2]=min(dp[x][2],F2); } } for(int i=1;i<=4;i++){ dp[x][i]=min(dp[x][i],dp[x][i-1]); } } int main(){ int n;cin>>n;mem(head,-1); rep(i,2,n){ int x;cin>>x; add(x,i); } dfs(1,-1); cout<<dp[1][2]<<endl; }
15.洛谷P3047【USACO12FEB】 Nearby Cow G(换根dp二次扫描+容斥原理)
题解:换根dp好想到,第一次dfs也容易知道状态转移。主要是第二次容斥怎么弄,我画了图才明白转移方程是 ans[v][j] += ans[x][j-1]-dp[v][j-2];因为x的j-1部分与原先v的j部分(未更新前ans[v][j] = dp[v][j]),而重叠部分刚好就是dp[v][j-2],于是就把它减去就可以了,这就是容斥的转移
#include<bits/stdc++.h> #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int n,k; int w[maxn],dep[maxn],dp[maxn][25]; void dfs(int x,int f){ rep(i,0,k) dp[x][i]=w[x]; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f) continue; dfs(v,x); for(int j=1;j<=k;j++){ dp[x][j]+=dp[v][j-1]; } } } int ans[maxn][25]; void dfs2(int x,int f){ for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==f) continue; ans[v][1]+=dp[x][0]; for(int j=2;j<=k;j++){ ans[v][j]+=ans[x][j-1]-dp[v][j-2]; } dfs2(v,x); } } int main(){ cin>>n>>k;mem(head,-1); rep(i,1,n-1){ int u,v;cin>>u>>v; add(u,v);add(v,u); } rep(i,1,n) cin>>w[i]; dfs(1,-1); rep(i,1,n){ rep(j,0,k){ ans[i][j]=dp[i][j]; } } dfs2(1,-1); rep(i,1,n){ cout<<ans[i][k]<<endl; } }
16.洛谷P4084【USACO17DEC】 Barn Painting G(树上状态机dp)
题解:这个比较简单,dp[i][j]表示当前结点为i颜色为j有多少种情况。那么就很简单了。状态转移方程是:
if(!fg1) dp[u][1]*=((dp[v][3]+dp[v][2])%mod),dp[u][1]%=mod;
if(!fg2) dp[u][2]*=((dp[v][3]+dp[v][1])%mod),dp[u][2]%=mod;
if(!fg3) dp[u][3]*=((dp[v][1]+dp[v][2])%mod),dp[u][3]%=mod;
最后输出(dp[root][1]+dp[root][2]+dp[root][3])%mod,记得开ll和一些特判吧
#include<bits/stdc++.h> #define ll long long #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define endl '\n' #define eps 0.000000001 #define pb push_back #define mem(a,b) memset(a,b,sizeof(a)) #define IO ios::sync_with_stdio(false);cin.tie(0); using namespace std; const int INF=0x3f3f3f3f; const ll inf=0x3f3f3f3f3f3f3f3f; const int mod=1e9+7; const int maxn=1e5+5; int tot,head[maxn]; struct E{ int to,next; }edge[maxn<<1]; void add(int u,int v){ edge[tot].to=v; edge[tot].next=head[u]; head[u]=tot++; } int n,q,col[maxn]; ll dp[maxn][4]; void dfs(int u,int fa){ int ct=0; int fg1=0,fg2=0,fg3=0; dp[u][1]=dp[u][2]=dp[u][3]=1; for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; if(col[v]==1) fg1=1,dp[u][1]=0; if(col[v]==2) fg2=1,dp[u][2]=0; if(col[v]==3) fg3=1,dp[u][3]=0; } if(fg1&&fg2&&fg3){ cout<<"0"<<endl; exit(0); } for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; ++ct; dfs(v,u); if(!col[u]){ if(!fg1) dp[u][1]*=((dp[v][3]+dp[v][2])%mod),dp[u][1]%=mod; if(!fg2) dp[u][2]*=((dp[v][3]+dp[v][1])%mod),dp[u][2]%=mod; if(!fg3) dp[u][3]*=((dp[v][1]+dp[v][2])%mod),dp[u][3]%=mod; }else{ if(col[u]==1){ dp[u][1]*=((dp[v][3]+dp[v][2])%mod); dp[u][1]%=mod; dp[u][2]=dp[u][3]=0; } else if(col[u]==2){ dp[u][2]*=((dp[v][1]+dp[v][3])%mod); dp[u][2]%=mod; dp[u][1]=dp[u][3]=0; } else if(col[u]==3){ dp[u][3]*=((dp[v][1]+dp[v][2])%mod); dp[u][3]%=mod; dp[u][1]=dp[u][2]=0; } } } if(ct==0){ if(!col[u]) dp[u][1]=dp[u][2]=dp[u][3]=1; else{ rep(i,1,3){ if(i==col[u]) dp[u][i]=1; else dp[u][i]=0; } } } } int main(){ cin>>n>>q;mem(head,-1); rep(i,1,n-1){ int u,v;cin>>u>>v; add(u,v);add(v,u); } rep(i,1,q){ int x,y;cin>>x>>y; col[x]=y; } dfs(1,-1); cout<<(dp[1][1]+dp[1][2]+dp[1][3])%mod<<endl; }