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

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

AtCoder ABC 282 E - Choose Two and Eat One (青色, 500 点)

これをグラフの問題だと思えるかどうか!

問題概要

箱の中に  N 個のボールが入っており、各ボールには  1 以上  M−1 以下の整数が書かれている。 i 番目のボールに書かれた整数は  A_{i}​ である。

箱の中に 2 個以上のボールが残っている限り、下記の行動を繰り返す。

  • 箱の中から 2 つのボールを選んで取り出す
  • 取り出した 2 つのボールに書かれた整数をそれぞれ  x,y としたとき、 x^{y} + y^{x} \bmod M を得点として獲得する
  • その後、取り出した 2 つのボールのうち、片方を削除して、もう片方を箱に戻す

合計得点としてあり得る最大値を求めよ。

制約

  •  2 \le N \le 500
  •  2 \le M \le 10^{9}
  •  1 \le A_{i} \le M - 1

考えたこと

まず最初に考えたことは「 x^{y} + y^{x} \bmod M などという設定」はほとんど意味がないであろうということだった。本当は  N \times N の行列を与えて、ボール  x, y に対して操作する得点は  A_{x, y} であるというようにしてもいいくらいの感じだろうと思った。それだと入力が多くなるので、乱数っぽいものを用意したのだろうと。

というわけで、サンプル 1 について、 N \times N の表にしてみる。

ボール 1 ボール 2 ボール 3 ボール 4
ボール 1 x 2 5 2
ボール 2 2 x 7 8
ボール 3 5 7 x 7
ボール 4 2 8 7 x

元の問題は、この表から  3 個の整数を選んだ総和が最大にするということになる。ただし、本当に大きい順に 3 つ選ぶことはできない。

この表の場合の最適解は (1, 3)、(2, 4)、(3, 4) である (ボール  x, y を選ぶことを  (x, y) と表記している)。

たとえば、(2, 3)、(2, 4)、(3, 4) とすれば、7 + 8 + 7 = 22 と大きくなるのに、それができないのはなぜだろうか?? まず気づくのはボール 1 が登場していないこと。そして、下図のように、ボール 2, 3, 4 がループしてしまっていることだ。

ループしているとダメみたい。たとえば、ボール 2, 3 を選んでボール 3 を消したとする。次にボール 2, 4 を選んでボール 3 を消したとする。そしたら、ボール 4 だけが残るので、最後にボール 3, 4 を選ぶことはできない。

ボールを選ぶ順序や、消すボールをどっちにするかを、どのように選んでもダメだ。

グラフで考える

ここから先はグラフでちゃんと考えることにする。頂点数  N の完全グラフだ。このグラフから  N-1 本の辺を選ぶ問題と言える (選んだ辺は「操作のために選ぶ 2 つのボールの組」に相当する)。

先ほどの観察から「選ぶ辺の集合によってループが形成されてはいけない」ということが言えそうだ。

実際これは容易に証明できる。もし  t 辺からなるループができたとする。このとき、どの順序で辺を選んだとしても、 t-1 回目の作業を終えた時点で、 t 個あったボールは残り 1 個になってしまう。最後の  t 回目の作業ができないのだ。

以上から、選ぶ辺集合がループを形成しないことが必要条件だと言えた。つまり、全域木になることが必要だということだ。

全域木ならすべて作れる

逆に、任意の全域木は、上手に「2 つのボールを選ぶ順序」と「どちらのボールを消すか」を設定することで実現できる。

具体的には、以下のことを繰り返せば良い。このように「葉から順に処理していく」という考え方はよく見かける。


  • まだ残っているボール (に対応する頂点) のうち、葉を 1 つ選ぶ
  • その葉に接続している辺の両端に対応する 2 つのボールを取り出す
  • 葉に対応する側のボールを食べる

これを繰り返すことで、「取り出した 2 つのボールに対応する辺の集合」が所望の全域木になるようにすることができる。

最大全域木問題

以上より、この問題は「最大全域木問題」に帰着された。最小全域木問題は有名。それと同じ方法で最大全域木問題も解ける。つまり、Kruskal 法を辺が重い順に適用していけばよい。

Kruskal 法はたとえばけんちょん本では 15 章に書いた。

計算量は  O(N^{2} \log {N}) となる。なお余談だが、Prim 法を priority_queue を用いずに書く方法で  O(N^{2}) にすることもできる。

コード

#include <bits/stdc++.h>
using namespace std;
using pint = pair<int, int>;

// a^n mod m
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;
}

