题意
给定一棵树,每次可以删一个点,一个点的权值为删除该点时周围没被删的点的个数。对于一种删点方式,会得到一个序列 {an}\{a_n\}{an}。
对于 k∈[1,n]k\in [1,n]k∈[1,n],求 gcd(a1,a2,..an)=k\gcd(a_1,a_2,..a_n)=kgcd(a1,a2,..an)=k 的方案数。
其中,1≤n≤105,∑n≤3×1051\le n\le 10^5,\sum n\le 3\times10^51≤n≤105,∑n≤3×105。
分析
可以将问题转化为:对于每一条边 (u,v)(u,v)(u,v),要么给 uuu 加一,要么给 vvv 加一,求 gcd=k\gcd=kgcd=k 的方案数。同时,对于任意一种删点方案,都会得到唯一的 {an}\{a_n\}{an}。
考虑套路地容斥,设 gkg_kgk 为 gcd=k\gcd=kgcd=k 的方案数,fkf_kfk 为 k∣gcdk|\gcdk∣gcd 的 的方案数,那么 gk=fk−∑k∣k′,k′≠kgk′g_k=f_k-\sum\limits_{k|k',k'\neq k}g_{k'}gk=fk−k∣k′,k′=k∑gk′。
首先可以知道,f1=2n−1f_1=2^{n-1}f1=2n−1。
然后考虑求 fk(k≥2)f_k(k\ge 2)fk(k≥2)。可以发现,叶子节点的权值要么是 000,要么是 111,因为 k≥2k\ge 2k≥2,所以叶子节点的权值一定是 000。那么一个叶子会给他父亲贡献 111。这启发我们可以从叶子节点开始进行拓扑排序,一层一层地确定每个点的权值。对于某一层的一个叶子节点,假设其权值目前为 vvv,那么其要么变成 vvv,要么变成 v+1v+1v+1。根据简单的数学知识,我们可以知道,vvv 和 v+1v+1v+1 至多只有一个是 kkk 的倍数。因此,对于一个 fkf_kfk,其方案数是确定的,要么为 000,要么为 111。我们可以 O(n)O(n)O(n) 来确定 fkf_kfk。
同时,由于 ∑i=1nai=n−1\sum\limits_{i=1}^n a_i=n-1i=1∑nai=n−1,因此 gcd(a1,a2,...,an)=gcd(a1,..,an,∑ai)=gcd(a1,..,an,n−1)∣n−1gcd(a_1,a_2,...,a_n)=gcd(a_1,..,a_n,\sum a_i)=gcd(a_1,..,a_n,n-1)|n-1gcd(a1,a2,...,an)=gcd(a1,..,an,∑ai)=gcd(a1,..,an,n−1)∣n−1。因此,我们只需要对所有 d∣n−1d|n-1d∣n−1 求 fdf_dfd 就行了。
时间复杂度为 O(nlogn+nσ0(n−1))O(nlogn+n\sigma_0(n-1))O(nlogn+nσ0(n−1))。其中,σ0(n)\sigma_0(n)σ0(n) 是 nnn 的因子个数,最多有 128128128 个。
代码如下
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define all(x) x.begin(), x.end()
int main() {
#ifdef local
freopen("../in.txt", "r", stdin);
#endif
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
const int maxn = 1e5;
vector<vector<int> > d(maxn + 1);
for(int i = 1; i <= maxn; i++){
for(int j = i; j <= maxn; j += i){
d[j].push_back(i);
}
}
int T;
cin >> T;
while(T--){
int n;
cin >> n;
vector<vector<int> > E(n + 1);
vector<int> f(n + 1), g(f), du(f);
for(int i = 1, u, v; i < n; i++){
cin >> u >> v;
E[u].push_back(v);
E[v].push_back(u);
du[u]++, du[v]++;
}
function<int(int)> check = [&](int x){
queue<int> q;
vector<int> w(n + 1), vis(w), d(du);
for(int i = 1; i <= n; i++) if(d[i] == 1) q.push(i);
while(!q.empty()){
int u = q.front();
q.pop();
if(vis[u]) continue;
vis[u] = 1;
for(int& v: E[u]){
if(vis[v]) continue;
--d[v];
if(w[u] % x == 0) w[v]++;
else w[u]++;
if(d[v] == 1) q.push(v);
}
}
for(int i = 1; i <= n; i++) if(w[i] % x != 0) return 0;
return 1;
};
for(int x: d[n - 1]) if(x != 1) f[x] = check(x);
const int mod = 998244353;
int two = 1;
for(int i = n - 1; i >= 1; i--){
two += two;
if(two >= mod) two -= mod;
g[i] = f[i];
if(i == 1) g[i] = two;
for(int j = i + i; j <= n - 1; j += i) g[i] -= g[j];
if(g[i] < 0) g[i] += mod;
}
for(int i = 1; i <= n; i++) cout << g[i] << ' ';
cout << '\n';
}
return 0;
}
本文介绍了一种解决关于树删点问题的方法,通过拓扑排序和容斥原理,计算给定树中每种删点方式得到特定gcd值的方案数。关键步骤包括确定叶子节点权值、利用递推公式求gcd方案数,以及利用因子个数优化时间复杂度。
295

被折叠的 条评论
为什么被折叠?



