けんちょんの競プロ精進記録

競プロの精進記録や小ネタを書いていきます

AtCoder ABC 186 E - Throne (水色, 500 点)

一次合同方程式を解く問題です!

問題概要

円周上に  N 個の椅子が並べられています。そのうち 1 つは玉座です。

高橋君は最初、玉座から時計回りに数えて  S 個隣の椅子に座っており、次の行動を繰り返します。

  • 行動:いま座っている椅子から時計回りに数えて  K 個隣の椅子に移動し座る

高橋君がはじめて玉座に座ることができるのは何回目の行動の後ですか? ただし、玉座に座ることが永遠にできない場合は、代わりに -1 を出力してください。

制約

  • (テストケース数)  \le 100
  •  2 \le N \le 10^{9}
  •  1 \le S \lt N
  •  1 \le K \le 10^{9}

解法 (1):合同方程式を立てて解く、逆元計算する

まずはオーソドックスな方法でやってみる。 x 回移動したときに王座に戻るとすると、それは  S + Kx N で割ったあまりが 0 となることを意味する。つまり

 S + Kx \equiv 0 \pmod N
 Kx \equiv -S \pmod N

となる。よって、この一次合同方程式を解く問題へと帰着された!! 一般に  ax \equiv b \pmod m を解く方法を考えよう。

 a m が互いに素のとき

この場合は簡潔になる。 a m が互いに素なら、 ac \equiv 1 \pmod m となるような  c が存在する。このような  c を、 m を法とした  a の逆元と呼ぶ。逆元は  a^{-1} と表すことが多い

そして、 ax \equiv b \pmod m の両辺に  a^{-1} をかけると、

 x \equiv a^{-1}b \pmod m

となる。逆元  a^{-1} の求め方は次の記事などに書いた。拡張 Euclid の互除法を使うことで求められる。

qiita.com

もしくは ACL にも逆元を求める関数があるので、それが使える!

 a m が互いに素でないとき

一般に、整数が互いに素でない場合については、最大公約数を考えてあげることで、互いに素な場合に帰着できることが多い。今回も  a m の最大公約数を  g とおいてみることにする。そして、 a, m g で割った商を  a', m' とすると、

  •  a = ga'
  •  m = gm'

となって、 a', m' は互いに素になる。結論から書けば、次のようになる。


  •  b g の倍数でないとき、解なし
  •  b g の倍数であるとき、 b = gb' として、 a'x \equiv b' \pmod {m'} を解く問題へと帰着される

これを検証してみよう。 ax \equiv b \pmod m であるとき、 ax - b m で割り切れなければならない。よって、

 ax - b = my

を満たす整数  y が存在することになる。よって

 b = ax - my = g(a'x - m'y)

となることから、 b g の倍数でなければならないことが言える。 b g の倍数でないときは  ax \equiv b \pmod m を満たす整数  x は存在しないと結論づけられる。

逆に  b g の倍数であるならば、 b = gb' とおくことで、

 ax - b = my
 ga'x - gb' = gm'y
 a'x - b' = m'y

となる。これは  a'x \equiv b' \pmod {m'} であることを意味する。

まとめ

 g = {\rm GCD}(K, N) として、

  •  S g の倍数でない場合は -1
  •  S g の倍数である場合は、まず  N, S, K をそれぞれ  g で割る。これによって  N K は互いに素になるので、 K の逆元  K^{-1} を求めると、 x \equiv -K^{-1}S \pmod N と求められる

計算量は  O(\log N)。以下のコードでは ACL の modint を用いた。modint を用いれば、逆元  K^{-1} を掛ける行為は、 K で割る行為と翻訳できる。

#include <bits/stdc++.h>
#include <atcoder/all>
using namespace std;
using namespace atcoder;
     
long long solve() {
    long long N, S, K;
    cin >> N >> S >> K;
    long long g = gcd(N, K);
    if (S % g) return -1;
    N /= g, S /= g, K /= g;
    modint::set_mod(N);
    modint res = -modint(S) / K;
    return res.val();
}

int main(){
    int T;
    cin >> T;
    while (T--) cout << solve() << endl;
}

解法 (2):中国剰余定理を使う

この方法なら、 K N が互いに素かどうかなども気にせず解くことができる!!

 S + Kx N の倍数であることを活用するのは一緒。ここで  y = Kx とおくと、次のように言える。

  •  y \equiv 0 \pmod K
  •  y \equiv -S \pmod N

これらをともに満たすような整数  y は中国剰余定理によって求めることができる。中国剰余定理も ACL にあるのでそれを使えば楽チン。

具体的には、解を  y \equiv r \pmod m と表したとき、 (r, m) を返してくれる。しかも、もし解なしならば  m = 0 とした解を返してくれる。解があるかどうかの判断さえも丸投げできるのだ!!!

#include <bits/stdc++.h>
#include <atcoder/all>
using namespace std;
using namespace atcoder;
     
long long solve() {
    long long N, S, K;
    cin >> N >> S >> K;
    auto p = crt({0, N - S}, {K, N});
    if (p.second == 0) return -1;
    return p.first / K;
}

int main(){
    int T;
    cin >> T;
    while (T--) cout << solve() << endl;
}

解法 (3):Euler の定理を活用する

実質的には解法 (1) と同じだけど、逆元計算を Euler の定理で頑張る解法。

一般に  p が素数であれば、 p を法とした  a の逆元は  a^{p-2} を計算することで求められる。なぜならば Fermat の定理より

 a^{p-1} \equiv 1 \pmod p
 a \times a^{p-2} \equiv p

が成立するからだ。今回は mod が素数とは限らないので、この方法は使えない。代わりに Fermat の定理を拡張した Euler の定理が使える。Euler の定理とは、 a N が互いに素であるとき

 a^{\phi(N)} \equiv 1 \pmod N

が成り立つというもの。ここで  \phi(N) は Euler 関数と呼ばれる。 \phi(N) 1 以上  N 以下の整数のうち  N と互いに素であるものの個数を表す。 N を素因数分解して

 N = p_{1}^{e_{1}} \times p_{k}^{e_{k}}

としたときに

 \phi(N) = m(1 - \frac{1}{p_{1}}) \dots (1 - \frac{1}{p_{k}})

となることが知られている。Euler 関数の計算は  O(\sqrt{N}) でできる。さて、Euler の定理より

 a \times a^{\phi(N)-1} \equiv 1 \pmod N

となる。よって  a の逆元は

 a^{-1} \equiv a^{\phi(N)-1}

と求められる。

コード

#include <bits/stdc++.h>
#include <atcoder/all>
using namespace std;
using namespace atcoder;

long long Euler(long long N) {
    long long res = N;
    for (long long p = 2; p * p <= N; ++p) {
        if (N % p == 0) {
            res = res / p * (p - 1);
            while (N % p == 0) N /= p;
        }
    }
    if (N != 1) res = res / N * (N - 1);
    return res;
}

long long modpow(long long a, long long n, long long mod) {
    long long res = 1;
    while (n > 0) {
        if (n & 1) res = res * a % mod;
        a = a * a % mod;
        n >>= 1;
    }
    return res;
}
     
long long solve() {
    long long N, S, K;
    cin >> N >> S >> K;
    S = N - S;
    long long G = gcd(N, K);
    if (S % G) return -1;

    N /= G, S /= G, K /= G;
    long long phi = Euler(N);
    long long invK = modpow(K, phi-1, N);
    return S * invK % N;
}

int main(){
    int T;
    cin >> T;
    while (T--) cout << solve() << endl;
}

解法 (4):Baby-Step Giant-Step 法を活用する