// Union-Find
struct UnionFind {
    vector<int> par;

    UnionFind() { }
    UnionFind(int n) : par(n, -1) { }
    void init(int n) { par.assign(n, -1); }
    
    int root(int x) {
        if (par[x] < 0) return x;
        else return par[x] = root(par[x]);
    }
    
    bool issame(int x, int y) {
        return root(x) == root(y);
    }
    
    bool merge(int x, int y) {
        x = root(x); y = root(y);
        if (x == y) return false;
        if (par[x] > par[y]) swap(x, y); // merge technique
        par[x] += par[y];
        par[y] = x;
        return true;
    }
    
    int size(int x) {
        return -par[root(x)];
    }
};

using pint = pair<int,int>;  // 頂点対
using Edge = pair<long long, pint>;  // 重み付き辺

int main() {
    // 入力
    long long N, M;
    cin >> N >> M;
    vector<long long> A(N);
    for (int i = 0; i < N; ++i) cin >> A[i];

    // すべての辺を求めて、重い順にソート
    vector<Edge> edges;
    for (int i = 0; i < N; ++i) {
        for (int j = i+1; j < N; ++j) {
            long long cost = (modpow(A[i], A[j], M) + modpow(A[j], A[i], M)) % M;
            edges.push_back(Edge(cost, pint(i, j)));
        }
    }
    sort(edges.begin(), edges.end(), greater<Edge>());

    // Kruskal 法の適用
    long long res = 0;
    UnionFind uf(N);
    for (auto e : edges) {
        long long add = e.first;
        int u = e.second.first, v = e.second.second;
        if (uf.issame(u, v)) continue;
        res += add;
        uf.merge(u, v);
    }
    cout << res << endl;
}

AtCoder ARC 111 A - Simple Math 2 (緑色, 300 点)

上手に整理すれば、とてもシンプルな問題!

問題概要

正の整数  N, M が与えられる。

 10^{N} M で割った商  \displaystyle \lfloor \frac{10^{N}}{M} \rfloor M で割ったあまりを求めよ。

制約

  •  1 \le N \le 10^{18}
  •  1 \le M \le 10000

考えたこと

 M で割るような操作を 2 回しているので、 10^{N} M^{2} で割った余りを考えるとよいのだろうなというあたりをつける。

 10^{N} M^{2} で割ったときの商を  Q、あまりを  R として、

 10^{N} = M^{2}Q + R

とおく。さらに、 R M で割ったときの商を  Q'、あまりを  R' とすると

 10^{N} = M^{2}Q + MQ' + R

と表せることになる。さて、このとき、 10^{N} = M(MQ + Q') + R と表せることから、次のことが読み取れる。


  •  10^{N} M で割ったときの商は  MQ + Q' に一致する
  • さらに、 MQ + Q' M で割ったあまりは  Q' に一致する
  • したがって、求める値は  Q' である

よって、この問題は「 10^{N} M^{2} で割ったあまりを  M で割った商」を求めることで解ける。

計算量は  O(\log N) となる。

コード

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

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;
}

int main() {
    long long N, M;
    cin >> N >> M;
    cout << modpow(10LL, N, M * M) / M << endl;
}

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;
    }
}

yukicoder No.2032 Let's Write Multiples!!

floor sum の練習!

問題概要

 L 以上  R 以下の  K の倍数をすべて十進表記で 1 回ずつ書き出した時、数字  C が書かれる回数を求めよ。

(テストケースが  T 個与えられる)

制約

  •  1 \le T \le 5 \times 10^{4}
  •  1 \le L \le R \le 10^{9}
  •  1 \le K \lt 10^{9}
  •  1 le C \le 9

考えたこと

各桁ごとに考える。整数  x の右から  k 桁めが  C である条件は

 x 10^{k+1} で割ったあまりが  C \times 10^{k} 以上  (C+1) \times 10^{k} 未満である

と言い換えられる。さらに、次のように言い換えられる。

  •  x k 桁めが  C であるとき、 \displaystyle \bigl\lfloor \frac{x + (10 - C) \times 10^{k}}{2^{k+1}} \bigr\rfloor - \bigl\lfloor \frac{x + (9 - C) \times 10^{k}}{2^{k+1}} \bigr\rfloor = 1
  • そうでないとき  \displaystyle \bigl\lfloor \frac{x + (10 - C) \times 10^{k}}{2^{k+1}} \bigr\rfloor - \bigl\lfloor \frac{x + (9 - C) \times 10^{k}}{2^{k+1}} \bigr\rfloor = 0

