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

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

JOI 二次予選 2021 D - 安全点検 (AOJ 0694, 難易度 7)

難易度 8 でもおかしくないと思った。
B や C より易しい気がしなくもないけど、本番の緊張感で B や C を飛ばして D を本気で考える決断はなかなかできなさそう。今回は B - パンケーキを見て冷静になれたか勝負だね...

問題概要

 N 個の工場がある。各工場は座標  A_{i} ( 0 \lt A_{1} \lt \dots \lt A_{N}) にあって、作業量が  B_{i} となっている。

 K 人の作業員がいる。時刻 0 にはすべての人が座標 0 で待機している。各人は

  • 1 秒で 1 の作業ができる (どの工場においても)
  • 1 秒で 1 だけ移動できる

すべての工場のすべての作業が完了するのに要する時間の最小値を求めよ。

制約

  •  1 \le N \le 10^{5}
  •  1 \le K, A_{i}, B_{i} \le 10^{9}

考えたこと:二分探索っぽい

それぞれの作業員の行動過程はかなり複雑なものになりうる。しかし冷静に考えると、すべての作業時間の総和は

  • 移動時間の総和 (これは一定ではない)
  • 作業時間の総和 (これは一定)

の総和となる。つまり、「移動時間の総和」をなるべく小さくしたいのだ。しかし、最奥の工場に送り込む人数が少なすぎると、その人の作業完了までの時間がボトルネックになってしまう。このように

  • 全員の作業時間を均一化した上で
  • すべての作業を完了したい

というシチュエーションは、二分探索が有効だと相場は決まっている。「最大値の最小化」において二分探索が有効なのと原理は一緒。

全員の作業時間を  x 秒以内にできるか

という判定問題を解くことになるのだが、この  x を固定することで初めて、"バランス" (今回は奥の工場に送り込む人数が多いと移動時間が無駄で、少ないと奥工場での作業がネックになる...というトレードオフがある) をとるための最適戦略を決められるのだ。

奥の工場から片付けていく

ここまでの考察で、もうほとんど解けている。奥の工場から順番に考えていこう。

全員の作業時間を  x 秒以内にしたいという制約条件の下では、最奥の工場へと送り込むべき人数が決まる。その人たちは、途中の工場へと寄り道せず、一心不乱にまず最奥の工場へと向かう (最後の一人だけ例外)。その時点で、最奥の工場で費やせる時間が決まる ( x - (移動時間))。よって、最奥へと送り込む人数の最小値が求められる。最後の一人だけは時間が余るので、その分は最奥から 2 番目の工場に割り当てれば OK。

こうして、最奥の工場から始めて、奥から 2 番目、3 番目、4 番目の工場へと送り込む人数が順に Greedy に決まっていく。 K 人で全工場の作業を終えられれば "Yes"、終えられなければ "No" だ。

計算量は、判定問題は、 O(N) で解けるので、座標と作業量の総和を  V として  O(N \log V) の計算量で解ける。

コード

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

int main() {
    int N, K;
    cin >> N >> K;
    vector<long long> A(N), B(N);
    for (int i = 0; i < N; ++i) cin >> A[N-i-1];
    for (int i = 0; i < N; ++i) cin >> B[N-i-1];

    auto judge = [&](long long x) -> bool {
        long long sum = 0, amari = 0;
        for (int i = 0; i < N; ++i) {
            if (amari >= B[i]) {
                amari -= B[i];
                continue;
            }
            long long work = B[i] - amari;
            long long each = x - A[i];
            if (each <= 0) return false;
            long long num = (work + each - 1) / each;
            sum += num;
            amari = each * num - work;
        }
        return sum <= K;
    };

    long long left = 0, right = 1LL<<60;
    while (right - left > 1) {
        long long x = (left + right) / 2;
        if (judge(x)) right = x;
        else left = x;
    }
    cout << right << endl;
}

AtCoder AGC 050 A - AtCoder Jumper (500 点)

これ本当にずっとわからなかった...言われてみればという感じ!!

問題概要

以下の条件を満たす、頂点数  N の有向グラフ (頂点番号を  1, 2, \dots, N とする) を構築せよ (自己ループも多重辺も可)。

  • すべての頂点の出次数は 2 である
  • 任意の頂点対  u, v に対して、 u から  v への最短路長が 10 以下である

制約

  •  1 \le N \le 1000

考えたこと

