对于树上状态机dp问题的一些总结与思考
前言:全篇纯属个人理解与感悟,建议带着批判的视角来审视。
本次博客的一些有关例题(难度递增)如下:AcWing285(没有上司的舞会);AcWing323(战略游戏);AcWing1077(皇宫看守);洛谷P2279【HNOI2003】消防局的设立;
1.AcWing285(没有上司的舞会)
题意:给你n个点n-1条边,每个点有权值,一个边最多可选一个点,问树上最大可选权值和
题解:我们仔细审题会发现题目要求,"一个边最多可选一个点"说明该边上的点可选数量为0-1这两种情况。因为是树上问题,我们对于父亲结点u来考虑。树上状态机dp问题无非是父亲结点与孩子结点之间关系的状态转移。如果选了父亲结点u(即定义状态为dp[u][1],1表示选了该节点,0表示没选该节点),那么其孩子节点v必定没选,所以我们可以推出这样一个关系dp[u][1] = Σdp[v][0] 。接下来我们就要考虑不选父亲结点的情况(即状态为dp[u][0]),因为一条边上的点可选可不选,而我们题目又要求树上最大权值和。那么我们推出关系dp[u][0]=Σmax(dp[v][0],dp[v][1])。以上就是状态转移的所有形式,最后输出max(dp[root][0],dp[root][1])就是本题所求的树上最大可选权值和了。
AC代码:
#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; }
2.AcWing323(战略游戏)
题意:给你n个点(编号为0 ~ n-1),每条边至少选1个点,问最少能选几个点?
题解:感觉和上面那个题很类似,状态转移也很好想。因为题目要求树上最小点数。那么我们对于一条边分析,如果父亲结点u没选,那么其孩子节点v必选,所以状态转移方程为dp[u][0]=Σdp[v][1]。如果父亲结点u选了,那么其孩子理论上可选可不选,但我们要求求最小点,而dp的思路是通过局部的最小的优化转移到整体,即以小见大,所以我们需要让局部最小,故转移方程为dp[u][1]=Σmin(dp[v][0],dp[v][1]).
AC代码:
#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])); } }
3.AcWing1077(皇宫看守)
题意:给你n个点,n-1条边,一个点可以看守与其相邻的点。每个点有权值,问最小花费
题解:设立dp函数表达式,dp[i][0]表示结点i向上一层最小花费,dp[i][1]表示i向上0层最小花费,dp[i][2]表示结点i向上-1层最小花费,是第四道题的easy版本。
其中,“覆盖到某层”的意思是在这棵子树中这一层和其以下层的所有点都被消防站覆盖到。比如,“覆盖到从节点i向上1层”指的是“以节点i为根的整棵子树和i的父亲都被覆盖”,“覆盖到从节点i向上-1层”指的是“节点i的所有儿子和它们的子孙都被覆盖”。此外,这3个状态是互相包含的。也就是dp[i][0] ≥ dp[i][1] ≥ dp[i][2]。
那么可以推出状态转移方程
dp[u][0]=w[u]+Σdp[v][2];
dp[u][1]=min(dp[v][0]+Σdp[t][1]) 与 dp[u][0]取min
dp[u][2]=Σdp[v][1]和 dp[u][0] 和 dp[u][1]取min
AC代码
#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][0]=w[x]; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; dfs(v); dp[x][0]+=dp[v][2]; dp[x][2]+=dp[v][1]; } if(head[x]==-1){ dp[x][1]=w[x]; } else{ dp[x][1]=INF; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; int F1=dp[v][0]; for(int j=head[x];j!=-1;j=edge[j].next){ int t=edge[j].to; if(t==v) continue; F1+=dp[t][1]; } dp[x][1]=min(dp[x][1],F1); } } for(int i=1;i<=2;i++){ dp[x][i]=min(dp[x][i],dp[x][i-1]); } } 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<<dp[root][1]<<endl; }
4.洛谷P2279【HNOI2003】消防局的设立
题意:给你n个点,n-1条边,一个点可以看守与其距离为2的点。每个点有权值,问最小花费
题解:和第四题一样,设立5个状态形式,然后转移,不加赘述了
AC代码
#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; }