hotaruの蛍雪日記

競プロとゲームをしています

CF#691Div.2とABC186でGCD問題が解けなかったので復習する

Codeforces Round #691 (Div. 2)のC問題で最大公約数(GCD)の問題が出た。 パナソニックプログラミングコンテスト(AtCoder Beginner Contest 186)のE問題で拡張ユークリッドの互除法が出た。

どちらも解けなかった(は?)。 CFは1592→1575(-17)、ACはパフォーマンス955で困る。

復習します。

C. Row GCD

問題のリンク 解説のリンク

解説によるとGCD(x, y) = GCD(x-y, y)らしいです。

証明してみるか。


x, ymの倍数のとき、 x-ymの倍数。→GCD(x, y) \leq GCD(x-y, y)

x-y, ymの倍数のとき、 xmの倍数。→GCD(x, y) \geq GCD(x-y, y)

ゆえにGCD(x, y) = GCD(x-y, y)


あとは解説の通りに実装。

コンテスト中は鳩の巣原理・周期性について考えていた。

コード

#include <algorithm>
#include <array>
#include <iostream>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <stack>
#include <vector>
using namespace std;
#define int long long

// 解法は公式tutorialを見て
// https://codeforces.com/blog/entry/85750

signed main() {
    cin.tie(0);
    ios::sync_with_stdio(0);

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

    sort(a.begin(), a.end());

    // gcd(a[i] - a[0])
    int gcd_ai_a0 = 0;
    for (auto &&i : a) {
        gcd_ai_a0 = gcd(i - a.front(), gcd_ai_a0);
    }

    vector<int> answers;
    answers.reserve(m);

    for (auto &&bi : b) {
        const int ans = gcd(a.front() + bi, gcd_ai_a0);
        answers.emplace_back(ans);
    }

    for (auto i = answers.begin(); i != answers.end(); i++) {
        cout << *i << (i != answers.end() - 1 ? ' ' : '\n');
    }
}

E - Throne

問題のリンク snukeの公式解説放送 公式解説

拡張ユークリッドの互除法が出てきます。 これらで拡張ユークリッドの互除法を勉強した。

拡張ユークリッドの互除法を使えば、Ax+By=GCD(A, B), x, y \in \mathbb{Z}を満たすx, yを高速に求められる。

これでユークリッドの互除法の理解は十分だな。 公式解説を見ながら問題を解いていきます。

x回の行動で玉座に座れることとS+xK \equiv 0 \bmod Nが成り立つことは同値です。
よって「Ax \equiv B \bmod Mとなる最小のxを求めよ」という形の問題が解ければよいです。この問題は次のように解くことができます。
(中略)
3. gcd(A,M)=1のとき、mod MにおけるAの逆元をA^{-1}として、x=A^{-1}Bが解
(略)
https://atcoder.jp/contests/abc186/editorial/401より引用。アクセス日は2020年12月23日。

  1. 拡張ユークリッドの互除法を使えば、Ax+By=GCD(A, B), x, y \in \mathbb{Z}を満たすx, yを高速に求められる。
  2. GCD(A, M) = 1ならば、Ax+My=GCD(A, M)を満たすxA^{-1}である。
  3. GCD(A, M) \neq 1ならば、Aの逆元は存在しない。

解説でdで割っているのは完全には理解していない。 4x=6 mod 10となるxを求めるときに2で割るのかー。

コード

#include <algorithm>
#include <array>
#include <iostream>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <stack>
#include <vector>
using namespace std;
#define int long long

// solve a testcase
void solve();

signed main() {
    cin.tie(0);
    ios::sync_with_stdio(0);

    int t;  // number of testcases
    cin >> t;
    // solve all testcases
    for (int i = 0; i < t; i++) {
        solve();
    }
}

// this function is copied from :https://www.youtube.com/watch?v=hY2FicqnAcc&feature=youtu.be
tuple<int, int, int> extgcd(int a, int b) {
    if (b == 0) return {a, 1, 0};
    int g, x, y;
    tie(g, x, y) = extgcd(b, a % b);
    return {g, y, x - a / b * y};
}
// end of copy

void solve() {
    int n, s, k;
    cin >> n >> s >> k;

    // s + xk = 0 mod n
    // を満たす最小のxを求める。

    // xk = -s mod n
    // Ax = B mod M

    int a = k;
    int b = -s;
    int m = n;

    // bを正にする
    while (b < 0) {
        b += n;
    }

    // 割っておく
    {
        const int g = gcd(a, gcd(b, m));
        a /= g, b /= g, m /= g;
    }

    // Aのmod Mの逆元を探す
    // gcd(A, M)!=0なら存在しない

    if (gcd(a, m) != 1) {
        cout << -1 << "\n";
        return;
    }

    // Aのmod Mの逆元は
    // Ax + My = gcd(A, M) = 1
    // を満たすx,yを拡張ユークリッドの互除法で計算して、そのx
    int x, y, g;
    tie(g, x, y) = extgcd(a, m);

    while (x < 0) {
        x += m;
    }

    int ans = x * b % m;
    cout << ans << "\n";
}

// thx
//
// 解説放送
// https://www.youtube.com/watch?v=hY2FicqnAcc&feature=youtu.be
// かいせつ
// https://atcoder.jp/contests/abc186/editorial/401
// drkenの記事
// https://qiita.com/drken/items/b97ff231e43bce50199a
// 蟻本

おまけ

証明ミスがあったら教えて下さい。