本当に何もわからなかった。でも、まったく進展がなかったわけではなく

  • 1-indexed で考えるよりも 0-indexed で考えた方がよさそう
  • なんらかの規則によって得られた結果を mod.  N するのがよさそう

といったことは考えていた。ここまではよかった。僕はここからスモールワールドについて調べて

「どの頂点も次数が低いグラフでも、最初は各頂点の近隣と結んでおいて、確率  p でランダムにつなぎ変えると、直径が  O(\log) のオーダーになる」

というのを見た。実際にやってみると、確かに直径が 20 くらいにはなるのだ (ここでは  u から  v への最短路長の最大値を直径と呼ぶことにする)。そこから焼き鈍したりしたけど、15 より下回ることはなかった。今思うと、evima さんのツイートを見て納得した。

つまり、乱択でできるほど甘くはないということだ。さらに、スモールワールドに関する知見から、 i ->  i+1 を固定して、他を色々試すとかもやってみた ( i から  i+1 を決め打ちしてそれに固執したのが敗因)。結局何をやっても決定打に至らなかった。

頂点 0 から任意頂点に行く方法

とりあえず、頂点 0 から任意頂点に行く方法を考えたりもしていた。結果的にはこの方針をもうちょっと頑張っていれば、正解にたどり着けたのかもしれない。頂点 0 から任意頂点に行く方法として、次の 2 通りがあると思っていた

  1. 頂点 0 から最初の一手で遠くまでいって、徐々に範囲を絞っていく (二分探索的アプローチ)
  2. 頂点 0 から最初は近く、徐々に遠くまで行けるようにしていく (ダブリング的アプローチ)

一応両方考えていたんだけど、1 をメインに考えてしまっていた。2 を頑張っていれば AC に至れたのかもしれない。2 のアプローチでは、こんな感じ。

  • 0 から 1 手で 1, 2 に行けるようにする
  • 1 から 3, 4、2 から 5, 6 に行けるようにすれば、0 から 2 手で 3, 4, 5, 6 に行ける
  • 同様に、0 から 3 手で 7, 8, 9, 10, 11, 12, 13, 14 で行けるようにする
  • ...

というふうにすれば、0 から  K 手で  2^{K} 個の頂点に行き渡らせることができる。このときの規則をちゃんと書くと、


頂点  i からは、 2i+1 ( {\rm mod}. N) と  2i+2 ( {\rm mod}. N) へリンクを張る


という感じだ。実はこれですべてが上手くいっている!!!

セグメント木やヒープのアナロジー

セグメント木では頂点  i の子供を  2i+1, 2i+2 にする。そしてセグメント木の根を中途半端なところにとれば、次のことが容易にイメージできる


任意の頂点  i に対して、 K ステップ先の子供をとると、 2^{K} 個の連番になる


その連番の始点がどうなるかはわからないけど、いずれにしてもちゃんと連番になるのだ。よって、毎回  {\rm mod}. N をとれば、任意の頂点から  K ステップで  2^{K} 個の連続する頂点に行けることになる。

よって  N = 1000 に対しては、 K = 10 で十分のようだ。

コード

checker 付き

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

int check(const vector<pint> &G) {
    int N = G.size();
    int res = 0;
    for (int v = 0; v < N; ++v) {
        queue<int> que;
        vector<int> dp(N, -1);
        que.push(v);
        dp[v] = 0;
        while (!que.empty()) {
            int u = que.front();
            que.pop();
            if (dp[G[u].first] == -1) {
                dp[G[u].first] = dp[u] + 1;
                que.push(G[u].first);
            }
            if (dp[G[u].second] == -1) {
                dp[G[u].second] = dp[u] + 1;
                que.push(G[u].second);
            }
        }
        for (int u = 0; u < N; ++u) res = max(res, dp[u]);
    }
    return res;
}

int main() {
    int N;
    cin >> N;
    vector<pint> res(N);
    for (int i = 0; i < N; ++i) {
        res[i].first = i * 2 % N;
        res[i].second = (i * 2 + 1) % N;
    }

    for (int i = 0; i < N; ++i) {
        cout << res[i].first + 1 << " " << res[i].second + 1 << endl;
    }

    // cout << check(res) << endl;
}

Codeforces Round #673 (Div. 1) D. Graph and Queries (R2600)

いろんな方法がありそう。Union-Find のマージ過程を表す木でやったけど、ほかにも Undo 付き Union-Find を使うなど

