F. Distributing Integers
题目:
有 n 个节点的无根树,对树进行编号,先选定一个点 k 编号为1, 然后依次对剩余结点编号
有一颗节点编号为 1 至 N 的树,第 i 条边连接点 ai 和 bi 。对于 1 至 N 的每个 k 进行如下操作:
按如下操作在树上每个点写一个数字:
1.在点 k 上写上 1;
2.按从 2 到 N 的顺序将数写在节点上:
3.选择一个仍未写有数字且与已写有数字的点相邻的点,如果有多个这样的点,随机选择一个。
输出所有写法的数量,结果模 1e9+7
简化下:给一棵 n 点树染色,每次只能选择与已染色结点相邻的未染色结点,所用颜色依次为 1∼n,问以每个结点为起点时的染色方案总数。结果模 1e9+7
数据范围:
1 ≤ N ≤ 2 ∗
思路:
如何求一个节点为起点(根)时的方案数:
(这个问题其实就是问一颗有根树的拓扑序列个数。其实我们知道不是树的有向无环图的拓扑序列个数是个np问题(查了查具体是叫非确定性多项式问题(Nondeterministic Polynomial Problem),但是树的拓扑序列个数是一个可解的问题。)
先考虑n个节点无约束时的染色方案数,即n个颜色的全排列n!。但实际有限制,从某个节点为起点代表该起点必须染色为 1,所以对于求每个为起点时满足的序列是 (n-1)!
这是仅仅考虑起点,还需要考虑子节点,因为染色是有限制的(下一个染色的点与已染色的点相邻),也就是说起点染完后,下一个点一定是起点的儿子节点,以此类推,说明子树染完时也必须满足子树的根是该子树子树排列第一个。如果该课子树的大小为 size,即该棵子树的排列中只有1/size 个是合法的。因为概率是相互独立的,所以当拿起点为根的数也看做子树时,对于所有排列n!中只有中是合法的(这里的size[1] = n,n!/size[1] = (n-1)!,所以 n! 不需要单独再除以 n)。
所以对于一个起点来说,合法的染色方案数是:。
如何求当节点1~n为起点(根)时的方案数(换根dp):
假设已经得到以 fa 为根的方案数,考虑 fa 的一个儿子节点 son ,根据公式
(size[fa] = n,size[son] 是以 son 为根节点的子树大小,size[x] 是除父节点 fa 和子节点 son 以外的所有子树的合法情况,即所有子树大小乘积之和)
考虑二者为根的两棵树的各自方案数,相差的只有以另一方为子树根的 size 数,其他的子树节点情况相同( size[x] ),如果根节点为 fa,以 son 为子树根的子树大小为 size[son],那么以 son 为根,fa 为子树根的子树大小为 n - size[son],由此得出以 son 为起点的方案数:
实现:
1.因为答案要 mod (1e9+7),模数是质数,需要用到逆元,所以我们先预处理出需要的 1~n 逆元inv[] 和 n 的阶乘 fac[n];
2.将树当做无向图存,从第一个节点开始,先通过 dfs 求出所有节点的子树的大小,用数组 sz[]存;
3.套公式求出以第一个节点为起点(根)的方案数:ans[1] = fac[n] * inv[sz[i]]
4.遍历第一个节点的儿子节点 u,换根求出以节点 u 为根的方案数 ans[u] = ans[1] * sz[u] * inv[n-sz[u]]。以此类推,继续递归将 u 节点的儿子节点换为根计算,求出所有节点作根时的方案数。
Code:
#include<iostream>
#include<algorithm>
#include<vector>
#include<map>
#include<cstring>
#include<math.h>
using namespace std;
//#define x first
//#define y second
#define IOS ios::sync_with_stdio(false);cin.tie(0);
#define int long long
typedef pair<int, int>PII;
const int N = 200010, mod = 1e9 + 7;
int n;
int fac[N], inv[N];
int sz[N], ans[N];
vector<int>e[N];
//计算a * b % mod
int mul(int a, int b)
{
return a * b % mod;
}
//预处理出n!和1~n的逆元
void init()
{
fac[0] = 1; //预处理0!=1
for (int i = 1; i <= n; i++)fac[i] = mul(fac[i - 1], i); //fact[i]表示i!
inv[1] = 1; //预处理1的逆元=1
for (int i = 2; i <= n; i++)inv[i] = mul((mod - mod / i), inv[mod % i]); //根据求1~n的逆元的公式预处理出n!和1~n的逆元inv[p&i]*(-p/i)(mod p)用数组inv计算逆元
}
//计算每个节点的子树大小
void dfs(int u, int fa)
{
sz[u] = 1; //sz[u]记录节点u为根节点的子树大小
for (int v : e[u]) //遍历节点u的子节点v
{
if (v != fa) //如果子节点存在
{
dfs(v, u); //递归从下往上计算,先将更靠下的子节点的大小算出来
sz[u] += sz[v]; //累计根节点u的子节点的子树大小,计算出以u为根节点的子树大小
}
}
}
//换根,将根节点fa与节点fa的儿子节点u交换,通过根节点的方案数推出儿子节点的方案数
void reroot(int u, int fa)
{
ans[u] = mul(mul(ans[fa], sz[u]), inv[n - sz[u]]); //根据公式得:新根节点u的方案数ans[u] = 原根节点(即现根节点u的父节点fa) * 节点u的子树大小 * (n-sz[u])的逆元
for (int v : e[u]) //枚举节点u的儿子节点
{
if (v != fa)
reroot(v, u); //继续递归算出所有节点换为根节点时的方案数
}
}
void solve()
{
cin >> n;
init(); //预处理出n!和1~n的逆元
for (int i = 0; i < n - 1; i++) //输入树的n-1条边
{
int u, v;
cin >> u >> v;
e[u].push_back(v); //用二维数组e存树的边,按无向图一条边存两次
e[v].push_back(u);
}
dfs(1, 0); //从第一个节点开始,累计出所有节点拥有的子树的大小
ans[1] = fac[n]; //ans[1]表示以第一个节点为起点开始染色,根据递推公式先赋个n!
for (int i = 1; i <= n; i++)ans[1] = mul(ans[1], inv[sz[i]]); //计算n!/n*size[u],即n!*n的逆元(n=sz[1],逆元为inv[sz[i]])*其他子树的大小的逆元(其他子树大小为sz[2~n])
for (int v : e[1])reroot(v, 1); //遍历第一个节点的所有儿子节点v,将v与根节点1换根。即由1位根节点时的ans[1]推出v为根节点时的ans[v]
for (int i = 1; i <= n; i++)cout << ans[i] << endl; //输出以n个节点为起点时各自的染色方案数
}
signed main()
{
IOS;
int t = 1;
//int t;
//cin >> t;
while (t--)
{
solve();
}
return 0;
}
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
吐槽:这题好懂!虽然也是F题吧,但是相比之前的F题看懂题解花费的时间确实少多了,题意不难懂,虽然写不出来,但是解题思路很好懂,尤其是公式一推出了,做法豁然开朗。这里头的递归也不是很复杂。本题注意点:
1.求1~n的逆元公式还是第一次见。写得敲击好的大大的博客:逆元知识普及(扫盲篇) -- from Judge_Judge_Cheung的博客-CSDN博客
2.换根dp也是第一次见:简言之,子孙变祖宗,祖宗变子孙。参考的大大的博客:AtCoder Beginner Contest 160 - Kanoon - 博客园 (cnblogs.com)
3.还有一个我之前有些模糊的点:对于遍历节点1的儿子节点v时的:for (int v : e[1])。之前总偷懒用auto,以为这样v就是e[1],其实每次循环的v都不同,是二维数组e[1]的元素,所以其实是int类型的。