2025杭电多校1解题报告
update1:补充1001博弈(才学Anti_Nim)2025-7-24
1007 树上LCM
题意
给一颗n个结点组成的树,一个数x,每个结点有一个值,问有多少条路径满足该路径的LCM = x, 路径的LCM即路径上所有结点的LCM。
1 <= n <= 3e5, 2 <= x <= 1e7,
a
i
a_i
ai <= 1e9 。
观察
LCM的性质:
l
c
m
(
x
1
,
x
2
,
x
3
.
.
.
,
x
n
)
lcm(x_1,x_2,x_3...,x_n)
lcm(x1,x2,x3...,xn) =
p
1
k
1
∗
p
2
k
2
∗
p
3
k
3
∗
.
.
.
∗
p
m
k
m
p_1^{k_1}*p_2^{k_2}*p_3^{k_3}*...*p_m^{k_m}
p1k1∗p2k2∗p3k3∗...∗pmkm
其中:
p
i
p_i
pi为质因子,
k
i
k_i
ki为对应的指数最大。
所以路径上的点至少需要满足三个条件。
- a i a_i ai对应的价值能整除x。
- a i a_i ai进行质因数分解后的指数需要小于等于 k i k_i ki
- 对任意的i,至少存在一个指数等于
k
i
k_i
ki
x最大能达到1e7,其质因子个数cnt不超过7个。这个数很小,考虑在这里切入。
思路
那些不能整除x的点,肯定无法形成路径。我们可以拆开这些点,这样会形成一些森林。
设目标lcm x的质因子个数为sz,我们对每个点进行状态压缩: 1 << sz, 其中第i位为1,表示这个点第i个质因子的指数小于
k
i
k_i
ki,其中
k
i
k_i
ki为x分解质因数后对应的指数,为0则表示相等。设
F
[
i
]
F[i]
F[i]表示状态为i的路径数,
S
[
i
]
S[i]
S[i]表示
i
⊂
点状态
i\subset点状态
i⊂点状态的点数,易知状态为i的路径个数就是所有
点状态
⊃
i
点状态\supset i
点状态⊃i的点组成的路径数,对于树上的路径计数,我们可以自然的想到使用并查集维护联通块,联通块上的路径数显然可以利用点的数量
O
(
1
)
O(1)
O(1)求出。
现在我们得到了每个
F
[
i
]
,
0
<
=
i
<
2
s
z
F[i],0<=i<2^{sz}
F[i],0<=i<2sz。现在可以利用容斥原理求出答案。由容斥原理
a
n
s
=
∑
i
=
1
(
2
s
z
)
−
1
(
−
1
)
popcount
(
i
)
⋅
F
[
i
]
ans = \sum_{i=1}^{(2^{sz}) - 1} (-1)^{\text{popcount}(i)} \cdot F[i]
ans=∑i=1(2sz)−1(−1)popcount(i)⋅F[i]
代码
// 树上dp,sosdp
#include <bits/stdc++.h>
using namespace std;
#define int long long
constexpr int inf = 1e18;
constexpr int mod = 998244353;
constexpr int N = 2e5 + 10;
void solve() {
int n, x;
cin >> n >> x;
vector<vector<int>> adj(n);
for(int i = 1; i < n; ++i) {
int u, v; cin >> u >> v;
u--, v--;
adj[u].push_back(v);
adj[v].push_back(u);
}
vector<int> a(n);
for(int &x : a) cin >> x;
vector<int> p, k;
vector<int> bad(n, 0);
bool ok = false;
for(int i = 0; i < n; ++i) {
if(x % a[i]) {
bad[i] = 1;
} else ok = true;
}
if(!ok) {
cout << "0\n";
return;
}
for(int j = 2; j * j <= x; ++j) {
if(x % j == 0) {
int cnt = 0;
while(x % j == 0) {
++cnt; x /= j;
}
p.push_back(j), k.push_back(cnt);
}
}
vector<int> vis(n, 0);
if(x > 1) p.push_back(x), k.push_back(1);
const int sz = (int)p.size();
const int full = 1LL << sz;
vector<array<int, 256>> dp(n);
int ans = 0;
auto dfs = [&] (auto && self, int u, int fa) -> void {
vis[u] = 1;
int now = 0;
int temp = a[u];
for(int i = 0; i < sz; ++i) {
int cnt = 0;
while(temp % p[i] == 0) temp /= p[i], ++cnt;
now |= ((cnt == k[i]) * (1LL << i));
}
if(now == (full - 1)) ans++;
dp[u].fill(0);
dp[u][now] = 1;
for(int &v : adj[u]) {
if(v == fa || bad[v]) continue;
self(self, v, u);
array<int, 256> sup = dp[u];
for(int i = 0; i < sz; ++i) {
for(int j = 0; j < full; ++j) {
if(!(j & (1 << i))) sup[j] += sup[j | (1 << i)];
}
}
for(int i = 0; i < full; ++i) {
int need = (full - 1) ^ i;
ans += sup[need] * dp[v][i];
}
for(int i = 0; i < full; ++i) {
dp[u][i | now] += dp[v][i];
}
}
};
for(int i = 0; i < n; ++i) {
if(!bad[i] && !vis[i]) dfs(dfs, i, -1);
}
cout << ans << '\n';
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
cin >> t;
while(t--) {
solve();
}
return 0;
}
提供一种莫比乌斯反演的方法
设
g
(
x
)
:
l
c
m
=
x
g(x):lcm=x
g(x):lcm=x的路径数,
f
(
x
)
:
l
c
m
∣
x
f(x):lcm|x
f(x):lcm∣x的路径数。显然有
f
(
x
)
=
∑
d
∣
x
g
(
d
)
f(x)=\sum_{d|x}g(d)
f(x)=d∣x∑g(d)
通过莫比乌斯反演有:
g
(
x
)
=
∑
d
∣
x
µ
(
d
)
∗
f
(
x
d
)
g(x)=\sum_{d|x}µ(d)*f(\frac{x}{d})
g(x)=d∣x∑µ(d)∗f(dx)
显然f(x)的数量可以通过dfs
O
(
n
)
O(n)
O(n)求出,发现1e7内的因子个数k不超过200,我们可以预处理出1e7内的莫比乌斯函数,对于每个因子d,
O
(
n
)
O(n)
O(n)计算
f
(
d
)
f(d)
f(d),复杂度为
O
(
k
n
+
N
)
O(kn+N)
O(kn+N)可以接受。
代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
constexpr int inf = 1e18;
constexpr int mod = 998244353;
constexpr int N = 1e7;
// 设g(x): lcm = x的路径,f(x): lcm整除x的路径。
// 显然f(x) = 1 * g(x)
// 则 g(x) = µ * f(x)
// 线性筛预处理莫比乌斯函数µ即可.
int mu[N + 1], minp[N + 1], p[N + 1];
int cnt = 0;
void init() {
mu[1] = 1;
for(int i = 2; i <= N; ++i) {
if(!minp[i]) p[++cnt] = i, mu[i] = -1;
for(int j = 1; j <= cnt && i * p[j] <= N; ++j) {
minp[i * p[j]] = p[j];
if(i % p[j] == 0) {
mu[i * p[j]] = 0;
break;
}
mu[i * p[j]] = -mu[i];
}
}
}
void solve() {
int n, x;
cin >> n >> x;
vector<vector<int>> adj(n);
for(int i = 1; i < n; ++i) {
int u, v; cin >> u >> v;
u--, v--;
adj[u].push_back(v), adj[v].push_back(u);
}
vector<int> a(n);
for(int &x : a) cin >> x;
vector<int> ok(n);
vector<int> siz(n);
int res = 0;
auto dfs = [&](auto &&self, int u, int fa) -> void {
siz[u] = ok[u];
for(int v : adj[u]) {
if(v != fa) self(self, v, u), siz[u] += siz[v];
}
if(ok[u] && (fa == -1 || !ok[fa])) res += siz[u] * (siz[u] + 1) / 2;
if(!ok[u]) siz[u] = 0;
};
auto cal = [&](int y) -> int {
for(int i = 0; i < n; ++i) {
ok[i] = (y % a[i] == 0);
}
res = 0;
dfs(dfs, 0, -1);
return res;
};
int ans = 0;
for(int i = 1; i <= x; ++i) {
if(x % i == 0 && mu[i]) {
ans += mu[i] * cal(x / i);
}
}
cout << ans << '\n';
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
init();
int t = 1;
cin >> t;
while(t--) {
solve();
}
return 0;
}
1001 博弈
前置知识:Nim和anti_Nim
我们记
T
=
⨁
i
=
1
n
x
i
T=\bigoplus_{i=1}^{n}x_i
T=⨁i=1nxi
对于Nim游戏有:若
T
=
0
T=0
T=0则必败,否则必胜。
对于anti_Nim游戏分两种情况:
如果所有堆全部为1,则奇败偶胜。
至少有一堆大于1,则
T
=
0
T=0
T=0必败,否则必胜。
题意
给 n n n个房间,每个房间有 k k k堆石头,具体的每一堆有 k i k_i ki堆石头, A l i c e , B o b Alice,Bob Alice,Bob可以轮流在任意房间取第任意堆的任意个石头,最后无法取石头的输掉游戏。规定必须在一个房间取完才可以去另外一个房间取, A l i c e , B o b Alice,Bob Alice,Bob都以最优状态取,但是 A l i c e Alice Alice 可以规定房间顺序,问有多少种房间的排列让 A l i c e Alice Alice必胜,答案对1e9 + 7取模。
Hint1:先弱化问题,考虑所有房间的所有堆都只有1个石头,这个问题比较显然。我们定义奇数房间为有奇数个堆的房间,反之亦然。当有奇数个奇数房间时,必胜,反之必败。
Hint2:偶数房间显然对答案没有影响,因为这不会反转先后手。
Hint3:现在考虑存在房间里面至少有一个多于一个石头的堆。这个是否会对先后手反转有一定影响?
题解
全为1的情况已经判完。现在考虑不全为1的同时不考虑偶数房间,因为它对答案没有影响我们可以随便放。有两种可能一个是
T
=
0
T=0
T=0的房间,我们定义这种房间为坏房间,反之为好房间。如果我们碰到的第一个不全为1的房间为好房间,我们发现无论是Nim还是anti_nim,它都是必胜的,即先手可以决定后面的出手顺序,这就是必胜态,故它前面的奇数房间不能反转出手顺序,即前面只能有偶数个奇数房间。反之,如果我们碰到的第一个不全为1的房间为坏房间,我们发现无论是Nim还是anti_nim,它都是必败的,即后手可以决定后面的出手顺序,这就是必败态,故它前面的奇数房间必须要反转出手顺序,即前面得有奇数个奇数房间来进行反转。
我们考虑枚举前面有多少个奇数房间统计答案。
代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
constexpr int inf = 1e18;
constexpr int mod = 1e9 + 7;
constexpr int N = 1e6 + 10;
int qpow(int base, int index) {
int res = 1;
while(index) {
if(index & 1) res = res * base % mod;
base = base * base % mod;
index >>= 1;
}
return res;
}
int f[N];
void fac() {
f[0] = 1;
for(int i = 1; i < N; ++i) {
f[i] = i * f[i - 1] % mod;
}
}
int comb(int x, int y) {
return f[x] * qpow(f[y], mod - 2) % mod * qpow(f[x - y], mod - 2) % mod;
}
void solve() {
int n;
cin >> n;
array<int, 4> cnt = {};
for(int i = 0; i < n; ++i) {
int k; cin >> k;
bool ok = true;
int s = 0;
for(int j = 0; j < k; ++j) {
int x; cin >> x;
s ^= x;
if(x > 1) ok = false;
}
if(ok) {
if(s) cnt[0]++;
else cnt[1]++;
} else {
if(s) cnt[2]++;
else cnt[3]++;
}
}
if(cnt[0] + cnt[1] == n) {
if(cnt[0] & 1) {
cout << f[n] << '\n';
} else cout << "0\n";
return ;
}
// 放多少个奇数在前面
int ans = 0;
for(int i = 0; i <= cnt[0]; ++i) {
int temp = 1;
if(i & 1) {
temp = f[i] * comb(cnt[0], i) % mod * cnt[3] % mod;
temp = temp * f[cnt[2] + cnt[3] - 1 + cnt[0] - i] % mod;
} else {
temp = f[i] * comb(cnt[0], i) % mod * cnt[2] % mod;
temp = temp * f[cnt[2] + cnt[3] - 1 + cnt[0] - i] % mod;
}
ans = (ans + temp) % mod;
}
// 偶数随便放。
for(int i = 1; i <= cnt[1]; ++i) {
ans = (ans * (n - cnt[1] + i)) % mod;
}
cout << ans << '\n';
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
fac();
int t = 1;
cin >> t;
while(t--) {
solve();
}
return 0;
}