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

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

AtCoder ABC 165 D - Floor Function (茶色, 400 点)

気づけば一発系。ただし、数式の見た目がエグく見えてしまうかもしれない...

問題へのリンク

問題概要

正の整数  A, B, N が与えられる。 0 以上  N 以下の整数  x に対する

  •  \lfloor \frac{Ax}{B} \rfloor -  A \times \lfloor \frac{x}{B} \rfloor

の値の最大値を求めよ。

制約

  •  1 \le A \le 10^{6}
  •  1 \le B, N \le 10^{12}

考えたこと

全探索すれば  O(N) だけど、それでは間に合わない。さて、式の見た目がエグくてエグくてやばい。ちょっと思うことは

  • もし「小数点以下切り捨て」とかなくて実数演算だったら、 \frac{Ax}{B} -  \frac{Ax}{B} = 0 で常に 0 になるなぁ...

というくらいだろうか。よって、なんとなく、この値が大きくなるためには、後半の  \lfloor \frac{x}{B} \rfloor のところが、でかい小数点を抱え落ちすると良さそうという気持ちになる。たとえば  \lfloor \frac{19}{10} \rfloor = \lfloor 1.9 \rfloor = 1 という具合だ。

手を動かす

しかし、依然としてよくわからないので、こういうのはいくつかの具体例的に手を動かして、試してみると良さそう。

試しにサンプル 1 に従って、 A = 5 B = 7 について考えてみる。そして  x = 0, 1, 2, \dots に対して値がどのように変わるのかを眺めるのだ。

 x  \lfloor \frac{Ax}{B} \rfloor  A \times \lfloor \frac{x}{B} \rfloor
0 0 0 0
1 0 0 0
2 1 0 1
3 2 0 2
4 2 0 2
5 3 0 3
6 4 0 4
7 5 5 0
8 5 5 0
9 6 5 1
10 7 5 2
11 7 5 2
12 8 5 3
13 9 5 4

とりあえず気づくことは、

  •  A \times \lfloor \frac{x}{B} \rfloor は、周期  B (= 7) で、 A (= 5) だけ増える

ということだ。そしてよくみるとそれは  \lfloor \frac{Ax}{B} \rfloor の方も一緒だ。つまり、

 f(x) = \lfloor \frac{Ax}{B} \rfloor - A \times \lfloor \frac{x}{B} \rfloor

の値は、 B ごとに周期的になっているということだ!!!よって、 x の値としては

  •  x = 0, 1, 2, \dots, B-1

を試せば十分ということがわかる。それだけではない。


 x = 0, 1, \dots, B-1 の範囲で

  •  \lfloor \frac{Ax}{B} \rfloor は単調増加だが
  •  \lfloor \frac{x}{B} \rfloor の値は不変
  • したがって、 \lfloor \frac{Ax}{B} \rfloor - A \times \lfloor \frac{x}{B} \rfloor は単調増加

ということもいえる。よって、

  •  x = 0, 1, \dots, B-1
  •  x \le N

を満たす最大の  x が最適になる。つまり  x = \min(N, B-1) に対する値を求めれば良い。計算量は  O(1)

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

int main() {
    long long A, B, N;
    cin >> A >> B >> N;
    long long x = min(B-1, N);
    cout << (A*x)/B - A*(x/B) << endl;
}

Codeforces Round #638 (Div. 2) E. Phoenix and Berries (R2400)

本番なんとかブザービートが決まった!

問題へのリンク

問題概要

赤いベリーと青いベリーとがある。
 N 組のビュッフェがあり、それぞれ赤いベリーが  a_{i} 個、青いベリーが  b_{i} 個入っている。

これらをカゴに詰めていきたい。1 つのカゴにはちょうど  K 個のベリーを入れたい。ただし、1 つのカゴには

  • 同じビュッフェのベリー
  • 同じ色のベリー

のどちらかの構成でなければならない。最多で何個のカゴが作れるか?

制約

  •  1 \le N, K \le 500
  •  0 \le a_{i}, b_{i} \le 10^{9}

考えたこと

直感的には、

  • 赤いベリーだけでできるだけ  K 個ずつ詰める
  • 青いベリーだけでできるだけ  K 個ずつ詰める

という風にした場合、ほとんどのベリーを詰めることができて、このとき残るのは

  •  a の総和を  K で割ったあまり  p 個の赤いベリー
  •  b の総和を  K で割ったあまり  q 個の青いベリー

である。 p \lt K,  q \lt K なので、 p + q \lt 2K。よって、これ以上カゴを増やせるとしたら 1 個だけなのだ。

