CF735div2E you 枚举因子 + 调和级数

博客主要讨论了一种树的定向问题,其中涉及到每个节点的入度与gcd(最大公约数)的关系。作者通过解释如何将树的边定向来满足特定的gcd条件,并使用埃拉托斯特尼筛法优化解决方案,避免了高时间复杂度的计算。文章通过DFS遍历树并枚举因子来判断是否存在满足条件的定向方案,最后给出了完整的C++代码实现。
摘要由CSDN通过智能技术生成

前言

经过 HJQwQ 巨佬亿波指点,蒟蒻 GGN_2015 终于理解了这道题的做法,HJQwQ 太强了。

题目链接:contest 1554 E. You

思路

原文中的 “删点操作”,等价于给树上的每条边定向,让后令 a x a_x ax 等于,结点 x x x 在树上的入度。由于树上有 n − 1 n-1 n1 条边,所以总定向方案数为 2 n − 1 2^{n-1} 2n1 种。

又由于 ∑ i = 1 n a i = n − 1 \sum _{i=1}^n a_i=n-1 i=1nai=n1,因此 k = gcd ( a 1 , a 2 , ⋯   , a n ) k = \text{gcd}(a_1, a_2, \cdots, a_n) k=gcd(a1,a2,,an) 一定是 n − 1 n-1 n1 的因子,如果不是,那么方案数为零。

枚举 n − 1 n-1 n1 的所有非 1 1 1 因子 k k k,考虑从叶子结点开始向上对所有树边定向。叶子节点的父亲边显然一定要指向父亲,这样它自己的入度为 0 0 0 gcd \text{gcd} gcd 才可能是 k k k。而当一个结点的所有儿子边的状态已经确定时,假设这个时候这个结点的入度为 v v v,因为 v v v v + 1 v+1 v+1 中至多只有一个是 k k k 的倍数,所以该节点的父亲边的方向也能定向。换言之对于每个 k > 1 k>1 k>1,方案数要么是 0 0 0 要么是 1 1 1

因此,只需要 O ( n ) O(n) O(n) 的一次 DFS 就能确定是否存在一种方案使得 gcd \text{gcd} gcd k k k 的倍数。最终得到的 gcd \text{gcd} gcd 并不一定真的是 k k k,但是如果暴力使用 欧几里得辗转相除计算真正的 gcd \text{gcd} gcd 时间代价太大,会让总时间复杂度变成 O ( n n × log ⁡ n ) O(n\sqrt{n}\times \log n) O(nn ×logn)

这里可以考虑使用埃拉托斯特尼筛法的思想:如果 ∃ k 2 > k 1 , k 1 ∣ k 2 \exist k_2 > k_1, k_1|k_2 k2>k1,k1k2 使得 能找到一种定向方案使得 gcd \text{gcd} gcd k 2 k_2 k2 的倍数,那么在 DFS 定向的过程中,其实,按 k 1 k_1 k1 定向 与 按 k 2 k_2 k2 定向,得到的一定是同一种方案,换言之 不存在 gcd \text{gcd} gcd 恰好等于 k 1 k_1 k1 的方案。

这里我们记 a n s ( x ) ans(x) ans(x) 表示是否存在一个 gcd \text{gcd} gcd 恰好等于 x x x 的方案,记 p r e ( x ) pre(x) pre(x) 表示以 x x x 为倍数约束进行 DFS 能否找到定向方案。

因此就有 p r e ( x ) = ⋃ x ∣ y a n s ( y ) pre(x) = \bigcup_{x|y} ans(y) pre(x)=xyans(y),稍微变形,得到:

a n s ( x ) = p r e ( x ) ∩ not    ⋃ x ∣ y , y > x p r e ( y ) ans(x) = pre(x) \cap \text{not}\; \bigcup_{x|y, y>x} pre(y) ans(x)=pre(x)notxy,y>xpre(y)

由此可以对 k > 1 k>1 k>1 计算出 a n s ans ans 数列,根据:

