[GESP202506 五级] 最大公因数
题目描述
对于两个正整数 a,ba,ba,b,他们的最大公因数记为 gcd(a,b)\gcd(a,b)gcd(a,b)。对于 k>3k > 3k>3 个正整数 c1,c2,…,ckc_1,c_2,\dots,c_kc1,c2,…,ck,他们的最大公因数为:
gcd(c1,c2,…,ck)=gcd(gcd(c1,c2,…,ck−1),ck)\gcd(c_1,c_2,\dots,c_k)=\gcd(\gcd(c_1,c_2,\dots,c_{k-1}),c_k)gcd(c1,c2,…,ck)=gcd(gcd(c1,c2,…,ck−1),ck)
给定 nnn 个正整数 a1,a2,…,ana_1,a_2,\dots,a_na1,a2,…,an 以及 qqq 组询问。对于第 i(1≤i≤q)i(1 \le i \le q)i(1≤i≤q) 组询问,请求出 a1+i,a2+i,…,an+ia_1+i,a_2+i,\dots,a_n+ia1+i,a2+i,…,an+i 的最大公因数,也即 gcd(a1+i,a2+i,…,an+i)\gcd(a_1+i,a_2+i,\dots,a_n+i)gcd(a1+i,a2+i,…,an+i)。
输入格式
第一行,两个正整数 n,qn,qn,q,分别表示给定正整数的数量,以及询问组数。
第二行,nnn 个正整数 a1,a2,…,ana_1,a_2,\dots,a_na1,a2,…,an。
输出格式
输出共 qqq 行,第 iii 行包含一个正整数,表示 a1+i,a2+i,…,an+ia_1+i,a_2+i,\dots,a_n+ia1+i,a2+i,…,an+i 的最大公因数。
输入输出样例 #1
输入 #1
5 3
6 9 12 18 30
输出 #1
1
1
3
输入输出样例 #2
输入 #2
3 5
31 47 59
输出 #2
4
1
2
1
4
说明/提示
对于 60%60\%60% 的测试点,保证 1≤n≤1031 \le n \le 10^31≤n≤103,1≤q≤101 \le q \le 101≤q≤10。
对于所有测试点,保证 1≤n≤1051 \le n \le 10^51≤n≤105,1≤q≤1051 \le q \le 10^51≤q≤105,1≤ai≤10001 \le a_i \le 10001≤ai≤1000。
提交链接
https://www.luogu.com.cn/problem/P13014
思路分析
🔍 暴力解法,适用于 60%60\%60% 数据
我们观察题目的形式,会发现每次查询是对整个数组加上一个偏移量 iii,然后再计算它们的最大公因数。这种问题在数据范围较小时,可以直接暴力解决。
1. 暴力模拟每个询问
-
对于每次询问 iii,我们把数组中每个元素都加上 iii,得到一个新的数组。
-
然后,我们对新数组依次计算
gcd
。 -
由于
gcd
满足结合律,我们可以通过从前往后扫描数组,依次更新当前的gcd
值。
例如,若有数组:
a = [6, 9, 12]
i = 1
则变为 [7, 10, 13]
然后计算 gcd(7, 10, 13)
实现方式:
- 初始设 gcd=a[0]+igcd = a[0] + igcd=a[0]+i
- 然后遍历其余元素:gcd=gcd(gcd,a[j]+i)gcd = \gcd(gcd, a[j] + i)gcd=gcd(gcd,a[j]+i)
最终的 gcdgcdgcd 就是这一组询问的答案。
2. 算法复杂度分析
- 外层:询问数量为 qqq
- 内层:每次询问需要遍历 nnn 个元素
- 总体复杂度为 O(q⋅n)O(q \cdot n)O(q⋅n)
对于题目中说的 60%60\%60% 数据范围(n≤1000,q≤10n \le 1000, q \le 10n≤1000,q≤10),这个复杂度是可以接受的:1000×10=1041000 \times 10 = 10^41000×10=104 次 gcdgcdgcd 计算。
参考代码
#include <bits/stdc++.h>
using namespace std;int main()
{int n, q;cin >> n >> q; //n个数字 q次询问vector<int> a(n);for (int &i : a)cin >> i;for(int i = 1; i <= q; i++) //q次询问{int gcd = a[0] + i;for(int j = 1; j < n; j++)gcd = __gcd(gcd , a[j] + i);cout << gcd << endl;}return 0;
}
✅ 解题思路(优化做法,适用于 100%100\%100% 数据)
🔍 问题本质转化
我们要计算每次查询:gcd(a₁ + i, a₂ + i, ..., aₙ + i)
qqq 次对 nnn 个数求 GCD,暴力做法时间复杂度是 O(q⋅n)O(q \cdot n)O(q⋅n),显然会超时。但我们可以利用 GCD 的平移不变性 和 结合律 优化这个过程。
🧠 核心数学性质
一个关键性质是:
gcd(x + a₁, x + a₂, ..., x + aₙ) = gcd(x + a₁, a₂ - a₁, a₃ - a₁, ..., aₙ - a₁)
也就是说,我们可以把所有的 ai+xa_i + xai+x 表达成:gcd(a₁ + x, d₂, d₃, ..., dₙ)
其中 di=ai−a1d_i = a_i - a₁di=ai−a1,即所有数与第一个数的差。
💡 为什么这个公式成立?
这个公式的核心原理来源于 GCD 的不变性 和 GCD 对加减法的封闭性:
对于任意整数 u,vu, vu,v,都有:gcd(u, v) = gcd(u, v - u)
这是我们在辗转相除法(欧几里得算法)中使用的基本规则。
🧠 用在这个问题里,我们怎么理解?
我们要求的是:g = gcd(x + a₁, x + a₂, ..., x + aₙ)
我们可以把所有的数都减去 (x+a1)(x + a₁)(x+a1),即:
(x + a₂) - (x + a₁) = a₂ - a₁
(x + a₃) - (x + a₁) = a₃ - a₁
...
(x + aₙ) - (x + a₁) = aₙ - a₁
根据 gcdgcdgcd 的性质,这种减法不会改变最终的 gcdgcdgcd 结果。所以我们可以把上面的式子变形为:
gcd(x + a₁, a₂ - a₁, a₃ - a₁, ..., aₙ - a₁)
✅ 举个例子理解
假设:a = [6, 9, 12]
查询值:x = 1
我们要求:gcd(6 + 1, 9 + 1, 12 + 1) = gcd(7, 10, 13)
现在按照公式转换:
x + a₁ = 7
差值:
9 - 6 = 3
12 - 6 = 6
转化为:gcd(7, 3, 6)
继续计算:
gcd(7, 3) = 1
gcd(1, 6) = 1
结果是:111,与原始计算:gcd(7, 10, 13)
得到的结果一致。
⚙️ 实现方式
我们可以将这些差值的 GCD 预处理出来,代码如下:
int g = a[1] - a[0];
for (int i = 2; i < n; i++) {g = __gcd(g, abs(a[i] - a[0]));
}
然后每次查询只需要计算:
__gcd(a[0] + x, g);
这就将 O(q⋅n)O(q \cdot n)O(q⋅n) 优化成了 O(n+qlogA)O(n + q \log A)O(n+qlogA)。
🔧 代码分析
差值 gcd 预处理
int g = a[1] - a[0];
for(int i = 2; i < n; i++)
{g = __gcd(g , abs(a[i] - a[0]));
}
这段代码做的是:将所有差值的 gcdgcdgcd 预处理出来,时间复杂度 O(n)O(n)O(n)。
查询处理:
for(int i = 1; i <= q; i++) {int gcd = __gcd(g , a[0] + i);cout << gcd << endl;
}
每次查询的时间复杂度是 O(logA)O(\log A)O(logA)。
📌 时间复杂度分析
- 差值 gcdgcdgcd 预处理:O(n)O(n)O(n)
- 每次查询:O(logA)O(\log A)O(logA),总共 qqq 次,复杂度为 O(qlogA)O(q \log A)O(qlogA)
- 总体时间复杂度:O(n+qlogA)O(n + q \log A)O(n+qlogA)
这个复杂度对于题目给定的最大范围 n,q≤105n, q \le 10^5n,q≤105 是完全可以接受的。
参考代码
#include <bits/stdc++.h>
using namespace std;int main()
{int n, q;cin >> n >> q; // n个数字 q次询问vector<int> a(n);for (int &i : a)cin >> i;int g = 0;for (auto it : a)g = __gcd(g, abs(it - a[0]));for (int i = 1; i <= q; i++) // q次询问{int gcd = __gcd(g, a[0] + i);cout << gcd << endl;}return 0;
}