前言
经过 HJQwQ
巨佬亿波指点,蒟蒻 GGN_2015
终于理解了这道题的做法,HJQwQ
太强了。
思路
原文中的 “删点操作”,等价于给树上的每条边定向,让后令 a x a_x ax 等于,结点 x x x 在树上的入度。由于树上有 n − 1 n-1 n−1 条边,所以总定向方案数为 2 n − 1 2^{n-1} 2n−1 种。
又由于 ∑ i = 1 n a i = n − 1 \sum _{i=1}^n a_i=n-1 ∑i=1nai=n−1,因此 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 n−1 的因子,如果不是,那么方案数为零。
枚举 n − 1 n-1 n−1 的所有非 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,k1∣k2 使得 能找到一种定向方案使得
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)=⋃x∣yans(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)∩notx∣y,y>x⋃pre(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)=2n−1−i=2∑n−1ans(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;
}