Frogs - HDU5514
icpc沈阳2015的题目。
容斥原理和数论的应用。
题目大意为给你一个数组 a [ i ] a[i] a[i]( a [ i ] a[i] a[i] 即为第 i i i 只青蛙的步长) 和若干格子编号为 0 0 0 到 m − 1 m - 1 m−1,对于任意的正整数 k , i k, i k,i,编号为 k ∗ a [ i ] m o d m k * a[i] \mod m k∗a[i]modm 的格子会被占用,问所有被占用的格子的编号之和。
首先对于一个 a [ i ] a[i] a[i],显然所有编号为 gcd ( a [ i ] , m ) \gcd(a[i], m) gcd(a[i],m) 的倍数的格子会被占用,抛弃掉之前的 a [ i ] a[i] a[i] ,直接将 gcd ( a [ i ] , m ) \gcd(a[i], m) gcd(a[i],m) 设为新的 a [ i ] a[i] a[i],这样的话,我们记 a [ i ] a[i] a[i] 的倍数组成的集合为 A i A_i Ai,那么最终答案为 S ( A 1 ∪ A 2 ∪ ⋯ ∪ A n ) S(A_1 \cup A_2 \cup \dots \cup A_n) S(A1∪A2∪⋯∪An),其中 S ( A ) S(A) S(A) 为集合 A A A 中所有元素的和。
这个东西显然可以用容斥做,我们记集合 { A 1 , A 2 , … , A n } \{A_1, A_2, \dots, A_n\} {A1,A2,…,An} 的幂集为 U U U,那么我们可以枚举 U U U 中所有元素,写出来容斥原理的公式:
S ( ⋃ i = 1 n A i ) = ∑ B ∈ P ( − 1 ) ∣ B ∣ − 1 S ( ⋂ A i ∈ B A i ) S(\bigcup_{i = 1}^nA_i) = \sum_{B \in P}(-1)^{|B| - 1}S(\bigcap_{A_i \in B}A_i) S(i=1⋃nAi)=B∈P∑(−1)∣B∣−1S(Ai∈B⋂Ai)
(不得不说容斥原理的公式写成这个样子真的简洁明了又暴力,直接枚举了 2 n 2^n 2n 种集合)
首先需要明确的是,我们使用容斥原理一定是要有一个出发点的,就是对于某一类集合,我们可以快速统计出来这一类集合的答案之和,因为我们不可能花费 O ( 2 n ) O(2^n) O(2n) 的时间复杂度去枚举所有 A i A_i Ai 的交集情况,在集合数繁多的情况下我们只能对这些集合进行分类合并。
对于本题来说,这个分类方法可以很自然地得出。
我们思考若干集合的交,例如 A 1 ∩ A 3 ∩ A 6 A_1 \cap A_3 \cap A_6 A1∩A3∩A6,那么在这个集合中的元素一定是 a [ 1 ] , a [ 3 ] , a [ 6 ] a[1], a[3], a[6] a[1],a[3],a[6] 的公倍数,即一定是 l c m ( a [ 1 ] , a [ 3 ] , a [ 6 ] ) lcm(a[1], a[3], a[6]) lcm(a[1],a[3],a[6]) 的倍数,并且 0 0 0 到 m m m 内所有 a [ 1 ] , a [ 3 ] , a [ 6 ] a[1], a[3], a[6] a[1],a[3],a[6] 的公倍数一定在这个集合中。
对于任意一组集合的并皆是如此,并且,我们不难发现,由于 a [ i ] a[i] a[i] 都是 m m m 的因数,那么若干个 a [ i ] a[i] a[i] 的最小公倍数也一定是 m m m 的因数。
所以,我们不妨把集合按照最小公倍数分类来进行容斥。
首先,根据上面的结论,我们有:
S ( ⋂ A i ∈ B A i ) = 1 2 l m l ( m l − 1 ) = 1 2 m ( m l − 1 ) S(\bigcap_{A_i \in B} A_i) = \frac 12 l\frac ml(\frac ml - 1) = \frac 12m(\frac ml - 1) S(Ai∈B⋂Ai)=21llm(lm−1)=21m(lm−1)
其中 l l l 为所有 A i ∈ B A_i \in B Ai∈B 的 i i i 中 a [ i ] a[i] a[i] 的最小公倍数。
我们不妨设
l = L C M ( B ) = l c m A i ∈ B a [ i ] l = LCM(B) = {lcm}_{A_i \in B} a[i] l=LCM(B)=lcmAi∈Ba[i]
那么我们就可以以对集合按照最小公倍数分类的方式重写容斥原理。
S ( ⋃ i = 1 n A i ) = ∑ l ∣ m ∑ B ∈ P [ L C M ( B ) = l ] ( − 1 ) ∣ B ∣ − 1 S ( ⋂ A i ∈ B A i ) S(\bigcup_{i = 1}^nA_i) = \sum_{l|m}\sum_{B \in P}[LCM(B) = l](-1)^{|B| - 1}S(\bigcap_{A_i \in B}A_i) S(i=1⋃nAi)=l∣m∑B∈P∑[LCM(B)=l](−1)∣B∣−1S(Ai∈B⋂Ai)
= ∑ l ∣ m ∑ B ∈ P [ L C M ( B ) = l ] ( − 1 ) ∣ B ∣ − 1 1 2 m ( m l − 1 ) = \sum_{l|m}\sum_{B \in P}[LCM(B) = l](-1)^{|B| - 1}\frac 12m(\frac ml - 1) =l∣m∑B∈P∑[LCM(B)=l](−1)∣B∣−121m(lm−1)
= 1 2 m ∑ l ∣ m ( m l − 1 ) ∑ B ∈ P [ L C M ( B ) = l ] ( − 1 ) ∣ B ∣ − 1 = \frac 12m\sum_{l|m}(\frac ml - 1)\sum_{B \in P}[LCM(B) = l](-1)^{|B| - 1} =21ml∣m∑(lm−1)B∈P∑[LCM(B)=l](−1)∣B∣−1
接下来,我们只需要考虑如何计算
∑ B ∈ P [ L C M ( B ) = l ] ( − 1 ) ∣ B ∣ − 1 \sum_{B \in P}[LCM(B) = l](-1)^{|B| - 1} B∈P∑[LCM(B)=l](−1)∣B∣−1
这一坨式子。
这里我们需要用到一个结论,也就是非常出名的莫比乌斯反演公式:
如果有函数 f ( n ) , g ( n ) f(n), g(n) f(n),g(n) 满足:
g ( n ) = ∑ d ∣ n f ( d ) g(n) = \sum_{d|n}f(d) g(n)=d∣n∑f(d)
那么他们同时也满足:
f ( n ) = ∑ d ∣ n μ ( n d ) g ( d ) f(n) = \sum_{d|n}\mu(\frac nd)g(d) f(n)=d∣n∑μ(dn)g(d)
这是一个非常非常非常有用的结论,他可以允许我们在枚举最大公约数或最小公倍数时去掉最大/最小,改为枚举公约数或者公倍数。这里的 μ \mu μ 是莫比乌斯函数,关于莫比乌斯反演在我另一篇博客中有讲解,可以移步去看,不过这里我们只需要知道这个结论就行了。
在这里,我们不妨设
f ( l ) = ∑ B ∈ P [ L C M ( B ) = l ] ( − 1 ) ∣ B ∣ − 1 f(l) = \sum_{B \in P}[LCM(B) = l](-1)^{|B| - 1} f(l)=B∈P∑[LCM(B)=l](−1)∣B∣−1
那么我们不难发现对于
g ( n ) = ∑ d ∣ n f ( d ) g(n) = \sum_{d|n}f(d) g(n)=d∣n∑f(d)
来说, f ( n ) f(n) f(n) 表示所有最小公倍数为 n n n 的集合 B B B 的 ( − 1 ) ∣ B ∣ − 1 (-1)^{|B| - 1} (−1)∣B∣−1 的和,那么 g ( n ) g(n) g(n) 即表示所有公倍数为 n n n 的集合 B B B 的 ( − 1 ) ∣ B ∣ − 1 (-1)^{|B| - 1} (−1)∣B∣−1 的和。
我们通过求出来 g ( n ) g(n) g(n) 的式子,然后利用莫比乌斯反演的公式来间接求出 f ( n ) f(n) f(n) 的式子。
g ( n ) g(n) g(n) 的式子非常好求,我们只需要知道数组 a [ i ] a[i] a[i] 中为 n n n 的约数的数的个数即可,假设个数为 x x x。
那么这些约数中任意若干个约数组成的集合都满足公约数为 n n n,即
g ( n ) = ∑ i = 1 x ( x i ) ( − 1 ) i − 1 = 1 − ( 1 − 1 ) x g(n) = \sum_{i = 1}^x{x \choose i}(-1)^{i - 1} = 1 - (1 - 1)^x g(n)=i=1∑x(ix)(−1)i−1=1−(1−1)x
我们可以得到,当数组 a [ i ] a[i] a[i] 中为 n n n 的约数的个数为 0 0 0 时, g ( n ) = 0 g(n) = 0 g(n)=0,否则 g ( n ) = 1 g(n) = 1 g(n)=1。
我们把这个式子代入原式:
S ( ⋃ i = 1 n A i ) = 1 2 m ∑ l ∣ m ( m l − 1 ) f ( l ) S(\bigcup_{i = 1}^nA_i) = \frac 12m\sum_{l|m}(\frac ml - 1)f(l) S(i=1⋃nAi)=21ml∣m∑(lm−1)f(l)
= 1 2 m ∑ l ∣ m ( m l − 1 ) ∑ d ∣ l μ ( l d ) g ( d ) = \frac 12m\sum_{l|m}(\frac ml - 1)\sum_{d|l}\mu(\frac ld)g(d) =21ml∣m∑(lm−1)d∣l∑μ(dl)g(d)
我们不妨交换一下求和顺序,将 g ( d ) g(d) g(d) 提前,得到
S ( ⋃ i = 1 n A i ) = 1 2 m ∑ d ∣ m g ( d ) ∑ l ∣ m d μ ( l ) ( m d l − 1 ) S(\bigcup_{i = 1}^nA_i) = \frac 12m\sum_{d|m}g(d)\sum_{l|\frac md}\mu(l)(\frac m{dl} - 1) S(i=1⋃nAi)=21md∣m∑g(d)l∣dm∑μ(l)(dlm−1)
我们设
T ( n ) = ∑ d ∣ n μ ( d ) ( n d − 1 ) = ∑ d ∣ n μ ( d ) n d − ∑ d ∣ n μ ( d ) = ϕ ( n ) − e ( n ) T(n) = \sum_{d|n}\mu(d)(\frac nd - 1) = \sum_{d|n}\mu(d)\frac nd - \sum_{d|n}\mu(d) = \phi(n) - e(n) T(n)=d∣n∑μ(d)(dn−1)=d∣n∑μ(d)dn−d∣n∑μ(d)=ϕ(n)−e(n)
这里的 ϕ ( n ) \phi(n) ϕ(n) 是欧拉函数, e ( n ) e(n) e(n) 是单位函数,仅当 n = 1 n = 1 n=1 时 e ( n ) = 1 e(n) = 1 e(n)=1,否则 e ( n ) = 0 e(n) = 0 e(n)=0。
如果你不知道什么是欧拉函数或者单位函数,那你就把它当做一个普通积性函数就可以了,我们仍可以通过积性函数的性质用筛法筛出来函数的值。
所以,最终答案为:
S ( ⋃ i = 1 n A i ) = 1 2 m ∑ d ∣ m g ( d ) T ( n d ) S(\bigcup_{i = 1}^nA_i) = \frac 12m\sum_{d|m}g(d)T(\frac nd) S(i=1⋃nAi)=21md∣m∑g(d)T(dn)
= 1 2 m ∑ d ∣ m g ( d ) [ ϕ ( n d ) − e ( n d ) ] = \frac 12m\sum_{d|m}g(d)[\phi(\frac nd) - e(\frac nd)] =21md∣m∑g(d)[ϕ(dn)−e(dn)]
这样,我们就可以先求出来 m m m 的所有约数,然后对于每个约数 d d d,求出来 g ( d ) , ϕ ( d ) g(d), \phi(d) g(d),ϕ(d) 的值。
时间复杂度为 O ( d ( m ) n ) O(d(m)n) O(d(m)n),其中 d ( m ) d(m) d(m) 为 m m m 的因数个数,这个值很小,最大只有不到 4000 4000 4000 个(据说)。
然而最终代码还是跑得很快的,甚至只需要 71 m s 71ms 71ms。
#include <bits/stdc++.h>
const int MAXN = 1e5 + 5;
int a[MAXN], ddiv[MAXN], g[MAXN], phi[MAXN], n, m, cnt, prime[MAXN], cnt2;
int find(int x) {
return std::lower_bound(ddiv + 1, ddiv + cnt + 1, x) - ddiv;
}
int e(int x) {
return x == 1;
}
void solve(int T) {
scanf("%d%d", &n, &m);
cnt = cnt2 = 0;
/*memset(g, 0, sizeof(g));
memset(phi, 0, sizeof(phi));
memset(ddiv, 0, sizeof(ddiv));
memset(prime, 0, sizeof(prime));*/
for(int i = 1; i <= n; i++) {
int x;
scanf("%d", &x);
a[i] = std::__gcd(x, m);
}
std::sort(a + 1, a + n + 1);
n = std::unique(a + 1, a + n + 1) - a - 1;
for(int i = 1; i * i <= m; i++) {
if(m % i != 0) continue;
int d1 = i, d2 = m / i;
ddiv[++cnt] = d1;
if(d1 != d2) ddiv[++cnt] = d2;
}
std::sort(ddiv + 1, ddiv + cnt + 1);
phi[1] = 1;
for(int i = 2; i <= cnt; i++) {
phi[i] = ddiv[i] - 1;
for(int j = 1; j <= cnt2; j++) {
if(ddiv[i] % prime[j] != 0) continue;
int t1 = ddiv[i] / prime[j];
if(t1 % prime[j] == 0) phi[i] = phi[find(t1)] * prime[j];
else phi[i] = phi[find(t1)] * (prime[j] - 1);
}
if(phi[i] == ddiv[i] - 1) prime[++cnt2] = ddiv[i];
}
for(int i = 1; i <= cnt; i++) {
g[i] = 0;
for(int j = 1; j <= n; j++)
if(ddiv[i] % a[j] == 0) {g[i] = 1; break;}
}
long long ans = 0;
for(int i = 1; i <= cnt; i++) ans = ans + 1LL * g[i] * (phi[find(m / ddiv[i])] - (i == cnt));
ans = ans * m / 2;
printf("Case #%d: %lld\n", T, ans);
}
int main() {
int T;
scanf("%d", &T);
for(int i = 1; i <= T; i++) solve(i);
return 0;
}