問題概要

頂点数  N、辺数  M のグラフが与えられる。初期状態では各頂点に  P_{1}, \dots, P_{N} という値がついている (すべて 1 以上で disjoint)。以下の 2 タイプのクエリに  Q 回答えよ。

  • type 1:頂点  v を含む連結成分内で、 P の値の最大値を求めよ。さらに最大となった箇所の  P 値を 0 で書き換えておく
  • type 2:辺  i を削除する

制約

  •  1 \le N \le 2 \times 10^{5}
  •  1 \le M \le 3 \times 10^{5}
  •  1 \le Q \le 5 \times 10^{5}

考えたこと

一般に、グラフの辺削除クエリは答えるのが難しい。よってこういうのは逆から見るのが定石。つまり、クエリをすべて終えて削除し尽くした状態から開始して、辺をつないでいくのだ。

しかし一方では type 1 のクエリは、更新を含むクエリなので、その順に答えていかないと混乱してしまう。

こういうときは、Union-Find の連結状態を保存できる機能が欲しい。一つの方法として、「Union-Find のマージ過程を表す木」を構築する方法がある (詳細は略)。今回もバッチリ。

Union-Find のマージ過程を表す木では、頂点  u と頂点  v がマージされた瞬間に形成されたグループに対応する超頂点を用意していく。そしてその木を Euler Tour することで、そのときそのときのグループが「区間」として取り出せるのだ。

よって、今回の type 1 の操作も

  • ある区間の最大値を求める
  • その最大値を 0 で更新する

という操作だとみなせるので、セグ木で扱える。計算量は  O(N + M \alpha(N) + Q \log N)

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

// 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)];
    }
};

// RMQ
template<class Monoid> struct RMQ {
    Monoid INF;
    int SIZE_R;
    vector<pair<Monoid,int> > dat;
    
    RMQ() {}
    RMQ(int n, const Monoid &inf): INF(inf) { 
        init(n, inf);
    }
    void init(int n, const Monoid &inf) {
        INF = inf;
        SIZE_R = 1;
        while (SIZE_R < n) SIZE_R *= 2;
        dat.assign(SIZE_R * 2, pair<Monoid,int>(INF, -1));
    }
    
    /* set, a is 0-indexed */
    void set(int a, const Monoid &v) { dat[a + SIZE_R] = make_pair(v, a); }
    void build() {
        for (int k = SIZE_R - 1; k > 0; --k) {
            dat[k] = max(dat[k*2], dat[k*2+1]);
        }
    }
    
    /* update, a is 0-indexed */
    void update(int a, const Monoid &v) {
        int k = a + SIZE_R;
        dat[k] = make_pair(v, a);
        while (k >>= 1) dat[k] = max(dat[k*2], dat[k*2+1]);
    }
    
    /* get {min-value, min-index}, a and b are 0-indexed */
    pair<Monoid,int> get(int a, int b) {
        pair<Monoid,int> vleft = make_pair(INF, -1), vright = make_pair(INF, -1);
        for (int left = a + SIZE_R, right = b + SIZE_R; left < right; left >>= 1, right >>= 1) {
            if (left & 1) vleft = max(vleft, dat[left++]);
            if (right & 1) vright = max(dat[--right], vright);
        }
        return max(vleft, vright);
    }
    inline Monoid operator [] (int a) { return dat[a + SIZE_R].first; }
    
    /* debug */
    void print() {
        for (int i = 0; i < SIZE_R; ++i) {
            Monoid val = (*this)[i];
            if (val != INF) cout << val;
            else cout << "INF";
            if (i != SIZE_R-1) cout << ",";
        }
        cout << endl;
    }
};

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

    int N, M, Q;
    cin >> N >> M >> Q;
    vector<int> C(N), X(M), Y(M);
    for (int i = 0; i < N; ++i) cin >> C[i];
    for (int i = 0; i < M; ++i) cin >> X[i] >> Y[i], --X[i], --Y[i];
    vector<int> type(Q), id(Q);
    set<int> delete_edge;
    for (int i = 0; i < Q; ++i) {
        cin >> type[i] >> id[i];
        --id[i];
        if (type[i] == 2) delete_edge.insert(id[i]);
    }
    
    // build tree
    UnionFind uf(N);
    vector<vector<int>> tree(N*2-1);
    vector<int> ancestor(N*2-1);
    iota(ancestor.begin(), ancestor.end(), 0);
    int V = N;
    auto merge = [&](int x, int y) -> void {
        x = uf.root(x), y = uf.root(y);
        if (x == y) return;
        uf.merge(x, y);
        if (uf.root(x) != x) swap(x, y);
        tree[V].push_back(ancestor[x]);
        tree[V].push_back(ancestor[y]);
        ancestor[x] = V++;
    };
    for (int i = 0; i < M; ++i) {
        if (delete_edge.count(i)) continue;
        merge(X[i], Y[i]);
    }

    // enroll queries
    vector<int> qnode(Q, -1);
    for (int i = Q-1; i >= 0; --i) {
        if (type[i] == 2) merge(X[id[i]], Y[id[i]]);
        else qnode[i] = ancestor[uf.root(id[i])];
    }
    
    // Euler Tour
    vector<int> left(N*2-1), right(N*2-1);
    int iter = 0;
    RMQ<int> rmq(N, -(1LL<<29));
    auto dfs = [&](auto self, int v) -> void {
        left[v] = iter;
        if (tree[v].empty()) rmq.set(iter++, C[v]);
        for (auto ch: tree[v]) self(self, ch);
        right[v] = iter;
    };
    for (int v = 0; v < N; ++v) {
        if (uf.root(v) != v) continue;
        dfs(dfs, ancestor[v]);
    }
    rmq.build();
        
    // query
    vector<int> res(Q, -1);
    for (int q = 0; q < Q; ++q) {
        if (type[q] == 2) continue;
        int v = qnode[q];
        auto tmp = rmq.get(left[v], right[v]);
        rmq.update(tmp.second, 0);
        res[q] = tmp.first;
    }
    for (auto val: res) if (val != -1) cout << val << endl;
}

