题目
题目描述 :牛牛最近得到了一颗树,根是 1 号节点,他想要把这颗树染色。
每个节点可以染成白色和黑色,牛牛认为一种染色方案是好的当且仅当任意两个黑点的 lca(最近公共祖先)的颜色也是黑色的。
求一共有多少种好的染色的方案。
答案可能很大,请输出答案对1e9 + 7取模后的结果。
输入描述:
第一行一个整数 n,表示这棵树有 n 个节点。
接下来n − 1行,每行两个整数u,v表示u,v之间有一条边。
一行一个整数,表示所有合法的点的集合数量。
解析
简单理解题意:就是两个黑色节点最近的共同祖先,一定是黑色;或者说是一个节点的左子树和右子树都有黑色节点的话,这个节点一定是黑色的。
知道题目意思以后,我们不难发现这是一道dp的题目(我当然发现了!只是不会写而已),所以超菜的我不厌其烦的还是要说这句话:
- 动态规划的基础就是递推!
那么我们就来推一下递推公式:
- 首先每个节点有两种情况:<1>节点为黑色(我们用dp[pos][0]表示)。<2>节点为白色(我们用dp[pos][1]表示)。
- 首先我们来讲黑色:黑色的节点,无论下面是什么情况(有没有黑色子节点都可以是黑的),
所以我们就可以肆无忌惮的得到状态转移方程:(乘法表示关联) - 然后我们来讲白色:白色的节点,要考虑下面是否有两棵子树都有黑色的情况,所以我们单独把每个子节点是(黑/白)的情况加起来就好了。
所以状态转移方程就是:(加法表示独立存在) - 然后记得过程取余就好了。
又到了传统异能:写代码:
- 首先用前向星保存好我们的边(前向星还不是很熟的看专栏,点这里)。
- 然后用dfs的方法,对整棵树的所有边进行遍历。并用上两个状态转移方程。
- 最后把根节点的两种颜色之和取模输出来就好了。
AC代码
#include <iostream> #include <cstring> using namespace std; typedef long long ll; //代码预处理区 const int MOD = 1e9 + 7; const int MAX = 1e6 + 7; struct node { int v, next;//边的终点和同起点的下一条边 } edge[MAX << 1]; int head[MAX], total = 0;//前向星初始化变量 ll dp[MAX][2];//树状dp数组,第二个参数为0节点是黑色,1是白色。 //全局变量区 template<class T>inline void read(T& res);//整型快读函数 void add_edge(int u, int v);//前向星加边函数 void dfs(int x, int fa);//深度优先遍历,遍历树 //函数预定义区 int main() { int n; read(n);//总边数 memset(head, -1, sizeof head); for (int i = 1; i < n; i++) { int u, v; read(u); read(v); add_edge(u, v); add_edge(v, u); } //前向星初始化 dfs(1, 0); printf("%lld\n", (dp[1][0] + dp[1][1]) % MOD); return 0; } template<class T>inline void read(T& res) { char c; T flag = 1; while ((c = getchar()) < '0' || c > '9') if (c == '-') flag = -1; res = c - '0'; while ((c = getchar()) >= '0' && c <= '9') res = res * 10 + c - '0'; res *= flag; } void add_edge(int u, int v) { edge[++total].v = v; edge[total].next = head[u]; head[u] = total; //起始点为1的加边 } void dfs(int u, int fa) { ll zero = 1, one = 1; for (int i = head[u]; ~i; i = edge[i].next) { int v = edge[i].v; if (v != fa) { dfs(v, u); zero = zero * (dp[v][0] + dp[v][1]) % MOD; one = (one + dp[v][0] + dp[v][1] - 1 + MOD) % MOD; //黑色和白色的状态转移方程 } } dp[u][0] = zero; dp[u][1] = one; } //函数区