yukicoder No.511 落ちゲー ~手作業のぬくもり~

https://yukicoder.me/problems/no/511

問題概要

無限行W列のグリッドで、2人ゲームが行われる。このゲームでは2人交互に長方形のブロックを上から落とし、ブロックの各列は一番下か他のブロックにつくまで落下を続ける。ブロックを落とすことである列の高さがHを超えたとき、そのブロックを落としたプレイヤーに1列につき1ポイント入る。すべての列がHを越えたとき多くのポイントを獲得している方が勝者になる。あるゲームでどこにブロックが置かれたかの記録が順番にN個与えられるので、ゲームの勝敗を求めよ。

W, N <= 105

H <= 1015

解法

Binary Indexed Tree で各列の高さを管理しつつ、並列二分探索をする。

まず列の高さをシミュレートするために区間add, 一点取得が高速でできるデータ構造が欲しい。これは区間add, 区間和取得が可能な普通のBITで実現できる。具体的にはimos法のイメージで、[l, r]に一律nを足すときはtable[l]にnを足し、table[r+1]に-nを足す。点xの現在値が知りたいときには[0, x]の区間和を取る。こうすると区間add, 一点取得ともにO(logW)でできるようになる。

次にある1列のみにおいてどちらが得点を得たかを判定してみる。これは前述のBITがあればブロックのクエリを二分探索してO(NlogNlogW)で求めることができる。

この二分探索を全部の列で行うことを考えると、普通にやったらO(NWlogNlogW)になって全く間に合わない。しかし各二分探索で探索する対象は同じであることを考えると、 これを全部並列に行うことで計算量を落とすことができる(並列二分探索)。具体的には以下のような手順で行う。

  • 各列について「今、二分探索のhigh, low, middleはどこか」を覚えておく。
  • BITで普通にブロックの積みをひとつずつシミュレートしていく
  • 途中で「今ここがmiddleである」という列があれば、普通の二分探索のようにhigh, lowの再計算を行う。
  • ブロックを最後まで積み終わったら、また最初に戻って積むことを繰り返す。
  • 全部の二分探索が収束したら終わり

個別の二分探索はlogN回で収束するため、ブロックの積みは高々logN回までしか繰り返されない。各繰り返し内でBITによるブロックのシミュレートはO(NlogW)かかるため、このパートはO(NlogNlogW)になる。また各二分探索の保持を平衡二分木とかでやると一回の処理がO(logW)ででる。そしてW個の二分探索それぞれがlogN回行われることになるので二分探索を捌くのはO(WlogNlogW)になる。2つのパートを合わせてO((N+W)logNlogW)となる。(たぶん)

感想

AGC002Dの解説で並列二分探索を知り、よくわからずにいろいろググってたらこの問題が並列二分探索の例題として挙げられていたのを見つけたので解いてみました。並列二分探索は頭良すぎて理解した瞬間感動しました。実装面の細かい話として最初各にぶたんを溜めておくところで連想配列内の動的配列に入れてたんですが、偏った入力で同じところへのpushがたくさん発生すると動的配列の伸長が起こりまくってクソ遅くなってしまったところ、平衡二分木に切り替えたらまともな速度になりました。そして何気にyukicoderの問題記録を書くのこれが初めての気がします。yukicoderも結構解いてるんですがブログに問題記録を書く習慣をつける前に適正レベルの問題を解きつくしてしまった感じなのであまり書く機会がありませんでした。今後もちょうどいいやつがあれば書いていきたいですね。この記事だけ異常に感想が長いな

コード (D言語)

import std.stdio, std.array, std.string, std.conv, std.algorithm;
import std.typecons, std.range, std.random, std.math, std.container;
import std.numeric, std.bigint, core.bitop;


void main() {
    auto s = readln.split;
    auto N = s[0].to!int;
    auto W = s[1].to!int;
    auto H = s[2].to!long;

    auto A = new int[](N);
    auto B = new long[](N);
    auto X = new int[](N);

    foreach (i; 0..N) {
        s = readln.split;
        A[i] = s[0].to!int;
        B[i] = s[1].to!long;
        X[i] = s[2].to!int - 1;
    }


    auto ans = new int[](W);
    auto rbt = new RedBlackTree!(Tuple!(int, int, int, int), "a[3] < b[3]", true)();

    foreach (i; 0..W)
        rbt.insert(tuple(i, -1, N-1, (N-2)/2));


    for (int settled = 0; settled < W; ) {
        BIT height = new BIT(W+10);

        foreach (i; 0..N) {
            height.add(X[i], X[i]+A[i], B[i]);
            auto nodes = rbt.equalRange(tuple(0, 0, 0, i)).array;

            foreach (node; nodes) {
                auto col = node[0];
                auto lo = node[1];
                auto hi = node[2];
                rbt.removeKey(node);

                if (hi - lo <= 1) {
                    settled += 1;
                    ans[col] = hi % 2 ? -1 : 1;
                } else {
                    if (height.sum(col) >= H) hi = (hi + lo) / 2;
                    else lo = (hi + lo) / 2;
                    rbt.insert(tuple(col, lo, hi, (hi+lo)/2));
                }
            }
        }
    }

    if (ans.sum > 0) writeln("A");
    else if (ans.sum == 0) writeln("DRAW");
    else writeln("B");
}


class BIT{
    int n;
    long[] table;

    this(int n){
        this.n = n;
        table = new long[](n+1);
    }

    void add(int i, long x){
        i++;
        while (i <= n){
            table[i] += x;
            i += i & -i;
        }
    }

    // [l, r)
    void add(int l, int r, long x) {
        add(l, x);
        add(r, -x);
    }

    // [0, i]
    long sum(int i){
        i++;
        long ret = 0;
        while (i > 0){
            ret += table[i];
            i -= i & -i;
        }
        return ret;
    }

    // [l, r)
    long sum(int l, int r){
        if (l >= r) return 0;
        return sum(r-1) - sum(l-1);
    }
}