题解AT_abc160_f [ABC160F] Distributing Integers

题目传送门

题目分析

这大概率是一道dp题,但是由于树形结构的特殊性,使得以点的序号作为阶段进行转移不再合适,再基于题干中的描述 对于1至N的每个k进行如下操作 ,启发我们先求出以一个点作为序列第一个元素的方案,随后进行转移。

那么我们先分析对于一个定点作为根节点的情况。
(1)如果正向的考虑这个问题,先求出每棵子树的状态再考虑合并,情况将类似于将两个有序序列合并成一个序列,并对于各个原序列而言元素顺序不变,这要求很多的排列组合与递归函数,如利用 G x ( a , b ) Gx(a, b) Gx(a,b) 来计算分别将两个长度为 a , b a,b a,b 的序列合并所可能的方案, G x ( a , b ) Gx(a, b) Gx(a,b) 需要转化为 G x ( a , b − 1 ) + G x ( a − 1 , b ) Gx(a, b - 1) + Gx(a - 1, b) Gx(a,b1)+Gx(a1,b) 递归求解,在本题背景下,即 O ( 2 × 1 0 5 ) O(2 \times10^5) O(2×105) 规模下这是无法做到的。

(2)因此,我们反向思考问题。对于总计 n ! n! n! 种忽视合法性的方案,分别对每棵子树考虑:由于该树根节点必须在子节点被选中前先被选中,因此必须是第一个此子树中被选中的,因此对于以 u u u 为根的子树而言,只有 n ! s i z [ u ] \frac{n!}{siz[u]} siz[u]n! 种方案是满足要求的,并且合法的方案对剩余未考虑的子树而言仍然可能是不合法的,反复利用这一性质,我们得到最终有 n ! ∏ i = 1 n s i z [ i ] \frac{n!}{\prod_{i=1}^{n} siz[i]} i=1nsiz[i]n! 种方案是合法的。考虑到取模,我们要使用逆元进行求逆的运算。

那现在考虑转移就更为简单了,只需用换根dp不断设新节点作为根节点,并改变影响的 s i z siz siz 值,改变连乘值,最后回溯修改回原状态即可。由于一次只涉及两个 s i z siz siz 值的修改(原根和现根),换根的时间复杂度被认为是 O ( 1 ) O(1) O(1) , 再考虑上快速幂,总时间复杂度在 O ( n l o g P ) O(nlogP) O(nlogP) 级别,P为取模数。

代码

#include <iostream>
#include <vector>

using i64 = long long;

const int MAXN = 2e5 + 5, P = 1e9 + 7;

int n;
i64 tot = 1, prod = 1, ans[MAXN], siz[MAXN];
std::vector<int> G[MAXN];

void Dfs1(int u, int f){
	++ siz[u];
	for(int v : G[u]){
		if(v == f){
			continue;
		}
		Dfs1(v, u);
		siz[u] += siz[v];
	}
	prod *= siz[u];
	prod %= P;
	return;
}

i64 myPow(i64 a, i64 b){//a^b
	if(b == 0){
		return 1;
	}

	i64 ret = myPow(a, b / 2) % P;

	if(b % 2 == 0){
		return (ret * ret) % P;
	}else{
		return (((ret * ret) % P) * a) % P;
	}
}

void Dfs2(int u, int f){
	ans[u] = (myPow(prod % P, P - 2) * tot) % P;

	for(int v : G[u]){
		if(v == f){
			continue;
		}
		//将v重置为根节点,需要重置siz[u],siz[v],prod的值
		prod = (myPow((siz[u] * siz[v]) % P, P-2) * prod) % P;

		i64 c = siz[u];
		siz[u] -= siz[v];
		siz[v] = c;

		prod *= siz[u];
		prod %= P;
		prod *= siz[v];
		prod %= P;
		
		Dfs2(v, u);
		//将u重置为根节点,需要重置siz[u],siz[v],prod的值
		prod = (myPow((siz[u] * siz[v]) % P, P - 2) * prod) % P;

		siz[v] -= siz[u];
		siz[u] = c;

		prod *= siz[u];
		prod %= P;
		prod *= siz[v];
		prod %= P;
	}
	return;
}
int main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	std::cout.tie(nullptr);

	std::cin >> n;

	for(i64 i = 1; i < n; ++ i){
		int a,b;
		std::cin >> a >> b;
		G[a].push_back(b);
		G[b].push_back(a);

		tot *= (i + 1);
		tot %= P;
	}

	Dfs1(1, 1);
	Dfs2(1, 1);

	for(int i = 1; i <= n; ++ i){
		std::cout << ans[i] << "\n";
	}
	return 0;
}

补充

这种形式的问题被称为拓扑序个数问题

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值