原题传送门
题意:
有n只青蛙,m块石头呈环放置(从0~m-1标记),第i只青蛙每次跳ai步,跳跃次数无限制。求被n只青蛙踩过的石头的下表和
思路1(欧拉函数):
很明显,对于第i只青蛙,被踩的石头的编号一定是gcd(ai, m)的倍数,那么我们先将ai转换为gcd(ai,m)
然后对于第 j 块石头,我们定义它只能被跳跃步数为x的青蛙踩中(x满足gcd(m,j)= x)例如下面的样例:
2 12
9 10
那么这个样例之中被踩过并且有贡献的石头编号为2,3,4,6,8,9,10
按照我们的定义
2 10 是被跳跃步数为2的青蛙踩过
3 9 是被跳跃步数为3的青蛙踩过
4 8 是被跳跃步数为4的青蛙踩过
6 是被跳跃步数为6的青蛙踩过
这里的青蛙只有两个,但是我们可以造两个步数为4 和 6的青蛙,这对答案是完全没有影响的
因此 x 的答案贡献为 m以内 ∑ j [gcd(m,j)== x]
由gcd(m,j)= x,可以得到gcd(m/x,j/x)= 1,即(j/x)为(m/x)内所有与(m/x)互素的数
那么上面对于x的答案贡献就转换为 x * ∑ i [gcd(m/x,i)== 1]
知识点:设小于n的所有与n互质的数的和为Sum,Sum=n∗φ(n)/2
证明:
1. gcd(x,n)= 1,那么gcd(n-x,n)= 1同样满足
2. 可见n以内与n互素的数字都是成对出现的,而素数对的个数就是φ(n)/2,并且每一对素数和为n
证毕
由此x的答案再次变换为
x *(φ(m/x)/ 2 *(m/x))
稍稍化简得到
φ(m/x)/ 2*m
到这里就可以直接枚举m的因子,看跳跃步数为当前的因子fac青蛙能否被构造(fac % ai == 0)即可
AC代码1:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e4 + 9;
int a[N], fac[N];
int Euler (int n) {
ll res = n;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
res = res / i * (i - 1);
while (n % i == 0) {
n /= i;
}
}
}
if (n > 1) {
res = res / n * (n - 1);
}
return res;
}
int get_fac(int n) {
int len = 0;
for (int i = 2; i * i <= n; i ++) {
if (n % i == 0) {
fac[len++] = i;
if (i * i != n) {
fac[len++] = n / i;
}
}
}
sort (fac, fac + len);
return len;
}
int main() {
int T, cnt = 1;
scanf ("%d", &T);
while (T --) {
int n, m;
bool flag = false;
scanf ("%d%d", &n, &m);
int len = get_fac(m);
for (int i = 0; i < n; i ++) {
scanf ("%d", a + i);
a[i] = __gcd(a[i], m);
if (a[i] == 1) {
flag = true;
}
}
ll ans = 0;
for (int i = 0; i < len; i ++) {
for (int j = 0; j < n; j ++) {
if (fac[i] % a[j] == 0) {
ans += 1ll * Euler(m/fac[i]) * m / 2 ;
break;
}
}
}
printf ("Case #%d: ", cnt ++);
if (flag) {
cout << 1ll * m * (m - 1) / 2 << endl;
} else {
cout << ans << endl;
}
}
return 0;
}
思路2(容斥原理):
首先预处理出m的所有因子,每个因子的答案贡献次数初始化为1,然后根据容斥原理去调节后续每个因子的答案贡献次数。例如下面的样例:
2 24
9 10
24的所有因子为 2,3,4,6,8,12
初始化所有因子的答案贡献为1
计算2的答案贡献的时候,2的答案贡献次数为1,因此4,6,8,12的答案贡献次数 -1
计算3的答案贡献的时候,3的答案贡献次数为1,6和12的答案贡献次数-1
计算4的答案贡献的时候,4的答案贡献次数为1,8和12的答案贡献次数-1
(!高潮来了!)
计算6的答案贡献的时候,答案贡献次数为-1,这个时候12的答案贡献次数 -(-1)也就是+1
计算8的答案贡献的时候,答案贡献次数为-1,后续因子没有8的倍数,因此无须调节其他因子的答案贡献次数
计算12的答案贡献的时候,答案贡献次数为-1,与上面同理
(容斥原理真奇妙)
AC代码2:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e4 + 9;
int a[N], vis[N], fac[N];
int init(int n) {
memset (vis, 0, sizeof vis);
int len = 0;
for (int i = 2; i * i <= n; i ++) {
if (n % i == 0) {
fac[len++] = i;
if (i * i != n) {
fac[len++] = n / i;
}
}
}
sort (fac, fac + len);
return len;
}
int main() {
int T, cnt = 1;
scanf ("%d", &T);
while (T --) {
int n, m;
bool flag = false;
scanf ("%d%d", &n, &m);
int len = init(m);
for (int i = 0; i < n; i ++) {
scanf ("%d", a + i);
a[i] = __gcd(a[i], m);
if (a[i] == 1) {
flag = true;
}
for (int j = 0; j < len; j ++) {
if (fac[j] % a[i] == 0) {
vis[j] = 1;
}
}
}
ll ans = 0;
for (int i = 0; i < len; i ++) {
ll res = m / fac[i];
ans += res * (res - 1) / 2 * fac[i] * vis[i];
for (int j = i + 1; j < len; j ++) {
if (fac[j] % fac[i] == 0) {
vis[j] -= vis[i];
}
}
}
printf ("Case #%d: ", cnt ++);
if (flag) {
cout << 1ll * m * (m - 1) / 2 << endl;
} else {
cout << ans << endl;
}
}
return 0;
}
(PS.以上的答案贡献计算都是O(1)的)