AtCoder PR

AtCoder ABC125C × TypeScript|1回の書き換えで最大公約数を最大化する

記事内に商品プロモーションを含む場合があります

この記事のゴール

  • 結論:答えは「各位置 i を除いた配列の GCD」の最大値。
  • 理由:1 つだけ好きな値に書き換えられるため、全体の GCD は「その 1 要素を除いた残りの GCD」まで引き上げられる。
  • 方法:左右の累積 GCD(prefix/suffix)で、各 i について gcd(prefix[i-1], suffix[i+1]) を候補に最大を取る。
  • 実装:TypeScript(Node.js)で O(N logV)、メモリは O(N) もしくは O(1) 配列。

問題概要(ABC125C)

与えられた N 個の整数列 A に対して、ちょうど 1 つの要素を 1〜10^9 の好きな整数に書き換える。書き換え後の配列全体の最大公約数(GCD)を最大にせよ。

  • 制約:2 ≤ N ≤ 10^5、1 ≤ A_i ≤ 10^9
  • 入力:
    1 行目 N、2 行目 A_1 … A_N
  • 出力:書き換え後の GCD の最大値
  • 例:N=3, A=[7,6,8] → 7 を 4 にすると全体 GCD は 2(これが最大)
  • 問題URL(AtCoder公式)https://atcoder.jp/contests/abc125/tasks/abc125_c

解法の芯

結論

各位置 i を除いた残り全体の GCD を計算し、その最大が答え。

理由

  • i 番目を任意の値 x に書き換えると、全体 GCD は gcd(x, G)(G = 「i を除いた GCD」)
  • x = G と選べば全体 GCD は少なくとも G になる。
  • よって、すべての i で「i を除いた GCD」を出して最大を取るだけでよい。

具体例

[7, 6, 8]

  • i=0 を除く [6,8] の GCD は 2
  • i=1 を除く [7,8] の GCD は 1
  • i=2 を除く [7,6] の GCD は 1
    最大が 2 → 実際に 7→4 と書き換えると全体 GCD は 2。

再確認

左右の累積 GCD を作れば、各 i を除いた GCD を O(1) で取り出せる。全体は O(N) 回の GCD で終わる。

アルゴリズムの全体像

  • prefix[i] = gcd(A[0], …, A[i])
  • suffix[i] = gcd(A[i], …, A[N-1])
  • i を除いた GCD = gcd(prefix[i-1], suffix[i+1])(端は片側のみ)
  • その最大が答え

計算量:GCD は O(logV)。前後 2 パス+評価で O(N logV)、メモリ O(N)(工夫で実質 1 配列にできる)

実装例(TypeScript / Node.js)

提出コード(1配列+1パス)

左の累積 L[i] と右側累積を 1 変数 right で持ち、後ろから 1 回なめるだけで更新します。

import * as fs from "fs";

// エントリポイント inputに入力データ全体が入る
function main(input: string): void {
  // const splittedInput = input.trim().split(/\r?\n/);
  const [nRaw, aRaw] = input.trim().split(/\r?\n/);
  const N = Number(nRaw);
  const A = aRaw.split(" ").map(Number);

  const gcd = (a: number, b: number): number => b ? gcd(b, a % b) : Math.abs(a);

  // L を作る
  const L = Array(N).fill(0);
  L[0] = A[0];
  for (let i = 1; i < N; i++) L[i] = gcd(L[i-1], A[i]);

  let ans = 0, right = 0;               // right は R[i+1] を順に表す
  for (let i = N - 1; i >= 0; i--) {
    const left = (i > 0 ? L[i-1] : 0);  // gcd(x,0)=x
    ans = Math.max(ans, gcd(left, right));
    right = gcd(right, A[i]);           // 次の i-1 に向け更新
  }
  console.log(ans);
}

//*この行以降は編集しないでください(標準入出力から一度に読み込み、Mainを呼び出します)
const inputData: string = fs.readFileSync("/dev/stdin", "utf8")
main(inputData);

参考実装(prefix/suffix の 2 配列で分かりやすく)

読みやすさ重視。境界処理が明瞭です。

import * as fs from 'fs';

const tokens = fs.readFileSync(0, 'utf8').trim().split(/\s+/).map(Number);
const N = tokens[0];
const A = tokens.slice(1);

const gcd = (a: number, b: number): number => {
  a = Math.abs(a); b = Math.abs(b);
  while (b !== 0) { const t = a % b; a = b; b = t; }
  return a;
};

const prefix = new Array<number>(N);
const suffix = new Array<number>(N);

prefix[0] = A[0];
for (let i = 1; i < N; i++) prefix[i] = gcd(prefix[i-1], A[i]);

suffix[N-1] = A[N-1];
for (let i = N - 2; i >= 0; i--) suffix[i] = gcd(suffix[i+1], A[i]);

let ans = 0;
for (let i = 0; i < N; i++) {
  const left = (i === 0) ? 0 : prefix[i-1];
  const right = (i === N-1) ? 0 : suffix[i+1];
  const withoutI = (left === 0) ? right : (right === 0) ? left : gcd(left, right);
  if (withoutI > ans) ans = withoutI;
}

console.log(ans.toString());

動作確認(例)

入力

3
7 6 8

出力

2

よくある間違いと落とし穴

  1. 端の処理を忘れるi=0i=N-1 で配列外参照をしない。
  2. 毎回 GCD を作り直して O(N^2):prefix/suffix を前計算する。
  3. 素因数分解や LCM に寄り道:本問は GCD の性質のみで十分。
  4. 数値精度が不安:A_i ≤ 10^9 なので JS の Number(53bit)で安全。
  5. 「ちょうど 1 回」の読み違い:同じ値に書き換え可=実質「高々 1 回」。解法は同じ。

発展的な実装ヒント

  • メモリ最小化prefix を変数、suffix のみ配列にすれば 1 配列。
  • テスト観点
    • 最小:N=2(例:5 1010
    • 全要素同一:[x, x, …]x
    • 互いに素っぽい:[6,10,15] → 最大は 5
    • ランダム/大 N で性能確認

まとめ

  • 上限は 「除いた残りの GCD」
  • prefix/suffix で候補を O(1) 取得 → 全体 O(N logV)
  • TypeScript で素直に実装でき、境界と I/O だけ注意すれば十分。

付録:入出力テンプレ(Node.js)

import * as fs from 'fs';
const it = fs.readFileSync(0, 'utf8').trim().split(/\s+/)[Symbol.iterator]();
const nextNum = () => Number((it.next().value ?? 0));
const N = nextNum();
const A = Array.from({ length: N }, nextNum);
// ここにロジックを実装