整数が格納されているリストがあります。ここから a 以上 b 以下の整数を削除するのであれば、以下のコードでできます。

しかしリストが長く、何度も削除する処理が繰り返される場合、時間がかかります。要素を削除すると抜けた部分を詰める処理が必要になるので、そのぶん遅くなってしまうのです。

要素が削除されることでリストのサイズが小さくなってくれれば終盤の処理は速くなってくれるかもしれませんが、以下のような処理の場合それもあまり期待できません。

今回はリストのなかからある区間の値を削除する処理を何度も繰り返さなければならないとき、処理をいかにして高速化するかを考えます。

RemovableMinMaxListクラスを自作する

まずリストに格納されている値をソートします。そして削除する値の区間が [min, max] であるとすれば、リストのなかで削除する部分のIndexがどうなるかを調べます。これは二分探索法を使えば O(log N)(Nはリストの長さ)でわかります。

削除する区間のIndexがわかったらその部分を実際に削除するかわりにその部分に印をつけます。残った部分がなにか知りたいときだけ残っている要素はなにか、削除された要素はなにかを返すようにします。imos法を使えば削除する区間の開始部分と終了部分に印をつける処理は O(1)、残っている要素と削除された要素を全列挙する処理は O(N) でできます。

ここでは ac-library-csharp を使っています。

参考:ac-library-csharpを使ってみる

コンストラクタ

コンストラクタで引数が渡されたらソートして long型のリストに格納します。Imosは要素に対して何回削除処理がおこなわれたかをカウントするときに必要になります。

削除する区間の開始点と終了点に印をつける

[min, max]を削除するときは二分探索法でA[x]がmin以上になる最小のx、A[y]がmaxを超える最小の y を探します。x から y – 1 までが削除する区間です。なのでImos[x]++;Imos[y]– とします。

結果を取得する処理

結果を取得する処理を示します。Imosの累積和を計算します。このときの各要素の値がその要素が削除された回数です。この値が1以上であればその要素は削除されていることになります。あとは削除された要素とされていない要素をわけて結果を返すだけです。

使ってみる

D – Santa Claus 2

問題の趣旨は以下のとおりです。

XY平面上には最大20万個の家がある。
サンタクロースがXY平面上を上下または左右に最大20万回移動する。
サンタクロースがすべての移動を完了したときサンタクロースの座標とサンタクロースが通過した家の個数を求めよ。

数直線上ではなくXY平面上なのでちょっと面倒ですが、X成分とY成分にわけて考えるとよさそうです。

X座標が同じ家のリストとY座標が同じ家のリストをつくり、サンタクロースが上下に移動するときは前者の対応するリストのなかからMin(移動開始Y座標, 移動終了Y座標)からMax(移動開始Y座標, 移動終了Y座標)までの要素を取り除き、左右に移動するときは後者の対応するリストのなかからMin(移動開始X座標, 移動終了X座標)からMax(移動開始X座標, 移動終了X座標)までの要素を取り除きます。

最後にX座標が同じ家のリストから取り除かれた要素を調べて、これに対応する要素をY座標が同じ家のリストから取り除きます。最終的にY座標が同じ家のリストから取り除かれた要素の数がサンタクロースが通過した家の個数となります。

機能を拡張する

自作したRemovableMinMaxListですが、リストを標榜しているのにランダムアクセスができません。削除されていない要素がいくつあるのか、先頭から i 番目の要素の値は何なのかがわかるようなものをつくることはできないのでしょうか?

そうだ!セグ木で殴ろう!

遅延評価型セグメント木の準備

遅延評価型のセグメント木を用意してすべての要素を 1 で初期化します。そして区間[min, max]を削除するときはセグメント木の区間[minIndex, maxIndex]を0で更新します。こうすることでセグメント木の区間[a, b]の総和を調べることで対応する区間の値で削除されていない値の個数を調べることができるようになり、これを二分探索することで削除されていない要素のうち先頭から i 番目がなにかも知ることができます。

リスト内の削除されていない要素の数を知りたいので、Operate(int x, int y) でセグメント木の各要素のうち 1 である要素の数を数えます。これは x + y でできます。

要素を削除する場合、区間にtrueを作用させます。なので作用の単位元 FIdentity は falseです。区間に true を作用させたらその要素の値は 0、それ以外のときは現状維持なので Mapping(bool f, int x) は f ? 0 : x、また区間Aへの作用と区間Bへの作用が重なったときは true を優先させたいので Composition(bool nf, bool cf) は nf | cf です。

コンストラクタ

コンストラクタを示します。セグメント木に全要素が 1 の配列をセットしています。

要素の数を取得する

リスト内にある削除されずに残っている要素の数、リスト内にある min 以上 max 以下の値の個数を取得する処理を示します。二分探索法で対応するindexを求め、セグメント木のその区間の総和を求めれば取得できます。

削除処理

リスト内の[min, max]を削除する処理を示します。前述のとおり二分探索法で削除する区間のindexを求めてセグメント木の区間に削除されたことを意味する true を作用させています。

引数で指定された値をひとつだけ削除する処理を示します。

まず引数で指定された値がリスト内にどれだけ残っているかを調べます。0 なら何もする必要はありません。存在する場合は後ろにあるものから削除します。

先頭から i 番目の要素はなにか?

削除されていない要素のうち先頭から i 番目の要素がなにかを求める処理を示します。

リストの区間[0, x]で削除されていない要素の数が i + 1 になるような最小の x を求めます。これはセグメント木に対して二分探索をしかければ求めることができます。A[x] が求めるべき値です。

v は先頭から何番目か?

引数で指定された値はリスト内に残されている値のなかで先頭から何番目にあるかを取得する処理を示します。これも二分探索法とセグメント木の合せ技で取得可能です。

削除された要素と削除されずに残った要素を取得する処理を示します。