AtCoder - ABC 160 - F(树的拓扑序计数,换根DP)

本文介绍了如何解决一个关于树的染色问题,其中每个节点只能染与其已染色邻居相同的颜色,并从每个节点开始计数染色方案。文章详细解释了计算每个节点为起点时的染色方案数的思路,包括计算无约束染色方案数、使用逆元处理模运算、求解子树大小以及换根递归DP的方法。此外,还提到了计算1到n的逆元和阶乘的预处理,以及在遍历树结构时的注意事项。
摘要由CSDN通过智能技术生成

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 ∗ 10^{5}

思路:

如何求一个节点为起点(根)时的方案数:

(这个问题其实就是问一颗有根树的拓扑序列个数。其实我们知道不是树的有向无环图的拓扑序列个数是个np问题(查了查具体是叫非确定性多项式问题(Nondeterministic Polynomial Problem),但是树的拓扑序列个数是一个可解的问题。)

先考虑n个节点无约束时的染色方案数,即n个颜色的全排列n!。但实际有限制,从某个节点为起点代表该起点必须染色为 1,所以对于求每个为起点时满足的序列是 (n-1)!

这是仅仅考虑起点,还需要考虑子节点,因为染色是有限制的(下一个染色的点与已染色的点相邻),也就是说起点染完后,下一个点一定是起点的儿子节点,以此类推,说明子树染完时也必须满足子树的根是该子树子树排列第一个。如果该课子树的大小为 size,即该棵子树的排列中只有1/size 个是合法的。因为概率是相互独立的,所以当拿起点为根的数也看做子树时,对于所有排列n!中只有\frac{1}{\prod _{u=1}^{n}size[u]}中是合法的(这里的size[1] = n,n!/size[1] = (n-1)!,所以 n! 不需要单独再除以 n)。

所以对于一个起点来说,合法的染色方案数是:\frac{n!}{\prod _{u=1}^{n}size[u]}

如何求当节点1~n为起点(根)时的方案数(换根dp):

假设已经得到以 fa 为根的方案数,考虑 fa 的一个儿子节点 son ,根据公式ans[fa] = \frac{n!}{n*size[son]*size[x]}

(size[fa] = n,size[son] 是以 son 为根节点的子树大小,size[x] 是除父节点 fa 和子节点 son 以外的所有子树的合法情况,即所有子树大小乘积之和) 

考虑二者为根的两棵树的各自方案数,相差的只有以另一方为子树根的 size 数,其他的子树节点情况相同( size[x] ),如果根节点为 fa,以 son 为子树根的子树大小为 size[son],那么以 son 为根,fa 为子树根的子树大小为 n - size[son],由此得出以 son 为起点的方案数:
ans[son] = \frac{n!}{n*size[n-size[son]]*size[x]}

实现:

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类型的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值