题意
有
T
组测试样例,每组测试样例:
给你一棵
n
个节点的树,你要进行
n
次删除,每次删除选择树上一个没有删过的节点,将它删除,删除后会得到分数,分数等于与它连接并且没有被删除的节点个数。
例如如图所示的这样一棵树:
删除节点的顺序为
[2, 3, 1]
,那么分数分别是
[1, 1, 0]
,所以
[1, 2, 3]
点分别的分数就是
[0, 1, 1]
,
我们称
[0, 1, 1]
是一种分数排列
。
你可以改变删除节点的顺序,让你分别输出分数
gcd(题目规定:gcd 0 不变)= [1, n]
的分数排列有多少种情况,答案
mod 998244353
。
数据范围:
1 <= T <= 10000, 2 <= n <= 1e5, T组数据 n 的和不超过 3e5.
思路
定义
f(g)
等于
gcd = g, 2g, 3g, 4g...kg <= n
的分数排列种数的和。
我们先分别求出
f(1), f(2) ... f(n)
,然后再对其进行容斥,就能得到
gcd(1), gcd(2) ... gcd(n)
的分数排列种数。
-
对于连接
u, v
这条边,它只会影响
u, v
的分数,所以每增加一条边都会让
排列种数 * 2
,可以得到
f(
1
)
=
2
(
n
−
1
)
f(1) = 2^{(n-1)}
f
(
1
)
=
2
(
n
−
1
)
-
f(g),g = [2, n]
的分数排列种数只可能是 0 或者 1。-
我们尝试构造
g = k
的分数排列,我们从叶子节点往上进行分析。 -
对于
叶子节点
leaf
肯定不能删,因为删了就会存在分数 1,这样
gcd
就肯定不可能等于
k
了,我们只能选择删
leaf
的父亲。 -
这时候我们关注
leaf
的
父亲
f
,在不考虑
f 的父亲
的情况,删除
f
得到的分数如果不能整除
k
,那就把
f 的父亲
考虑进来,如果还是不能整除
k
,
gcd
就肯定不可能等于
k
了。如果能整除
k
,
f
和
f 的父亲
的边贡献给了谁也就知道了。 - 按照上面的思路,我们会发现分数排列只可能有一种情况。
-
我们尝试构造
-
如果
i-1
不能整除
g
,是肯定不会存在一个分数排列
gcd = g
的倍数的。
代码
#include<iostream>
#include<vector>
using namespace std;
const int N = 1e5+5;
const int MOD = 998244353;
vector<int> map_[N];
int f[N], a[N];
bool has_ans;
void dfs(int u, int f, int k) {
for(int i = 0; i < map_[u].size(); i++) {
if(!has_ans) return;
int to = map_[u][i];
if(to == f) continue;
dfs(to, u, k);
if(a[to]%k) {
a[to]++;
if(a[to]%k) has_ans = false;
} else {
a[u]++; // 当 u == 1 的时候,++ 后一定 % k == 0
}
}
}
int main(void) {
ios::sync_with_stdio(false);
int T;
cin >> T;
while(T--) {
int n;
cin >> n;
for(int i = 1; i <= n; i++) {
map_[i].clear();
f[i] = 0;
}
f[1] = 1;
for(int i = 1; i < n; i++) {
f[1] *= 2, f[1]%=MOD;
}
for(int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
map_[u].push_back(v);
map_[v].push_back(u);
}
for(int i = 2; i <= n; i++) {
if((n-1)%i != 0) continue;
for(int j = 1; j <= n; j++) a[j] = 0;
has_ans = true;
dfs(1, 0, i);
f[i] = has_ans;
}
for(int i = n; i >= 1; i--) {
for(int j = i+i; j <= n; j += i) {
f[i] = (f[i] - f[j] + MOD) % MOD;
}
}
for(int i = 1; i <= n; i++) {
cout << f[i];
if(i == n) cout << '\n';
else cout << ' ';
}
}
return 0;
}