競技プログラミング練習会

2019 Normal

第14回

セグメント木、(BIT、)平方分割

担当 :  laft

自己紹介

自己紹介

  • 京都大学工学部情報学科計算機科学コース2回生
  • 本名:平井 雅人 (これは新入生向けに本名を晒しているだけなので、自分でスライド作る時は不要。自己紹介自体はすると良いと思う。)
  • AtCoder色(Rating:1496)
  • CとC++を普段使ってます。
  • ちょっと体調崩してるけど頑張っていきたい…。
    • 早く健康になりたいね

KMC-ID:laft

皆さんの自己紹介もお願いします

  • KMC-ID or 名前
  • 所属(大学など)
  • ICPCに参加するか
  • 意気込み
  • 何か言いたいこと

今日の内容

今日の内容

  • セグメント木
    • RMQ
  • BIT
  • 平方分割

Range Minimum Query (RMQ)

Range Minimum Query(RMQ)

  • サイズNの数列\(\{a_n\}\)について次の2つのクエリをQ個処理せよ。初期値は\(2^{31}-1\)とする。
    • find(\(l,r\)) : \(a_l, a_{l+1}, .. , a_r\)の最小値を求める。
    • update(\(i,x\)) : \(a_i\)の値を\(x\)に更新する。

制約

  • \(1\leqq N \leqq 10^5\)
  • \(1\leqq Q \leqq 10^5\)
  • \(1 \leqq x \leqq 2^{31}-1\) (\(x\)はupdateの第二引数)

愚直解

  • サイズNの配列を作って直感的にやってみる。
  • find(\(l,r\)): \(a_l, a_{l+1}, .. , a_r\)の最小値を普通に調べる。
    最悪全てを計算する必要があるのでO(N)。
  • update(\(i,x\)) : \(a_i\)の値を書き換えるだけ。O(1)。
  • 仮に全てがfindクエリだったとするとO(NQ)で最悪\(10^{10}\)となり、間に合わない

