题目描述
小蓝随手写出了含有 n 个正整数的数组 {a1, a2, · · · , an} ,他发现可以轻松地算出有多少个有序二元组 (i, j) 满足 aj 是 ai 的一个因数。因此他定义一个整数对 (x1, y1) 是一个整数对 (x2, y2) 的“因数”当且仅当 x1 和 y1 分别是 x2 和 y2的因数。他想知道有多少个有序四元组 (i, j, k, l) 满足 (ai, aj) 是 (ak, al) 的因数,其中 i, j, k, l 互不相等。
输入格式
输入的第一行包含一个正整数 n 。第二行包含 n 个正整数 a1, a2, · · · , an ,相邻整数之间使用一个空格分隔。
输出格式
输出一行包含一个整数表示答案。
样例输入
5
3 6 2 2 7
样例输出
4
提示
【样例说明】
四元组 (1, 4, 2, 3) :(3, 2) 为 (6, 2) 的因子;四元组 (1, 3, 2, 4) :(3, 2) 为 (6, 2)的因子;四元组 (4, 1, 3, 2) :(2, 3) 为 (2, 6) 的因子;四元组 (3, 1, 4, 2) :(2, 3) 为 (2, 6) 的因子。
【评测用例规模与约定】
对于 20% 的评测用例,n ≤ 50 ;
对于 40% 的评测用例,n ≤ 10^4;
对于所有评测用例,1 ≤ n ≤ 10^5 ,1 ≤ ai ≤ 10^5 。
错误解法
这道题目最直观的想法便是暴力求解,但伴随而来的便是极高的时间复杂度。思路是先循环遍历数组,判断因数条件并找出所有的有序二元组对,用集合进行存储;之后遍历集合,以 i, j, k, l 互不相等为条件,生成有序四元组,计算四元组个数。
注意:在暴力求解时不要使用 long long 类型,不然会导致部分测试点超时。
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n + 1);
for(int i = 1; i <= n; i++)
{
cin >> a[i];
}
set<pair<int, int>> s;
for(int i = 1; i <= n; i++)
{
for(int j = i + 1; j <= n; j++)
{
if(a[i] % a[j] == 0)
{
s.insert({j, i});
}
if(a[j] % a[i] == 0)
{
s.insert({i, j});
}
}
}
int cnt = 0;
for(const auto& t1 : s)
{
for(const auto& t2 : s)
{
if(t1.first != t2.first && t1.first != t2.second && t1.second != t2.first && t1.second != t2.second)
{
cnt++;
}
}
}
cout << cnt << endl;
return 0;
}
可以粗略计算一下这个暴力解法的时间复杂度。显然在生成因数对集合时时间复杂度是 O(n^2);在统计四元组数量时,最坏情况下每个元素都与其他元素形成因数对,因此集合 s 的大小可能接近 O(n^2),这个步骤的时间复杂度是 O(n^4)。一般认为计算器一秒能进行 10^8 次计算。O(n^4) 数据范围大致为 n = 70,所以根据评测用例规模此做法只能得到 30~40 分。
整体思路
由于只用计算四元组的个数,不用计算具体是哪些四元组,考虑数论中的容斥原理,运用排列组合先计算出基本因数对数,之后再排除重复和无效情况。
#include <bits/stdc++.h>
#define endl '\n'
#define int __int128
using namespace std;
/*
t[x]:表示数字 x 在输入中出现的次数
s[x]:表示数字 x 的不同因数的个数(不包括 x 本身)
b[x]:表示数字 x 的倍数的个数
*/
const int N = 1e5 + 5;
const int P = 1e5;
int t[N], s[N], b[N];
int read()
{
int x = 0, f = 1;
char ch = getchar();
while(ch < '0' || ch > '9')
{
if(ch == '-')
{
f = -1;
}
ch = getchar();
}
while(ch >= '0' && ch <= '9')
{
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
void print(int x)
{
if(x < 0)
{
putchar('-');
x *= -1;
}
if(x > 9)
{
print(x / 10);
}
putchar(x % 10 + '0');
}
signed main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
n = read();
for(int i = 1; i <= n; i++)
{
t[read()]++;
}
int ans = 0;
for(int i = 1; i <= P; i++)
{
if(t[i] == 0)
{
continue;
}
for(int j = i * 2; j <= P; j += i)
{
if(t[j] == 0)
{
continue;
}
s[j] += t[i];
b[i] += t[j];
}
b[i] += t[i] - 1;
s[i] += t[i] - 1;
ans += t[i] * b[i];
}
ans *= (ans + 1); // 加 1 是为了后面的补上 i = j && k = l
for(int i = 1; i <= P; i++)
{
if(t[i] == 0)
{
continue;
}
ans -= t[i] * b[i] * b[i]; // 减去 i = j
ans -= t[i] * s[i] * s[i]; // 减去 k = l
ans -= 2 * t[i] * b[i] * s[i]; // 减去 i = l || j = k
ans += t[i] * (t[i] - 1); // 补上 i = l && j = k
}
print(ans);
return 0;
}
公式推导
先算出 i ≠ j 且 ai ∣ aj 的 (i, j) 数量 m,得 ,之后再平方便是 i ≠ k, j ≠ l 且 ai ∣ ak, aj ∣ al 的四元组 (i, j, k, l) 组数。
然后计算需要容斥减去的组数。当 i = j 时,较小数相等,枚举这个数 x,取两个倍数,组数为 。当 k = l 时,较大数相等,枚举这个数 x,取两个因数,组数为
。当 i = l 或 j = k 时,即一组中较大数与另一组中较小数相等,枚举相等的中间值 x,取一个因数和一个倍数,组数为
。
最后计算需要容斥加上的组数。当 i = j 且 k = l 时,此时两个较小数和两个较大数分别相等,组数即为最初求出的二元组 (i, j) 数量 m。当 i = l 且 j = k 时,由于倍数的要求限制了 i ≤ k, j ≤ l,所以此时 i, j, k, l 均相等,枚举相等值 x,组数为 。其他四种情况都会产生 i = k 或 j = l,与已经确定的条件 i ≠ k, j ≠ l 冲突,所以组数为 0。之后满足三个或四个条件也会产生同样的冲突,组数均为 0,所以不用处理,容斥结束。
注意:由于 O(n^4) 达到了 10^20 级别,开 __int128 才能确保完全通过。
具体步骤
1. 按要求读入数据,运用数组模拟桶来统计输入数字出现的次数。
2. 在遍历桶的时候计算数字的因数个数和倍数个数,并以此计算出基本的因数对个数。
3. 运用容斥原理分别减去和加上相应的重复组数,求得最终答案。