a n s ( 1 ) = 2 n − 1 − ∑ i = 2 n − 1 a n s ( i ) ans(1)=2^{n-1}-\sum_{i=2}^{n-1} ans(i) ans(1)=2n1i=2n1ans(i)

便可以计算出 a n s ( 1 ) ans(1) ans(1)

代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

int read() {
	int ans = 0; char c = getchar();
	while('0' > c || c > '9') c = getchar();
	while('0' <=c && c<= '9') ans = (ans << 3) + (ans << 1) + (c^'0'), c = getchar();
	return ans;
}

namespace tree {
	const int maxn = 100000 + 6;
	int ans[maxn];
	int fst[maxn], nxt[maxn * 2], eto[maxn * 2], ecnt; /// 前向星 
	
	void init(int n) {
		memset(ans, 0xff, sizeof(int) * (n + 1)); /// 表示还没被计算过 
		memset(fst, 0x00, sizeof(int) * (n + 1));
		ecnt = 0;
	}
	
	void addedge(int f, int t) { /// 添加单向边 
		int id = ++ ecnt;
		eto[id] = t;
		nxt[id] = fst[f]; /// 前向星 fst[f] 是 f 的第一条出边 
		fst[f] = id;
	}
	
	int dfs(int x, int f, int k) { /// 要求每个点的度都是 k 的倍数 
		int soncnt = 0;
		for(int e = fst[x]; e; e = nxt[e]) {
			int t = eto[e];
			if(t == f) continue; /// 跳过父亲节点 
			int tmp = dfs(t, x, k);
			if(tmp == -1) { /// -1 表示无论是否取父亲边,都无法使该点度为 k 的倍数 
				return -1;
			}
			soncnt += tmp; /// 0 表示需要取父亲边, 1 表示不能取父亲边 
		}
		if(soncnt % k == 0) return 1; /// 不能取父亲边 
		if((soncnt + 1) % k == 0) return 0; /// 需要取父亲边 
		return -1; /// 无论取不取都不行 
	}
}
using tree :: ans;

const int mod = 998244353;
int qpow(int a, int b, int p) { /// 快速幂 
	int ans = 1, mul = a % p;
	while(b) {
		if(b & 1) ans = (long long)ans * mul % p;
		b >>= 1;
		mul = (long long)mul * mul % p;
	}
	return ans % p;
}

void solve() {
	int n = read();
	tree :: init(n); /// 多组数据记得初始化 
	for(int i = 1; i <= n-1; i ++) {
		int f = read(), t = read();
		tree :: addedge(f, t);
		tree :: addedge(t, f); /// 添加双向边 
	}
	for(int i = 1; i*i <= n-1; i ++) {
		int L = i;
		int R = (n-1)/i;
		if(ans[L] == -1 && L != 1) ans[L] = tree :: dfs(1, 0, L) == 1;
		if(ans[R] == -1 && R != 1) ans[R] = tree :: dfs(1, 0, R) == 1; /// dfs  判断是否可行 
	}
	int sum = 0;
	for(int i = 2; i <= n-1; i ++) { /// 想要确定 i 是否可行 
		if(ans[i] == 1) {
			for(int j = 2*i; j <= n-1; j += i) {
				if(ans[j] == 1) { /// 如果说 i 有一个 倍数可行,那么 i 一定不可行 
					ans[i] = 0;
					break;
				}
			}
			/// 如果 i 的倍数都不行,那么 i 才可行 
		}
		sum += (ans[i] == 1); /// 统计可行的方案总数 
		if(sum >= mod) sum -= mod;
	}
	ans[1] = (qpow(2, n-1, mod) - sum) % mod; /// 总共有 2^(n-1) 种定向方案 
	if(ans[1] < 0) ans[1] += mod;
	for(int i = 1; i <= n; i ++) printf("%d ", ans[i] >= 0 ? ans[i] : 0);
	printf("\n");
}

int main() {
	int T = read();
	while(T --) {
		solve();
	}
	return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值