逆に、もし  s \le p,  K - s \le q を満たすような  s が存在して、ベリーの合計量が  K 以上であるようなカゴたちから

(赤いベリー  x 個、青いベリー  K-x 個)

という組を抽出していって、 x の総和を  K で割ったあまりが  s になるようにできるならば、カゴを 1 個増やした解を構成できる!

DP へ

このままだと  a_{i} \le 10^{9} とかなので大変だ。だがよく考えると、

  • dp[ i ][ j ] := 最初の i 個のビュッフェからいくつか選んで、その中から適合する (赤いベリー, 青いベリー) を選んだときの、それまでに選んだ赤いベリーの個数の総和を割ったあまりが j にできるかどうか

という風にすればよいので、状態空間の大きさは  O(NK) で、計算量も  O(NK^{2}) とかにできる。

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

long long N, K;
vector<long long> a, b;
long long solve() {
    long long sa = 0, sb = 0;
    for (int i = 0; i < N; ++i) sa += a[i], sb += b[i];
    long long res = sa/K + sb/K;

    vector<vector<bool> > dp(N+1, vector<bool>(K+1, false));
    dp[0][0] = true;
    for (int i = 0; i < N; ++i) {
        for (int j = 0; j < K; ++j) {
            if (!dp[i][j]) continue;
            dp[i+1][j] = true;
            for (int aj = 0; aj <= min(K-1, a[i]); ++aj) {
                if (K-aj <= b[i]) dp[i+1][(j+aj) % K] = true;
            }
        }
    }
    bool ok = false;
    for (long long x = 0; x <= K; ++x) {
        if (x <= sa%K && K-x <= sb%K && dp[N][x]) ok = true;
    }
    if (ok) ++res;
    return res;
}

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

    while (cin >> N >> K) {
        a.resize(N); b.resize(N);
        for (int i = 0; i < N; ++i) cin >> a[i] >> b[i];
        cout << solve() << endl;
    }
}

AtCoder ABC 164 D - Multiple of 2019 (水色, 400 点)

まさかの既出!!!しかも割と最近の ABC-E!!!

drken1215.hatenablog.com

問題へのリンク

問題概要

 1 から  9 までの数字のみからなる文字列  S が与えられる。

 S の連続する区間であって、 2019 の倍数であるものが何個あるのかを求めよ。

制約

  •  1 \le |S| \le 2 \times 10^{5}

考えたこと

実は完全にマジで既出だった!!!!!
ただし ABC 157-E では、「2019」のところが一般的な素数  P なので、ちょっとだけコーナーケースを含んで難しくなってる。今回の問題では、 P 2019 なので、少しだけ簡単になってる。

drken1215.hatenablog.com

さて、そもそもこの問題のように、「連続する区間」についての問題では、"累積和的なもの" をとるのが定石である。通常、累積和は左側から考えるケースが多いけど、今回は右側から考える。

たとえば 8 桁の整数 S = 41817134 について考えてみる。これの部分文字列を、次のような 9 個の整数の差分として考えることにする。

  • s[0] = 0
  • s[1] = 4
  • s[2] = 34
  • s[3] = 134
  • s[4] = 7134
  • s[5] = 17134
  • s[6] = 817314
  • s[7] = 1817134
  • s[8] = 41817134

たとえば、部分文字列の一つである 18171 は、次にように考えることができる。


  • 18171 は、右から 7 文字分 (s[7] = 1817134) から、右から 2 文字分 (s[2] = 34) を除去したものである
  • それを踏まえて s[7] - s[2] を計算すると、1817100 となる
  • これは 18171 を 100 倍した値である

このように、「該当する部分文字列に 10 を何回かかけると、累積和 s の差分で表せる」という状態になっていることがわかる。

区間が 2019 で割りきれることを、累積和の言葉で

さらにさらに、該当する部分文字列が 2019 の倍数ということは、

  • 18171 は 2019 の倍数なので
  • s[7] - s[2] = 1817100 も 2019 の倍数である
  • よって、s[7] と s[2] を 2019 で割ったあまりは等しい

という風に解釈できる!!!つまり、次のようになる!


  • S の連続する区間が、右から a 文字分から、右から b 文字分を取り除いたものであるとき
  • その区間の整数が 2019 の倍数になるということは、s[a] と s[b] を 2019 で割ったあまりが等しい

注意点として、「連続区間が 2019 の倍数なら、s[a] と s[b] を 2019 で割ったあまりが等しい」というのは間違いなく成り立つが、逆に「s[a] と s[b] を 2019 で割ったあまりが等しいならば、該当する連続区間が 2019 の倍数」というのが成り立つのか心配になる。