どうする?

  • 前処理で区間の最小値を持っておく?
    • 普通にやると、区間の最小値は、最小値が更新されると次の最小値をO(N)でしか復元できない。(multisetを使うとO(logN)になるが、綺麗に収まらない部分の線形探索がボトルネックになるので無意味)
      • 実は効率よく分割すると解ける。(これが後でやる平方分割
  • ​​そこでセグメント木が登場する。

セグメント木

セグメント木って?

  • 二項演算\(\times\)において、次のことが要素数Nに対しO(logN)でできる。
    • find : \(a_l \times a_{l+1} \times ... \times a_{r-1} \times a_r \) の計算
    • update : \(a_i = x\)の更新
  • 但し、二項演算\(\times\)は次を満たす。
    • 結合律 \(a\times (b\times c) = (a\times b) \times c \)
    • \(a\times e = e\times a = a\)なる単位元eが存在する。
    • (交換律 \(a\times b=b\times a\)を満たす必要はない。)
  • このような二項演算をモノイドと呼ぶ
  • 要はモノイドであればセグ木に乗せられる
  • →応用性が極めて高い!!

二項演算について

  • マグマ:二項演算がその集合で閉じている。
    • 写像:\(\mathbb{N}\times\mathbb{N}\rightarrow\mathbb{N}\)のように、演算元の集合と演算後の集合が同じ。
    • 要は引数と戻り値の型が同じ関数
    • かなり弱い条件。
  • 半群:マグマで結合法則を満たす。
  • モノイド:半群で単位元がある。
  • :モノイドで逆元がある。(後でもうちょい詳しくやります。)

 

マグマから群に進むに連れて、条件が厳しくなる。

RMQはセグ木に乗せられる?

  • 一般に、二項演算minは自然数において結合律は満たすが、単位元を持たない。
  • しかし、競プロでは要素の値が上に有界であるため、単位元としてINF=(要素の最大値以上の値)とすれば良い。
  • よって競プロではモノイドと見做せる。
    • \(\min(a,INF)=\min(INF,a)=a\)
  • 故にRMQはセグ木に乗せて良い

セグメント木の構造

セグメント木の構造

これはRMQの例。

一番下の行に元の配列を入れておく。

セグメント木の構造

その1つ上の行には、各ノードの2つの子のうち小さい方を入れる。

セグメント木の構造

その1つ上の行には、各ノードの2つの子のうち小さい方を入れる。

セグメント木の構造

その1つ上の行には、各ノードの2つの子のうち小さい方を入れる。

セグメント木の構造

その1つ上の行には、各ノードの2つの子のうち小さい方を入れる。

セグメント木の構造

この結果、例えば緑の値は赤の値の区間minとなる。

セグメント木の構造

この結果、例えば緑の値は赤の値の区間minとなる。

セグメント木の構造

この結果、例えば緑の値は赤の値の区間minとなる。

この性質によってクエリを高速に処理できるようになる。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

この赤い部分が指定した5~13番目の範囲。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

セグ木の性質から、この部分だけで十分。

  • 根から葉に向かってDFS。
  • ​常に現在見ている頂点がどこからどこまでの区間minなのかを持って探索する。
    1. もし、5~13の完全に外なら探索打ち切り。影響のない単位元INFを返す。
    2. もし、5~13の完全に内ならそこを部分解として探索打ち切り。得たノードを返す。
    3. どちらでもないなら、DFSを続行し、その結果得た2つの解のminを返す。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

赤線の外なので探索打ち切り!

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

赤線の外なので探索打ち切り!

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

赤線のなので必要な解!

探索は打ち切る。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

赤線のなので必要な解!

探索は打ち切る。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

上から欲しいものを得るまで探索する。

find(\(l,r\))

ex. \(a_5\)から\(a_{13}\)の最小値を求める。(0-indexed)

のminをとれば十分!!

  • \(O(\log N)\)
    • ある頂点が探索されるためには、その親が赤線をまたいでいる必要がある。
    • 赤線をまたぐ頂点は各段で高々2つ
    • よって、探索される頂点は各段で高々4つ
    • 段数は大体 \(\log{N}\)段
    • 従って、\(O(\log N)\)
  • 少なくとも普通に探索するよりも効率が良さそうということだけ伝われば良いです。

find(\(l,r\))

update(\(i,x\))

ex. \(a_3\)を4に書き換える。(0-indexed)

下から順に書き換えていく。

update(\(i,x\))

ex. \(a_3\)を4に書き換える。(0-indexed)

下から順に書き換えていく。

update(\(i,x\))

ex. \(a_3\)を4に書き換える。(0-indexed)

下から順に書き換えていく。

update(\(i,x\))

ex. \(a_3\)を4に書き換える。(0-indexed)

下から順に書き換えていく。

update(\(i,x\))

ex. \(a_3\)を4に書き換える。(0-indexed)

下から順に書き換えていく。

update(\(i,x\))

ex. \(a_3\)を4に書き換える。(0-indexed)

下から順に書き換えていく。

  • \(O(\log{N})\)
    • 段数が\(\log{N}\)段。
    • 各段につき高々1回のアクセス。
  • 元の配列を示す最下段を書き換え、影響をもたらしうるところを下から書き換える。

update(\(i,x\))

  • 構築      : O(N)
    • 頂点数が2N程度なので、下から決めるだけ
  • find       : O(logN)
  • update : O(logN)

セグメント木の計算量

実装

完全二分木を作るために、最初に\(N\)以上の2べき(\(2^x\))のサイズだったことにする。
完全二分木は配列で管理できる!
そこで、下のようにindexを振ることにする。

実装

親から子へのアクセス。

親のindexをnとすると、2*n+1, 2*n+2でそれぞれの子へアクセスできる。

実装

逆に、子から親へのアクセス。

子のindexをnとすると、
\(\lfloor\frac{n-1}{2}\rfloor\)でアクセスできる。

実装まとめ

  • 完全二分木になるように、\(N\)以上の最初の2べきを配列サイズであるとする。
  • \(N\)以降の不要な要素は単位元で初期化しておけば影響を無視できる。
  • 故に、セグ木のサイズは
    \(N+N/2+N/4+...+1=2N-1\)
  • 完全二分木は配列で管理すれば、効率よくアクセスできる。
  • 現在いる頂点のindexを \(i\) とすると、
    • 子のindex\(2i+1, 2i+2\)
    • 親のindex\((i-1)/2\) (割り切る)

コード例:コンストラクタ

struct rmq{
    int n; //size
    vector<int> segtree; //完全二分木を格納した配列
    const int INF = INT_MAX;
    
    rmq(int sz){
    	n = 1;
        while(n<sz) n<<=1; //配列サイズをsz以上の2冪に。
        //セグ木のサイズは2*n-1, 取り敢えず全て単位元で初期化しておく。
        segtree.assign(2*n-1,INF);
    }
    
    int find(int l, int r){
    	//後で
    }
    void update(int i, int x){
    	//後で
    }
};

コード例:find

struct rmq{
    int n; //size
    vector<int> segtree; //完全二分木を格納した配列
    const int INF = INT_MAX;
    
    rmq(int sz){} //やった。
    
    // [l,r)が欲しいminの区間。
    // [a,b)は現在見ている頂点の持つminの区間。最初は全区間。
    // kはその頂点が格納されているindex
    // b=nとしてデフォルト引数にメンバ変数nを使いたかったが、
    // できないのでオーバーロード
    int find(int l, int r){
    	return find(l,r,0,n,0);
    }
    int find(int l, int r, int a, int b, int k){
    	if(b<=l || r<=a) return INF;  //区間外
        else if(l<=a && b<=r) return segtree[k];  //区間内
        //それ以外なら、子を調べてその結果をminしたものを返す。
        else return 
            min(find(l,r,a,(a+b)/2,2*k+1), 
            	find(l,r,(a+b)/2,b,2*k+2));
    }
    
    void update(int i, int x){} //後で
};

コード例:update

struct rmq{
    int n; //size
    vector<int> segtree; //完全二分木を格納した配列
    const int INF = INT_MAX;
    
    rmq(int sz){} //やった。

    int find(int l, int r){} //やった
    
    void update(int i, int x){
    	segtree[i+=n-1] = x; //元の配列のindexは頂点数2*n-1より、n-1足したもの。
        while(i>0){
            i = (i-1)/2;  //親に移動
     	    //子同士を比較して小さい方を親の値として更新。
            segtree[i] = min(segtree[2*i+1],segtree[2*i+2]);
        }
    }
};

コード例:使い方

struct rmq{
    int n; //size
    vector<int> segtree; //完全二分木を格納した配列
    const int INF = INT_MAX;
    
    rmq(int sz){} //やった。

    int find(int l, int r){} //やった
    
    void update(int i, int x){} //やった
};
int main(){
    rmq tr(10);
    tr.update(5,3); //5番目の要素を3に更新する。
    cout << tr.find(0,3) << endl; //INFが出力される
    cout << tr.find(4,6) << endl; //3が出力される
}
    	

BIT(Binary Indexed Tree)

BITって?概要

  • Binary Indexed Treeの略。Fenwick木とも。
  • セグメント木と同様に区間に対してO(logN)で計算できる。
  • セグメント木よりも使える演算が少ないが、定数倍が格段に軽い
  • 実装がセグ木に比べると
  • セグ木の定数倍が気になる事なんてそうそうないし、みんなセグ木のライブラリを持ってて実装をその場でしないので、ぶっちゃけそんなに使わない
  • 「こういうのもあるんだな」くらいのお気持ちで聞いておいてください。

BITって?:演算の制限

  • セグメント木では、モノイドに対して使うことができたが、BITでは(モノイド+逆元)でなければ使うことができない。
    • 逆元の存在:\(\forall a\in\mathbb{S}\)に対し、\(a\times b = b\times a= e\)なる\(b\in\mathbb{S}\)が存在すること。この時、\(b\)を\(a\)の逆元といい、\(a^{-1}\)と表す。
    • 例えば、整数の集合\(\mathbb{Z}\)内で通常の加算\(+\)は、逆元を持ち、\(\forall a\in\mathbb{Z}\)に対し逆元\(-a\)が存在するので群をなす。
    • \(\mathbb{Z}\)内でminは逆元を持たないのでモノイドであるが群をなさない。

BITって?:使われ方

  • 先ほどの話から、minは逆元がなく群でないので、BITに乗せられない。
  • 主に扱うのは、Range Sum Query(RSQ) と呼ばれるような、updateのある区間和の計算である。
    • 一点update, 区間sumをする問題。
    • 当然セグ木で解ける。
    • これによって転倒数を求めるアルゴリズムもある。

BIT:構造

群には逆元があるので、計算で求められる場所を省略できる。

BIT:構造

群には逆元があるので、計算で求められる場所を省略できる。

BIT:構造

群には逆元があるので、計算で求められる場所を省略できる。

計算量

  • add (update) : O(logN)
  • sum (find) : O(logN)
  • セグ木よりも定数倍が軽い。(数倍程度)
  • 必要メモリ量も少ない。(サイズNの配列になる)
  • 実装量も少なくて済む。(数倍程度)
  • ただし応用範囲は狭く、主にRSQに用いられる。

実装

  • 割愛します。
  • 実装について詳しく載ってるのでこのサイトがオススメです。
  • 一応僕が書いたのもこの辺においておきます。(セグ木のコード例と同じ場所。)

平方分割

  • 前処理で区間の最小値を持っておく?
    • 普通にやると、区間の最小値は、最小値が更新されると次の最小値をO(N)でしか復元できない。(multisetを使うとO(logN)になるが、綺麗に収まらない部分の線形探索がボトルネックになるので無意味)
      • 実は効率よく分割すると解ける。(これが後でやる平方分割

RMQの最後のスライド

平方分割って?

  • \(n\)要素の配列を\(\sqrt{n}\)個ごとに区切り、そのn個の要素を区間演算した結果を持っておく。
    • 区切られたグループをバケット(bucket)と呼ぶ。
  • 下はRMQの例。バケットごとの最小値を記録している。

特徴

  • RMQの例のように、バケットごとに何らかの値を持たせ、その情報を使って高速化する。
  • 実装は簡単。応用範囲も広い。

補助データとして持てるもの

  • バケットごとの最小値
    • Range Minimum Queryが解ける。
  • バケットごとの合計
    • Range Sum Queryが解ける。
  • 最小値と合計を2つとも別々に持っておく。
    • RMQとRSQを同時に解ける。
  • バケット内全体の要素に対する変更。
    • 区間Add、区間Updateクエリに対応できる。

例:RMQ

  • 各バケットに自分が管理している\(\sqrt{N}\)要素の最小値を記録しておく。

find

  • 例は区間[3,13]について
  • バケットを完全に覆う区間については、バケットの値をとる。
  • バケットの一部にかかっている区間については、バケットの中で普通に要素を取る。

find

  • 中の要素を見る必要があるのは区間両端のバケツのみ。高々2箇所
  • それ以外はバケットの値を使える。
  • バケットの数は\(\sqrt{N}\)
  • 各バケットは\(\sqrt{N}\)だけ要素を管理している。
  • 最悪でも、バケットの中身*2+バケットの総数= \(3\sqrt{N}\)個の要素を見れば良い。
  • よって、find一回あたりO(\(\sqrt{N}\))

update

  • 要素を更新したバケット内を愚直に調べて、バケットの値を再計算すれば良い。
  • そのバケットの要素も\(\sqrt{N}\)個なのでO(\(\sqrt{N}\))

RMQを解く時の総計算量

  • バケットの構築に、一度全ての要素を調べてバケットごとの最小値を計算する必要があるのでO(N)かかる。
  • クエリごとではfindもupdateもO(\(\sqrt{N}\))
  • 全体でO(N+\(Q\sqrt{N}\))となり、間に合う。

実装(ざっくり)

  • 分割幅\(\sqrt{N}\)は問題の最悪ケースを基準に固定しておくと実装が楽です。
  • 分割幅widthに対し、(N+width-1)/width個のバケットがあれば十分です。
  • バケットのindexはそれが含む区間
    [\(k\sqrt{N}, (k+1)\sqrt{N}\))内の任意のindexを\(\sqrt{N}\)で割ったもの。

補足

  • RMQしかやらなかったけど、他のクエリについては、このサイトを見ると良いと思う。
    • 詳しく載っていて良いです。
  • (普通のセグ木では解けず、遅延セグメント木を使わないとできない)区間更新クエリ(RUQ)にも対応できる(上のサイト参照)
  • \(\sqrt{N}\)ごとに何かするという発想は重要で、たまに使う

コンテスト

コンテスト

終わり!

ごはん食べよう!!!

Made with Slides.com