裏紙

ほぼ競プロ、たまに日記

Codechef January Challenge 2018 - Killing Monsters

問題

Contest Page | CodeChef

問題概要

モンスターがn体いて、それぞれに0からn-1までのIDが振られている。iのIDを振られているモンスターは、はじめ体力がh_iある。次のようなクエリがq個来る:

  • クエリ(x,y) : k & x = kを満たすkについて、kのIDを振られているモンスターの体力をy減少させる。

各クエリの終了時点で、生き残っている(体力が1以上)モンスターが何体いるかを答えよ。

  •  1 \le n \le 2^{17}
  •  1 \le h_i \le 10^9
  •  1 \le q \le 2^{18}
  •  0 \le x \le 10^9
  •  1 \le y \le 10^9

イデア

クエリの性質上、クエリを区間に分割して高速化する、みたいなことは出来なさそう。そこで、今回は「クエリの平方分割」を行う。

まず、与えられる全クエリを時系列順でブロックに分ける(1ブロックの中に含まれるクエリは B = \sqrt{q} \ 個)。

そして、そのブロックを順番に処理していく。あるブロックについて、そのブロック内のクエリをまとめて、それぞれのモンスターにどれくらいのダメージが入るのかを計算する。この計算にかかる計算量は、Segtreeの配列の構造をイメージすると、各段で右隣が左隣に足し込まれるイメージ(最上位ビットが0か1かの違いで、最上位ビットが1の方に入るダメージは0の方にも入る)を持つと、分割統治的に区間を伝播させることが出来るのでO(nlogn)で処理ができる(実装も参照)。

そうすると、そのタイミングで死んでしまうモンスターが分かるので、そのモンスターに注目してそのブロック内のクエリをもう一度追いかけて、死ぬタイミングがちょうどどのクエリなのかを明らかにする。この操作には1モンスターに対してO(B)かかる。

以上のことをブロックごとにやっていくと、モンスターが死ぬタイミングのチェックはクエリ全体を処理する中でn回だけ発生するので、全体としてO(nB) = O(n \sqrt{q})で全ての処理が完了できる。

実装(C++)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define rep(i,n) for(int (i)=0;(i)<(int)(n);++(i))
#define all(x) (x).begin(),(x).end()
#define pb push_back
#define fi first
#define se second

const int B = 600;
const int N = 1<<17;

// 累積ダメージ
ll d[N]={};
// ブロック内のダメージ
ll t[N]={};

// [l,r)の区間に伝播
void f(int l, int r){
    if(r-l==1) return;

    int m = (l+r)/2;
    f(l,m);
    f(m,r);

    // 最上位ビットが1のやつに入るダメージは最上位ビットが0でも入る
    rep(i,m-l) t[i+l] += t[i+m];
}

int main(){
    int n;
    cin >>n;
    vector<int> h(n);
    rep(i,n) cin >>h[i];

    int Q;
    cin >>Q;
    vector<int> x(Q),y(Q);
    rep(i,Q){
        cin >>x[i] >>y[i];
        x[i] &= (N-1);
    }

    vector<int> ans(Q);
    rep(i,B){
        if(B*i>=Q) break;
        fill(t,t+N,0);
        for(int j=B*i; j<min(B*(i+1),Q); ++j) t[x[j]] += y[j];

        f(0,N);

        vector<int> check;
        rep(j,n){
            // このブロック内で死ぬモンスター
            if(d[j]<h[j] && h[j]<=d[j]+t[j]) check.pb(j);
        }

        for(int j:check){
            ll dam = d[j];
            for(int k=B*i; k<min(B*(i+1),Q); ++k){
                if((j&x[k]) == j) dam += y[k];
                if(dam>=h[j]){
                    --ans[k];
                    break;
                }
            }
        }

        // 累積の方に渡す
        rep(j,n) d[j] += t[j];
    }

    ans[0] += n;
    for(int i=1; i<Q; ++i) ans[i] += ans[i-1];
    rep(i,Q) cout << ans[i] << endl;
    return 0;
}