网易游戏笔试题

题意是给出一个数组,要求三元组(a,b,c),使得gcd(a,b,c)=1,求这些三元组的数量。数组大小和数的范围都是1到1e5。

一开始想的是基本的容斥原理,后来大佬说到可以直接套莫比乌斯反演,没想到有限域学到的莫比乌斯反演能这样用(查了一下才发现是基本NOI,ACM的模板题了,之前还没见过orz)。

学习了一下,首先先简单介绍一下莫比乌斯反演。其公式表示如下:

证明过程就简略了,主要思想就是将一个较难做的$a(n)$函数,转化成较简单的$b(n)$去求解,有限域上给的应用例子是欧拉函数相关的, ,因为直接计算欧拉函数复杂度稍高,那么就可以通过转化来计算欧拉函数

介绍最直接的莫比乌斯反演入门题,就是给两个区间,从每个区间各取一个数,求有多少对互素的数。直接求的数量复杂度较高,那么可以转换一下求的数量,因为那是可以直接求到的,如枚举$k=2$时,就只需要求两个区间内的偶数数量,然后做组合数即可,两次除法一次乘法,从而从简单问题的角度解决了较难的问题(此处扩展来说就不一定是素数了,把1改成其他数即可)。唔 可能有人会问对应的$n$和$d$是什么,我也有这样的疑问,而我的想法是,$n$甚至可以是那段阶乘,所以实际计算过程需要遍历区间内每一个数。

那么回到笔试题目来,题目给出的是可能包含重复数的正整数数组,要求三元组,那么就使用同样的套路即可,尽管不是连续的整数,但是函数的表达还是一样的要求$gcd(a,b,c)==1$的数量,转换成求$1 | gcd(a,b,c)$的数量,然后套上莫比乌斯反演公式即可。而因为此处不是连续的数字,所以可能要花点功夫,因为数的范围还是1e5,因此可以将数都放到桶里面方便取,然后间隔着去找$gcd(a,b,c)=k % 1 == 0$的数量,如要$k$的,那就从$k$开始,取$k$, $2k$, $3k$的数即可,这里要注意一点就是,我们求的是$cd(a,b,c)=k % 1 == 0$的数量,因此尽管$buckets[k]$可能为0,但是那一系列数还是能取到满足要求的,那么是否会重复计数呢?公式里面其实就已经考虑了这个问题,所以不用担心(逃)。

代码如下,添加了一个暴力的用于检验答案对错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

#include <algorithm>
#include <vector>
#include <ctime>
#include <cstring>
#include <cstdlib>
using namespace std;



int (int x, int y)
{
if (x % y == 0)
return y;
else
return gcd(y, x % y);
}

int baoli(const vector<int> &arr)
{
int ans = 0;
for (int i = 0; i < arr.size(); ++i)
{
for (int j = i + 1; j < arr.size(); ++j)
{
for (int k = j + 1; k < arr.size(); ++k)
{
if (gcd(gcd(arr[i], arr[j]), arr[k]) == 1)
{
ans++;
}
}
}
}
return ans;
}

const int maxn = 100000 + 10;
int buckets[maxn];
int mu[maxn], vis[maxn], primes[maxn];
int cnt;
void getmu()
{
memset(vis, 0, sizeof(vis));
memset(mu, 0, sizeof(mu));
cnt = 0;
mu[1] = 1;
for (int i = 2; i <= maxn; ++i)
{
if (!vis[i])
{
primes[cnt++] = i;
mu[i] = -1;
}
for (int j = 0; j < cnt && primes[j] * i <= maxn; ++j)
{
vis[primes[j] * i] = 1;
if (i % primes[j] == 0)
break;
mu[i * primes[j]] = -mu[i];
}
}
}

int maxi = 0, n = 0;
long long solve()
{
long long f = 0;
for (int i = 1; i <= maxi; ++i)
{
long long p = 0;
for (int j = i; j <= maxi; j += i)
{
p += buckets[j];
}
f += mu[i] * p * (p - 1) * (p - 2) / 6;
}
return f;
}

int main()
{
getmu();
cin >> n;
vector<int> arr(n);
memset(buckets, 0, sizeof(buckets));
srand(0);
for (int i = 0; i < n; ++i)
{
// cin >> arr[i];
arr[i] = rand() % 100000 + 1;
buckets[arr[i]] += 1;
maxi = max(maxi, arr[i]);
// cout << arr[i] << " ";
}
cout << endl;
// cout << baoli(arr) << endl;
cout << solve() << endl;

return 0;
}

当然了,莫比乌斯反演其实也是对容斥问题的求解思路,因此,这道题也可以按照这个思路有容斥问题的解法,具体应该是选出3元组的方式的数量减去gcd不为1的数量,而遇到2,3得6这种情形,就要把6的那部分加回来(很像$mu$函数了…)。

还有一个算法复杂度的问题,就是solve那两重循环的复杂度应该是多少,实际上可以发现第二重循环时$ln(n)$的泰勒展开,所以整个复杂度时$O(nlog(n))$的,而莫比乌斯反演需要用到的$mu$的计算方法时$O(n)$的,所以整个算法复杂度是$O(nlog(n))$。