事実、既出となった ABC 157 E では、これが成り立たないケースがあるのだ。でも今回は成り立つ。その理由は 2019 と 10 が互いに素だからなのだけど、詳しくは該当問題の記事にて。

具体的な解法へ

以上の考察から、次のように解ける。


  • N 文字の文字列 S から、長さ N+1 の累積和 s[0], s[1], ..., s[N] を作る
  • これを 2019 で割ったあまりで分類する (具体的には counter[ r ] := あまりが r のやつが何個あるか)
  • 各 r ごとに、counter[r] 個から 2 個選ぶ方法の数を、足し上げる

計算量は、 O(N)

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

long long solve(const string &S) {
    int N = S.size();
    vector<long long> val(2019, 0);
    long long fac = 1;
    long long cur = 0;
    val[cur]++;
    for (int i = 0; i < N; ++i) {
        long long add = S[N-1-i] - '0';
        cur = (cur + fac * add) % 2019;
        fac = (fac * 10) % 2019;
        val[cur]++;
    }
    long long res = 0;
    for (int i = 0; i < val.size(); ++i) {
        res += val[i] * (val[i] - 1) / 2;
    }
    return res;
}

int main() {
    string S;
    cin >> S;
    cout << solve(S) << endl;
}

AtCoder ABC 164 E - Two Currencies (青色, 500 点)

これ、頂点を倍加してダイクストラする系。

問題へのリンク

問題概要

 N 頂点  M 辺の連結な無向グラフが与えられる。各辺  i には

  • 通行に要する料金  A_{i} 円 (所持金が  A_{i} 以上でなければ通行できない)
  • 通行に要する所要時間  B_{i}

という属性がある。また、各頂点  i では所持金を増やすことができる。

  • 1 回の操作で増やせる料金は  C_{i}
  • それに要する時間は  D_{i}

となっている。同じ頂点で何度でも所持金増加操作を実施することができる。
最初、頂点  1 にいて、所持金は  S 円である。各頂点  t = 2, 3, \dots, N に対して、その頂点に到達するための最小の所要時間を求めよ。

制約

  •  2 \le N \le 50
  •  1 \le M \le 100
  •  0 \le S \le 10^{9}
  •  1 \le A_{i} \le 50
  •  1 \le B_{i}, C_{i}, D_{i} \le 10^{9}

解法

拡張ダイクストラと言われているパターンの問題例としては、以下のものとかがある。

drken1215.hatenablog.com

所持金の大小を気にせずに普通のダイクストラ法でやろうとすると、あるところで辺 e = (u, v) の移動をするのに所持金が足りない...みたいなことになる。よって辺 e = (u, v) を通りたいなら、今までのどこかの頂点で所持金を補充しておく必要がある。でもあらかじめどの頂点で所持金を補充しておくべきかとか、よくわからない。

そこで登場するアイディアは


グラフの各頂点に、所持金の情報も持たせる。具体的には頂点 v について

  • (v, 0): 頂点 v に到達していて、所持金が 0 円であるような状態
  • (v, 1): 頂点 v に到達していて、所持金が 1 円であるような状態
  • (v, 2): 頂点 v に到達していて、所持金が 2 円であるような状態
  • ...

という風にして、頂点を倍加していく


というものだ。これをすることで、

  • 頂点 u (所持金 s 円) から、所持金 a 円 (所要時間 b 秒) を必要とする辺 e = (u, v) を通ることは、改めて頂点 (u, s) から頂点 (v, s - a) への、重み b の辺とみなせる

  • 頂点 u (所持金 s 円) で c 円分を補充をする (所要時間 d 秒) 作業は、頂点 (u, s) から頂点 (u, s + c) への、重み d の辺とみなせる

ということになる。そして

  • dp[ v ][ s ] := 頂点 v にいて、所持金 s という状態になるのに要する最小の所要時間

として、ダイクストラ法によって求めていくイメージ。なお、この問題、辺の張り方を雑にやってしまうと結構 TLE が危険になってしまう (下に簡単に注意書きした)。

このように拡張したグラフ上でダイクストラする。

さて、もう一つの気づくべきポイントとしては、今のままでは所持金上限は  S \le 10^{9} ということで計算量がヤバいことになる。しかし少し考えてみると、

  • 2500 円以上あっても関係ない
  • なぜなら、「一度訪れた頂点に二度以上行く意味はない」ことから、使う辺の本数は高々  N-1 本で、それぞれ最大  A_{i} \le 50 円しか消費しないことから、2500 円以上あっても余る