よって、 0 以上  N 以下の  K の倍数の各桁について、 \displaystyle \bigl\lfloor \frac{x + (10 - C) \times 10^{k}}{2^{k+1}} \bigr\rfloor - \bigl\lfloor \frac{x + (9 - C) \times 10^{k}}{2^{k+1}} \bigr\rfloor の値の総和を求めればよい。

計算量は  O((\log R)^{2}) となる。

コード

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

// sum_{i=0}^{n-1} floor((a * i + b) / m)
// O(log(n + m + a + b))
long long floor_sum(long long n, long long a, long long b, long long m) {
    if (n == 0) return 0;
    long long 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;
    long long 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;
}

int main() {
    int CASE;
    cin >> CASE;
    while (CASE--) {
        long long L, R, K, C;
        cin >> L >> R >> K >> C;

        // 0 以上 N 以下の整数についての答えを求める
        auto solve = [&](long long N) -> long long {
            long long res = 0;
            long long beki = 1;
            for (int k = 0; k <= 10; ++k) {
                long long upper = floor_sum(N/K+1, K, beki*(10-C), beki*10);
                long long lower = floor_sum(N/K+1, K, beki*(9-C), beki*10);
                res += upper - lower;
                beki *= 10;
            }
            return res;
        };
        cout << solve(R) - solve(L-1) << endl;
    }
}

AtCoder ABC 283 Ex - Popcount Sum (橙色, 600 点)

floor sum と聞いて!!

問題概要

 1 以上  N 以下の整数のうち、 M で割って  R 余るものを考える。そのような整数の popcount 値の総和を求めよ。

( T ケース与えられる)

制約

  •  1 \le T \le 10^{5}
  •  1 \le M \le N \le 10^{9}
  •  0 \le R \lt M

考えたこと

TL を見て、floor sum と知った上での考察。

「popcount の総和を求めよ」という設定自体から、とりあえず各桁ごとに求めればよさそうという推測は立つ。

 k 桁目の値が  1 になるものの個数を求めるために、その条件を言い換えていくことにする。まずよくあるのは、

 x k 桁目が  1 である
 \Leftrightarrow  x 2^{k+1} で割ったあまりが  2^{k} 以上  2^{k+1} 未満である

という言い換え。この言い換えは、たとえば次の問題で本質的に活躍する。この言い換えは苦手で、少し前に頑張って意識したので、今回はスムーズに出た。

drken1215.hatenablog.com

これは、次のように言い換えられる。

 \displaystyle \bigl\lfloor \frac{x + 2^{k}}{2^{k+1}} \bigr\rfloor - \bigl\lfloor \frac{x}{2^{k+1}} \bigr\rfloor = 1

つまり、元の問題は次のように言い換えられる。


 0 以上  N 以下の  M で割って  R あまる整数  x について、

 \displaystyle \bigl\lfloor \frac{x + 2^{k}}{2^{k+1}} \bigr\rfloor - \bigl\lfloor \frac{x}{2^{k+1}} \bigr\rfloor

の総和を求めよ。


さらに

 x = Mt + R とおくと、 t の条件は

 \displaystyle 0 \le t \le \lfloor \frac{N-R}{M} \rfloor

となる。最終的な答えは、 n = \lfloor \frac{N-R}{M} \rfloor + 1 とおいて、

 \displaystyle \sum_{t = 0}^{n-1} \bigl\lfloor \frac{Mt + R + 2^{k}}{2^{k+1}} \bigr\rfloor - \sum_{t = 0}^{n-1} \bigl\lfloor \frac{Mt + R}{2^{k+1}} \bigr\rfloor

と表せる。この値は floor sum で求められる。計算量は  O((\log N)^{2}) となる。

コード

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

// sum_{i=0}^{n-1} floor((a * i + b) / m)
// O(log(n + m + a + b))
long long floor_sum(long long n, long long a, long long b, long long m) {
    if (n == 0) return 0;
    long long 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;
    long long 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;
}

int main() {
    int CASE;
    cin >> CASE;
    while (CASE--) {
        long long N, M, R;
        cin >> N >> M >> R;
        long long range = (N - R) / M + 1;

        long long res = 0;
        for (int k = 0; k <= 30; ++k) {
            long long upper = floor_sum(range, M, R + (1LL<<k), 1LL<<(k+1));
            long long lower = floor_sum(range, M, R, 1LL<<(k+1));
            res += upper - lower;
        }
        cout << res << endl;
    }
}