P9161 Trees 题解

· · 题解

题面传送门

我的 BLOG

前置知识: 树形 DP

题意描述

给定两个数 mn,表示有 mn 个点的树。其中每棵树形态、结构相同,给定它们的 n-1 条边。需要你为每棵树上的所有结点赋值,令 a(i,j) 表示为第 i 棵树上的第 j 个点被赋的值。赋值规则如下:

要求出有多少种赋值方案数

算法分析

因为要求方案数,可以想到使用 DP。而题目要求是在树上操作,可以确定是树形 DP

定义状态

树形 DP 的状态比较套路,一般设两维(dp[i][j])。第一维表示“以 i 为根结点的字树”,第二维根据题意要求,表示 i 号位置为 j。意义什么呢?因为题目所求是赋值方案,于是可以断定:

但这个状态有一个小**错误**,假如其中一棵树 $i$ 号位置为 $1$,则其他树必须为 $0$,而不能全部为 $1$。因此需要分类讨论: 1. 当 $j=0$ 时: $dp[i][j]$ 表示,以 $i$ 为根结点的子树,第 $i$ 号位置为 $0$ 的方案数。 2. 当 $j=1$ 时: $dp[i][j]$ 表示,以 $i$ 为根结点的子树,其中一棵树第 $i$ 号位置为 $1$,其他树为 $0$ 的方案数。 ### 状态转移方程 个人认为,找状态转移方程是此题的难点。一旦推出来了,剩下的就只是打板子了。 根据我们定义的状态,需要分两种情况讨论: 1. 当第 $i$ 位为 $0$ 时: - 当它的儿子为 $0$ 时: 为以前的方案数乘以这个儿子的方案数:$dp[i][0] = dp[i][0] \times dp[son][0]$。 - 当它的儿子为 $1$ 时: 对于每棵树的它的这个儿子的位置,都可以为 $1$,根据乘法原理,为以前的方案数乘以 $m$ 种为 $1$ 的方案数:$dp[i][0] = dp[i][0] \times m \times dp[son][1]$。 2. 当第 $i$ 位为 $1$ 时: - 当它的儿子为 $0$ 时: 同样为以前的方案数乘以这个儿子的方案数:$dp[i][1] =dp[i][1] \times dp[son][0]$。 - 当它的儿子为 $1$ 时: 对于每棵树的它的这个儿子的位置,除了它本身的儿子,其他的都可以为 $1$,根据乘法原理,为以前的方案数乘以 $m-1$ 种为 $1$ 的方案数:$dp[i][1] =dp[i][1]\times (m-1) \times dp[son][1]$。 合并得: 1. $dp[i][0] =dp[i][0]\times (dp[son][0]+m \times dp[son][1])$。 2. $dp[i][1] =dp[i][1] \times (dp[son][0]+(m-1) \times dp[son][1])$。 ### 答案 根据我们定义的状态,答案为:以根结点为根的树,所有树根结点为 $0$ 或任意一棵树根结点 $1$ 的方案数之和。即: $$ans=dp[1][0]+m \times dp[1][1]$$ ### 初始状态 根据状态转移方程,因为需要乘,如果每个 $dp[i][j]$ 设为 $0$,则最终答案只能为 $0$。因此将所有 $dp[i][j]$ 设为 $1$,即: $\forall i \in [1,n],\forall j \in [0,1]$,有 $a_{(i,j)}\in\{1\}$。 ## 注意事项 - 因为是树,要建**双向边**,且在 dfs 时**需要判父亲**,不然会不断走重复的路,直至卡死。 - 方案数可能很大,记得**每转移一次就取模一次**,并开 long long。 - **一定记得先搜索再转移**,不然它的儿子的方案数还未算出,转移是无效的。 - 有表达不当出可以辅以代码注释理解。 ## 代码 ~~~ #include<bits/stdc++.h> #define mod 1000000007//模数 using namespace std; const int N=1e6+10; int n,m; long long dp[N][2];//记得开long long vector<int> vec[N]; void dfs(int cur,int fa){//因为是无向边,需要记录它的父亲以避免重复 for(int i=0;i<vec[cur].size();i++){ int to=vec[cur][i]; if(to==fa)//判断父亲 continue; dfs(to,cur);//先搜索,再转移,不然dp[to][0/1]还没有算出来,转移无效 dp[cur][0]=(dp[cur][0]*(dp[to][0]+m*dp[to][1]%mod))%mod; dp[cur][1]=(dp[cur][1]*(dp[to][0]+(m-1)*dp[to][1]%mod))%mod;//状态转移 } } signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n>>m; for(int i=1;i<n;i++){ int u,v; cin>>u>>v; vec[u].push_back(v); vec[v].push_back(u);//记得存双向边 } for(int i=1;i<=n;i++) dp[i][0]=dp[i][1]=1;//初始状态 dfs(1,0);//从根结点深搜 cout<<(dp[1][0]+m*dp[1][1]%mod)%mod;//答案 return 0; } ~~~ **[AC 记录](https://www.luogu.com.cn/record/127023868)** ## 总结 此题的难点主要是找出**状态转移方程**,一旦找到了,其他问题也迎刃而解。如何找到呢?最重要的是理解题意和所求。在理清他们的关系后,经过模拟与假设,逐渐摸索出正解。当然,树形 DP 这一类题的细节也挺多的,~~我深有体会,有可能就是一个符号的差距,就是一片红和一片绿的差距。~~ 因此,多注重细节,才能在考场上稳中求胜。 插句题外话,CSP2023 即将到来,这有可能是我考前最后一次写题解了。在此预祝各位: **CSP2023** $RP+\infty$!!!