ということに気づく。よって

if (S >= 2500) S = 2500;

という風にして良いし、拡張したグラフの各頂点も、所持金 2500 円まで考えれば良い。

計算量は、

  • 頂点数:  O(N^{2}A)
  • 辺数:  O( (N + M) NA)

より、全体で  O( (N+M) NA (\log{N} + \log{A})) になる。

#include <bits/stdc++.h>
using namespace std;
template<class T> inline bool chmax(T& a, T b) { if (a < b) { a = b; return 1; } return 0; }
template<class T> inline bool chmin(T& a, T b) { if (a > b) { a = b; return 1; } return 0; }

using pll = pair<long long, long long>; // (money, time)
using Edge = pair<int, pll>;
using Graph = vector<vector<Edge>>;
const long long INF = 1LL<<60;
const int MAX = 2500;

int N, M;
long long S;
Graph G;
vector<long long> C, D; // money, time

void solve() {
    if (S >= MAX) S = MAX;
    vector<vector<long long>> dp(N, vector<long long>(MAX+1, INF));
    priority_queue<pair<long long, pll>, vector<pair<long long, pll> >, greater<pair<long long, pll> > > que;
    dp[0][S] = 0;
    que.push(make_pair(0, pll(0, S)));
    while (!que.empty()) {
        auto p = que.top(); que.pop();
        long long time = p.first;
        int v = p.second.first;
        long long s = p.second.second;
        if (time > dp[v][s]) continue;

        // 補充
        if (s + C[v] <= MAX) {
            long long ns = s + C[v];
            long long ntime = time + D[v];
            if (chmin(dp[v][ns], ntime)) {
                que.push(make_pair(ntime, pll(v, ns)));
            }
        }
        // 辺を通る
        for (auto e : G[v]) {
            if (s < e.second.first) continue;
            int nv = e.first;
            long long ns = s - e.second.first;
            long long ntime = time + e.second.second;
            if (chmin(dp[nv][ns], ntime)) {
                que.push(make_pair(ntime, pll(nv, ns)));
            }
        }
    }
    for (int v = 1; v < N; ++v) {
        long long res = INF;
        for (int s = 0; s <= MAX; ++s) chmin(res, dp[v][s]);
        cout << res << endl;
    }
}

int main() {
    cin >> N >> M >> S;
    G.assign(N, vector<Edge>());
    for (int i = 0; i < M; ++i) {
        long long u, v, a, b;
        cin >> u >> v >> a >> b;
        --u, --v;
        G[u].push_back(Edge(v, pll(a, b)));
        G[v].push_back(Edge(u, pll(a, b)));
    }
    C.resize(N); D.resize(N);
    for (int i = 0; i < N; ++i) cin >> C[i] >> D[i];
    solve();
}

計算量的な罠

今回の拡張ダイクストラ、実は割と罠もある。辺の貼り方を雑にやると TLE ギリギリになる。具体的には、

(u, s) から (v, s-a), (v, s-a+c), (v, s-a+2c), ... にそれぞれコストが b, b+d, b+2d, ... の辺を張る

という風にするのも自然に思えるのだ。だがこれだと、上に書いた辺の張り方に比べると、辺の本数がざっくり 2500 倍くらいになるので、TLE がかなり厳しくなる。

AtCoder ABC 144 C - Walk on Multiplication Table (灰色, 300 点)

約数列挙!!!

問題へのリンク

問題概要

正の整数  N が与えられる。
 a \times b = N を満たすような正の整数  (a, b) の組をすべて考えたときの、 a + b - 2 の最小値を求めよ。

制約

  •  2 \le N \le 10^{12}

考えたこと

これはまさに約数列挙の問題!!!
以下の記事の「3. 約数列挙」のところで、本質的に同じ問題の解説を行なっている。

qiita.com

具体的には次のようにすればよい。

  •  a = 1, 2, \dots と割っていく (ただし、 a \times a \le N の範囲まででよい)
  •  N a を割り切るとき、 b = \frac{N}{a} とする
  • そのような  a + b - 2 の最小値を求める

基本的には試し割りしていく全探索解法だが、 a \times a \le N の範囲でよい、というのがポイントになる。計算量は  O(\sqrt{N})

コード

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

int main() {
    long long N;
    cin >> N;
    long long res = N + 1 - 2; // (a, b) = (1, N)
    for (long long a = 1; a * a <= N; ++a) {
        if (N % a == 0) res = min(res, a + N/a - 2);
    }
    cout << res << endl;
}