【算法】GCD on Blackboard

976 阅读3分钟

题目来自 AtCoder 上的 GCD on Blackboard

题目

黑板上写着 NN 个整数 A1,A2,,ANA_1, A_2, \dots, A_N

你将选择其中一个数并将其替换为另一个 1110910^9 的整数。这个数可以与替换之前的数相同。

替换后,黑板上所有整数的最大可能的 GCD 是多少?

注:GCD 为最大公约数,表示能整除所有 A1,A2,,ANA_1, A_2, \dots, A_N 的最大数。

输入

第一行包含一个整数 NN2N1052 \le N \le 10^5)。

第二行包含 NN 个整数,A1,A2,,ANA_1, A_2, \dots, A_N1Ai1091 \le A_i \le 10^9)。

输出

输出一个整数,表示替换一个数后所有数的最大可能的 GCD。

例子

例 1

3
7 6 8
2

解释:
77 替换为 44gcd(4,6,8)=2\gcd(4,6,8) = 2。这是我们能得到的最大 GCD 值。

例 2

3
12 15 18
6

解释:
1515 替换为 66gcd(12,6,18)=6\gcd(12,6,18) = 6。这是我们能得到的最大 GCD 值。

例 3

2
10 10
10

解释:
将一个 1010 替换为 1010(也就是说没有替换),gcd(10,10)=10\gcd(10,10) = 10。这是我们能得到的最大 GCD 值。

题目要求

输入:NNA1,A2,,ANA_1, A_2, \dots, A_N
输出:最大化 gcd(A1,A2,,AN)\gcd(A_1, A_2, \dots, A_N)
条件:可以替换一个 AiA_i

解题思路

关键 1

假设我们要将 AiA_i 替换成 xx,那么替换后的最大 GCD 只能是除了 AiA_i 以外的数的 GCD,即

maxgcd(A1,A2,,Ai1,x,Ai+1,,An)=gcd(A1,A2,,Ai1,Ai+1,,An)\begin{aligned} & \max \gcd(A_1, A_2, \dots, A_{i - 1}, x, A_{i + 1}, \dots, A_n) \\ & = \gcd(A_1, A_2, \dots, A_{i - 1}, A_{i + 1}, \dots, A_n) \end{aligned}

为什么?

首先,我们选择的 xx 一定要是其他数的公约数(否则最后结果不就变成 1 了嘛)。其次,xx 要尽可能的大,那不就是其他数的最大公约数(GCD)了。

换句话说,虽说题目是写替换一个数,但实则和删除一个数没有差别。这一点非常关键。

关键 2

现在我们有思路了,遍历数组,尝试删除一个数,看看剩余数的 GCD 最大能是多少。

这个数据量(N105N \le 10^5),两层 for 循环,铁定超时。

我们需要优化一下,使用前/后缀和预先计算 gcd(A1,A2,,Ai1)\gcd(A_1, A_2, \dots, A_{i - 1})gcd(Ai+1,Ai+2,,AN)\gcd(A_{i + 1}, A_{i + 2}, \dots, A_N)

定义:

pre[i]=gcd(A1,A2,,Ai)rpre[i]=gcd(Ai,Ai+1,,An)\mathrm{pre}[i] = \gcd(A_1, A_2, \dots, A_i) \\ \mathrm{rpre}[i] = \gcd(A_i, A_{i + 1}, \dots, A_n) \\

对于每个数 AiA_i,我们能直接求 gcd(pre[i1],rpre[i+1])\gcd(\mathrm{pre}[i - 1], \mathrm{rpre}[i + 1])

时间复杂度:O(n+log(maxAi))O(n+\log(\max A_i))
空间复杂度:O(n)O(n)

代码

C++:

#include <bits/stdc++.h>

using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    int a[n];
    for (int i = 0; i < n; i++)
        cin >> a[i];

    int pre[n], rpre[n];
    pre[0] = a[0];
    for (int i = 1; i < n; i++)
        pre[i] = __gcd(pre[i - 1], a[i]);
    rpre[n - 1] = a[n - 1];
    for (int i = n - 2; i >= 0; i--)
        rpre[i] = __gcd(rpre[i + 1], a[i]);

    int ans = -1;
    for (int i = 0; i < n; i++) {
        int gcd_oth; // 除了 a[i] 以外的所有数的 GCD
        if (i == 0) gcd_oth = rpre[i + 1];
        else if (i == n - 1) gcd_oth = pre[i - 1];
        else gcd_oth = __gcd(pre[i - 1], rpre[i + 1]);

        ans = max(ans, gcd_oth);
    }
    cout << ans;
}

Python:

from math import gcd


n = int(input())
a = list(map(int, input().split()))

pre = [a[0]]
for i in range(1, n):
    pre.append(gcd(pre[i - 1], a[i]))
rpre = [a[n - 1]]
for i in range(n - 2, -1, -1):
    rpre.insert(0, gcd(rpre[0], a[i]))

ans = -1
for i in range(0, n):
    # 除了 a[i] 以外的所有数的 GCD
    if i == 0:
        gcd_oth = rpre[i + 1]
    elif i == n - 1:
        gcd_oth = pre[i - 1]
    else:
        gcd_oth = gcd(pre[i - 1], rpre[i + 1])

    ans = max(ans, gcd_oth)

print(ans)

总结

提交结果

  1. 没辙,数论得学好;
  2. 前缀和不仅只能是“和”,XOR 和 GCD 操作都挺常见的,核心思想是这类型的计算都能一边遍历数组一边累计结果;
  3. 求 “除了第 ii 个数以外的数的某某运算” 或许可以使用前缀和 + 后缀和。

相关知识点:数论、前/后缀和