セグメント木を育てる 2

・問題を区間に言い換える

ARC033-C_データ構造
集合Sに数字が追加されていくので、下からX番目の数字が何かを答える問題です。
set等で解けそうな感じですが、一番大きいor一番小さいはO(1)で分かるのですが、下からX番目は辿っていくしかなくO(N)かかっちゃいます。

・数字Xが下から何番目かを記録する配列を持つ。
 EX.5 8 3 9 2と数字が追加されるとします。
 0 0 0 0 0 0 0 0 0 という要素9個の配列を用意し、ここに1~9の値が下から何番目かを記録していきます。
 5 → 0 0 0 0 1 1 1 1 1
 8 → 0 0 0 0 1 1 1 2 2
 3 → 0 0 1 1 2 2 2 3 3
 9 → 0 0 1 1 2 2 2 3 4
 2 → 0 1 2 2 3 3 3 4 5
 例えば、5の場合は、5番目の要素3が答えになります。
 実際に5より小さい数字は2と3なため、5は3番目に小さいです。
 こうすることで嬉しいことは、できた配列が昇順になるため、二分探索(C++でいうlower_bound)でX番目に小さい数字が求められることです。
 更に更新された値を見てみると、まるで累積和のような更新の仕方になっています。区間で言い換えると、数字Aが追加された場合、区間(A,N)が1増える。と言えます。

・セグ木を累積和用に改造
minをプラスに変えると、累積和が求められるようになります。
範囲外はLLONG_MAXではなく0を返すようにしましょう(プラスに変えたため、LLONG_MAXのままだと、計算に影響が出ます。0の場合、0を足しても答えは変わらないため影響が出ません。)

int seg_find(int x,int y){
  struct segF{
      static int segfind(int x,int y,int k,int l,int r){
          //cout<<x<<","<<y<<","<<k<<","<<l<<","<<r<<endl;
          if(r<x||l>y) return 0;
          if(l>=x&&r<=y) return seqv[k];
          else{
              int segl=segfind(x,y,k*2+1,l,(l+r)/2);
              int segr=segfind(x,y,k*2+2,(l+r)/2+1,r);
              return segl+segr;
          }
      }
  };
  return segF::segfind(x,y,0,0,seqv_n);
}

void seg_update(int n,int a){
  int k=n+seqv_n;
  seqv[k]=a;
  while(k>0){
      k=(k-1)/2;
      seqv[k]=seqv[k*2+1]+seqv[k*2+2];
  }
}

・上記のセグ木を使って実装
上記のセグ木を使い、二分探索で答えを探すコードを実装します。

signed main(){
   int Q;
   cin>>Q;
   seg_init(200000,0);
   for(int i=0;i<Q;i++){
       int T,X;
       cin>>T>>X;
       if(T==1){
           seg_update(X,1);
       }
       else{
           int l=0;
           int r=200000;
           while(1){
               int mid=(l+r)/2;
               int n=seg_find(0,mid);
               //cout<<mid<<","<<n<<endl;
               if(n>=X){
                   if(r==mid) break;
                   r=mid;
               }
               else{
                   if(l==mid) break;
                   l=mid;
               }
           }
           cout<<r<<endl;
           seg_update(r,0);
       }
   }
}

区間じゃなさそうな問題を区間に言い換えることにより、セグ木を使ってACすることができました。

・BIT

上記の実装のfindを見てほしいのですが、

int n=seg_find(0,mid);

と、実は0から始まる区間のfindしか行っていません。
サイズ8のセグ木に対して0から始まる区間のfindを考えた場合
区間は(0,7),(0,3),(0,1),(0,0),(1,1),(2,3),(2,2),(3,3),(4,7),(4,5),(4,4),(5,5),(6,7),(6,6),(7,7)の15個ありますが、
(0,0) = (0,0)
(0,1) = (0,1)
(0,2) = (0,1)+(2,2)
(0,3) = (0,3)
(0,4) = (0,3)+(4,4)
(0,5) = (0,3)+(4,5)
(0,6) = (0,3)+(4,5)+(6,6)
(0,7) = (0,7)
となっており、8種類しか使っていません。
https://algo-logic.info/binary-indexed-tree/
こちらのページにある図を見ると分かりやすいのですが、セグ木で左右に枝分かれするときの右側のノードは、実は0から区間を計算するときには使われないのです。

BITと呼ばれるデータ構造はこのことに注目し、0から区間でしか計算できない代わりに、使われない個所を削ったものになります。

・BITの実装

http://hos.ac/slides/20140319_bit.pdf
これが分かりやすいです。

各ノードにへんてこな順序を付けると、
下位から見て初めて1があるbitが何番目かで、区間の長さが分かり
区間の長さから、次に更新/探索する位置が分かります。

下位から見て初めて1があるbitが何番目かは、

x&-x

とすると分かります。x=10の場合、
x= 10=1010
x=-10=0110
   &を取ると、0010となります。
絵を書けばわかりますが、10の区間の長さは2です。マイナスの補数表現によって、上手いこと炙り出せています。

上記のことを踏まえて、実装は以下になります。

void bit_init(int N,int a){
   seqv.clear();
   int n=N+1;
   seqv_n=n;
   for(int i=0;i<n;i++){
       seqv.push_back(a);
   }
}
void bit_update(int n,int a){
   seqv[n]+=a;
   while(n<=seqv_n){
       n+=(n&-n);
       seqv[n]+=a;
   }
}
int bit_find(int y){
   int ret=0;
   while(y>0){
       ret+=seqv[y];
       y-=(y&-y);
   }
   return ret;
}

上記BITを用いて

signed main(){
   int Q;
   cin>>Q;
   bit_init(200000,0);
   for(int i=0;i<Q;i++){
       int T,X;
       cin>>T>>X;
       if(T==1){
           bit_update(X,1);
       }
       else{
           int l=0;
           int r=200000;
           while(1){
               int mid=(l+r)/2;
               int n=bit_find(mid);
               if(n>=X){
                   if(r==mid) break;
                   r=mid;
               }
               else{
                   if(l==mid) break;
                   l=mid;
               }
           }
           cout<<r<<endl;
           bit_update(r,-1);
       }
   }
}

とすることでACが取れました。
セグ木ではメモリ7036 KB、速度634 msに対し、
BITではメモリ4876 KB、速度366 msでした。
時間的にもメモリ的にもセグ木より優れているといえます。

なお、値の更新の際に、セグ木は左右のノードを見て再計算するのに対し、BITは上に値を伝播させていきます。そのため、削除時にノードに設定する値が少し異なります。

・BITのついで

https://note.com/nullkara/n/n35a61d04c3a8
前に記載したABC174のF問題がそうなのですが、累積和の性質を使えば(0,N)区間しか求められませんが、好きな区間の和を求めることができます。
例えば、区間(4,8)を知りたい場合は、区間(0,8)-区間(0,3)とすれば良いです。

そのため、BITは競プロでは任意区間の累積和を高速に簡単に求めるデータ構造として使われています。(多分)

・さいごに

ABC174のFのnoteを書いていたときはBITの動きが理解できませんでしたが、今なら何となくわかります。

この記事が気に入ったらサポートをしてみませんか?