別解:Undo 付き Union-Find

同じように操作を逆順に併合していって、改めてクエリの先頭からこなしていく方法も考えられそう。

type 2 の辺削除クエリは、undo 付き Union-Find の undo 機能によって実現できる (その辺削除がグループを分断するものであるかどうかはあらかじめ求めておく、分断するものでない場合は undo しない)。

Codeforces Round #673 (Div. 1) B. Make Them Equal (R2000)

こどふぉ特有の  X 回以下の操作で〜を達成せよ、という問題

問題概要

長さ  N の正の整数からなる数列  A_{1}, \dots, A_{N} が与えられる。以下の操作を  3N 回以下繰り返すことで、数列の値がすべて等しくなるようにしたい。そのような操作列を一つ求めよ。不可能である場合は -1 を出力せよ。

[操作]:整数  i, j, x を選び、 A_{i} の値を  i \times x だけ減少させて、 A_{j} の値を  i \times x だけ増加させる。ただし  A_{i} の値が負になってはいけない

制約

  •  1 \le N \le 10^{4}

考えたこと

まず、操作をどのように行っても、 A の総和が不変であることに注目する。よって、明らかに  A の総和  S N の倍数でなければならない。

このとき  V = \frac{S}{N} とする。すべての要素をうまいこと  V に揃えたい。色々迷走してしまったが、最終的にたどり着いたのは、次の方針だ。

  • フェーズ I: A_{1} 以外のすべての要素を 0 にする (それが可能であることを後に示す)
  • フェーズ II: A_{1} の要素を各要素に分配していって実現する

フェーズ I が実現可能ならば、フェーズ II は簡単だ。単に  A_{1} から各  A_{i} に対して  x = V として操作を行なっていけばよい。

フェーズ I は、実は  i = 2, 3, \dots, N の順序で行なっていけばよいのだ。最悪ケースは

 A = (1, 1, 1, 1, 1, 4, 5)

のように、ひたすら 1 が並ぶケース。しかし  A_{i} \ge 1 であることに注目しよう。そうすると、

 A_{1} + A_{2} \ge 2

であることが保証される。よって、一旦  A_{1} から  A_{2} へ要素を移して  A = (0, 2, ...) の状態にしてから、 A_{2} から  A_{1} に戻せば  A = (2, 0, \dots) の状態になる。

より一般に、 i = 2, 3, \dots, N の順に

  •  A_{i} i の倍数になるように、 A_{1} から適切な量を  A_{i} に配る
  •  A_{i} が 0 になるように  A_{i} から  A_{1} へ配る

とすれば OK。これで最大でも  3(N-1) 回の操作で実現できる。

コード

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

