えびちゃんの日記

えびちゃん(競プロ)の日記です。

非再帰セグ木上の任意始点にぶたん

セグ木は知っているとします。 知らない人は えびちゃんのスライド なり適当な記事なりを読んでください。

セグ木上のにぶたんも上のスライドに載っていますが、もう少し解説したいかなと思います。 上のスライドにある図を見るとイメージが湧きやすいかもしれません。

愚直にやると、セグ木の区間*1取得と、二分探索で、ふたつ log がついてしまいます。 セグ木の実装を非再帰でやって、\([l, r)\) の区間和を \(O(\log(r- l))\) でできるとしても、オーダーは落ちません。

参考までに、\(n = 10^6\) とすると、\(n\log(n)^2\) は \(n\sqrt{n}/2.5\) 程度ですが、\(n\log(n)\) は \(n\sqrt{n}/50\) 程度です。 落とせる log は落としておきたい気がします。

この方針以外の実装もある気がしますが、えびちゃん的にはこれが好きなので、とりあえずこれを紹介します。 気づいたらそのうち宗派が変わっているかもしれません。

定義

セグ木の葉の個数を \(n\) とします。 セグ木の元の配列長が \(n\) ということです。 区間 \([l, r)\) の和を \(a[l, r)\) と書くことにします。

左端を固定するとき

以下を満たす述語 \(p\) が与えられるとします。

  • 単位元 \(e\) に対して \(p(e)\) が true
  • ある \(i\) が存在して \(p(a[l, i))\) が false であれば、任意の \(i \lt j \le n\) に対して \(p(a[l, j))\) が false

このとき、\(p(a[l, i))\) が true かつ、次のいずれかを満たす \(i\) を返します。

  • \(i = n\)
  • \(i < n\) かつ \(p(a[l, i+1))\) が false

右端を固定するとき

以下を満たす述語 \(p\) が与えられるとします。

  • 単位元 \(e\) に対して \(p(e)\) が true
  • ある \(i\) が存在して \(p(a[i, r))\) が false であれば、任意の \(0 \le j \lt i\) に対して \(p(a[j, r))\) が false

このとき、\(p(a[i, r))\) が true かつ、次のいずれかを満たす \(i\) を返します。

  • \(i = 0\)
  • \(i \gt 0\) かつ \(p(a[i-1, r))\) が false

補足

一度 false になると、区間を広げても false であり続ける述語を受け取ります。 条件を満たす最大の区間を求めているのに相当します。 両方の場合において、引数と返り値を昇順に \(l, r\) とすると、\(p(a[l, r))\) は true となります。

\(p(e)\) が true であることを要求しているのは、そうしないと返り値がつらくなるためです。 -1 を返すのはあまりうれしくないし、そもそも単位元の時点で false なら、呼び出す前から答えがわかりますし。

なお、これは ACL のものの定義と同じです。

簡単な場合

まずは、セグ木を完全二分木に限定し、二分探索の始点を左端に固定した場合を考えます。 全区間の和を述語に渡した場合は false になるとします(そうかどうかの判定は述語を高々一回呼べばわかる)。

これは、必要な情報を管理しながら、ノードを下っていくことでできます。 必要な情報というのは、「今まで足されている値」「今見ている添字」くらいだと思います。

適当に非再帰で簡単に実装できます。 たとえば、左に潜るなら i <<= 1 のようなことをすればよいです。

auto x = e;  // 今まで足した値。単位元で初期化
while (i < n) {
  i <<= 1;
  auto tmp = op(x, a[i]);
  if (p(tmp)) {
    // 左の子を足しても大丈夫
    x = tmp;  // 足した結果で上書き
    i += 1;
  } else {
    // 左の子を足すとだめ
    // 特に何もしなくていいね
  }
}
return i - n;

一般の場合(左端を固定)

セグ木を完全二分木に限定せず、二分探索の始点を特に限定しない場合を考えます。始点を \(l\) とします。

セグ木で \([l, n)\) の区間和を求める際に見るノードを、左から順に見ていきます。 このノードたちの添字は、非再帰セグ木のループを使うと簡単に求められます(順序には注意)。

必要に応じて自分で図を書いたり、上のスライドの図を見るといいかもしれません。

これらのノードを順に見ていきながら、「ここまで加えると条件を満たさなくなる」という境界を探します。 このノードは \(O(\log(n))\) 個なので、単に線形探索するだけで問題ありません(ここすき)。

さて、そのようなノードが見つかったとします*2。 重要なこととして、あるノードの子たちを見ると、完全二分木になっています。 このことから、上で示した簡単な場合に帰着されるので、\(O(\log(n))\) 時間でできます。

以上を合わせて、\(O(\log(n))\) 時間で答えを求められます。

一般の場合(右端を固定)

上記で左から見ていたところを、右から見るようにするだけです。 off-by-one とかには注意。

遅延セグ木の場合

必要に応じて伝播しながらやると、同じ感じでできます。

おわり

候補が \(O(\log(n))\) 個なので線形探索をしても \(O(\log(n))\) 時間で済むパートすき。

*1:適宜、和はモノイド積と読み替えてください。

*2:見つからなければ全区間が条件を満たします。