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

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

yukicoder No.2066 Simple Math !

floor sum のいい感じの練習問題!

問題概要

正整数  P, Q, K が与えられる。

非負整数  x, y を用いて  Px + Qy という形で表せる正の整数のうち、 K 番目に小さいものを求めよ。

( T 個の入力ケースが与えられる)

制約

  •  1 \le T \le 10^{4}
  •  1 \le P, Q, K \le 10^{9}

考えたこと

いかにも二分探索という問題。次の判定問題を考える。


 0 以上  M 以下の整数のうち、 Px + Qy という形で表せる整数の個数を求めよ

(この値が  K+1 個以上である最小の  M が答え)


 Px + Qy という形で表せる正\整数」をまともに数えるのは大変なので、できれば  (x, y) の組を数える方に帰着させたい。

そこで、 P, Q を最大公約数  G で割っておくことにする。その状態で答えを求めて、最終的に答えに  G をかければよい。よって、 P, Q は互いに素であると仮定してよい。

 (x, y) を一意にするために

 P, Q が互いに素であるならば、次のことが言える。


 0 \le x \lt Q とするとき、整数  t に対して、

 t = Px + Qy

を満たす整数  (x, y) の値の組はただ  1 つに定まる


よって、 0 \le x \lt Q の範囲で  x を動かしていき、 0 \le Px + Qy \le M となるような  y の個数を足していけばよい。

 \displaystyle 0 \le y \le \frac{M - Px}{Q}

と変形できる。 x \le \frac{M}{P} のとき、 \displaystyle \lfloor \frac{M - Px}{Q} \rfloor + 1 個であるとわかる。これを合算していく。floor sum が使える。

このままだと  x の係数が負になっているので、次のように設定する。

  •  N = \min(\lfloor \frac{M}{P} \rfloor + 1, Q)
  •  a = P
  •  b = M + Q - (N-1)a
  •  m = Q

こうして、floor sum を適用すればよい。計算量は対数の 2 乗オーダーとなる。

コード

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

// sum_{i=0}^{n-1} floor((a * i + b) / m)
// O(log(n + m + a + b))
// __int128 can be used for T
template<class T> T floor_sum(T n, T a, T b, T m) {
    if (n == 0) return 0;
    T res = 0;
    if (a >= m) {
        res += n * (n - 1) * (a / m) / 2;
        a %= m;
    }
    if (b >= m) {
        res += n * (b / m);
        b %= m;
    }
    if (a == 0) return res;
    T ymax = (a * n + b) / m, xmax = ymax * m - b;
    if (ymax == 0) return res;
    res += (n - (xmax + a - 1) / a) * ymax;
    res += floor_sum(ymax, m, (a - xmax % a) % a, a);
    return res;
}

// calc #n that can be expressed n =  Px + Qy (P, Q is coprime)
// 0 <= n <= M
long long calc_num(__int128 P, __int128 Q, __int128 M) {
    __int128 mp = M / P;
    __int128 N = min(mp + 1, Q);
    __int128 a = P, b = M + Q - a * (N - 1);
    return floor_sum(N, a, b, Q) - 1;
}

int main() {
    int CASE;
    cin >> CASE;
    while (CASE--) {
        long long P, Q, K;
        cin >> P >> Q >> K;

        long long G = gcd(P, Q);
        P /= G, Q /= G;

        long long low = -1, high = 1LL<<50;
        while (high - low > 1) {
            long long M = (low + high) / 2;
            if (calc_num(P, Q, M) >= K) high = M;
            else low = M;
        }
        cout << high * G << endl;
    }
}