void solve(vector<long long> &A) {
    int N = A.size();
    long long sum = 0;
    for (auto v: A) sum += v;
    if (sum % N != 0) {
        cout << -1 << endl;
        return;
    }
    long long num = sum / N;

    // 0-indexed
    vector<long long> ri, rj, rx;
    auto run = [&](int i, int j, long long x) -> void {
        //cout << i+1 << " " << j+1 << " " << x << endl;
        if (x == 0) return;
        ri.push_back(i+1), rj.push_back(j+1), rx.push_back(x);
        A[i] -= x * (i+1);
        A[j] += x * (i+1);
    };

    // 0 に集める
    for (int i = 1; i < N; ++i) {
        long long p = A[i] / (i+1), r = A[i] % (i+1);
        if (r == 0) run(i, 0, p);
        else {
            run(0, i, i+1-r);
            run(i, 0, A[i]/(i+1));
        }
    }

    // 0 から揃えていく
    for (int i = 1; i < N; ++i) {
        long long dif = num - A[i];
        run(0, i, dif);
    }

    // 出力
    cout << ri.size() << endl;
    for (int i = 0; i < ri.size(); ++i) {
        cout << ri[i] << " " << rj[i] << " " << rx[i] << endl;
    }
}

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

    int T;
    cin >> T;
    while (T--) {
        int N;
        cin >> N;
        vector<long long> A(N);
        for (int i = 0; i < N; ++i) cin >> A[i];
        solve(A);
    }
}

Codeforces Round #673 (Div. 1) C. XOR Inverse (R2000)

バチャ中に TLE が取れなかった...
 O(N \log N \log A) で実装してしまっていたけど、 O(N \log A) にする必要があったみたい。

問題概要

長さ  N の 0 以上の整数からなる数列  A_{1}, \dots, A_{N} が与えられる。

0 以上の整数  x を適切に定めて

 B_{i} = A_{i} XOR  x

で定まる数列  B_{1}, \dots, B_{N} の転倒数が最小となるようにせよ。転倒数が最小になる場合が複数通り考えられるならば、そのうち  x が最小のものを答えよ。

制約

  •  1 \le N \le 3 \times 10^{5}

考えたこと

例によって、上位桁から順に見ていくとなにかわかったりするのかなと考えた。

 d 桁目を考えているとき、とりあえずそれより上位の桁については値が fix しているものとして、それより上位の桁が等しいもの同士で考えることにする。そしてその中で、 d 桁目が等しいもの同士の転倒数は気にしないことにする (それについてはより下位の桁を考えるときに再考する)。

さて、 d 桁目が 1 であるものと 0 であるものとに分けたとき、

  • (1, 0) の並びになっている箇所の個数を  P
  • (0, 1) の並びになっている箇所の個数を  Q

とする。なお、 d 桁目より上位の桁が異なる部分についてもすべて総合する。このとき

  •  P \gt Q の場合は、 x d 桁目は 1 とする
  •  P \le Q の場合は、 x d 桁目は 0 とする

とすれば OK。重要なことは  d 桁目をどう扱うかは、より低位の桁について考えるときにまったく影響しないのだ。よって上位桁から Greedy に考えていけば OK (実際には上位桁からじゃなくても、各桁ごとに完全に独立に考えることができるので、任意の順序でよい)。

各桁  d ごとの  \min(P, Q) の総和が求める転倒数の最小値となる。計算量は  O(N \log A) ( A を登場する値の最大値とする)。

コード

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

int main() {
    int N;
    scanf("%d", &N);
    vector<int> A(N);
    for (int i = 0; i < N; ++i) scanf("%d", &A[i]);

    int res = 0;
    long long score = 0;
    vector<vector<int>> div(1, A);
    for (int d = 30; d >= 0; --d) {
        vector<vector<int>> nex;
        int mask = 1<<d;
        long long ok = 0, ng = 0;
        for (const auto &v: div) {
            vector<int> zero, one;
            for (auto val: v) {
                if (val & mask) {
                    ok += zero.size();
                    one.push_back(val);
                }
                else {
                    ng += one.size();
                    zero.push_back(val);
                }
            }
            if (!one.empty()) nex.emplace_back(one);
            if (!zero.empty()) nex.emplace_back(zero);
        }
        swap(div, nex);
        score += min(ok, ng);
        if (ok < ng) res += mask;
    }
    printf("%lld %d\n", score, res);
}