蓝桥杯2024年第十五届省赛A组-因数计数

题目描述

小蓝随手写出了含有 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,得 m=\sum_{i=1}^{P} t_x \times b_x,之后再平方便是 i ≠ k, j ≠ l 且 ai ∣ ak, aj ∣ al  的四元组 (i, j, k, l) 组数。

然后计算需要容斥减去的组数。当 i = j 时,较小数相等,枚举这个数 x,取两个倍数,组数为 \sum_{i=1}^{P} t_x \times b_x^2。当 k = l 时,较大数相等,枚举这个数 x,取两个因数,组数为 \sum_{i=1}^{P} t_x \times s_x^2。当 i = l 或 j = k 时,即一组中较大数与另一组中较小数相等,枚举相等的中间值 x,取一个因数和一个倍数,组数为 \sum_{i=1}^{P} 2\left ( t_x \times b_x \times s_x \right )

最后计算需要容斥加上的组数。当 i = j 且 k = l 时,此时两个较小数和两个较大数分别相等,组数即为最初求出的二元组 (i, j) 数量 m。当 i = l 且 j = k 时,由于倍数的要求限制了 i ≤ k, j ≤ l,所以此时 i, j, k, l 均相等,枚举相等值 x,组数为 \sum_{i=1}^{P} t_x\left ( t_x - 1 \right )。其他四种情况都会产生 i = k 或 j = l,与已经确定的条件 i ≠ k, j ≠ l 冲突,所以组数为 0。之后满足三个或四个条件也会产生同样的冲突,组数均为 0,所以不用处理,容斥结束。

注意:由于 O(n^4) 达到了 10^20 级别,开 __int128 才能确保完全通过。

具体步骤

1. 按要求读入数据,运用数组模拟桶来统计输入数字出现的次数。

2. 在遍历桶的时候计算数字的因数个数和倍数个数,并以此计算出基本的因数对个数。

3. 运用容斥原理分别减去和加上相应的重复组数,求得最终答案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值