えびちゃんの日記

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

要素の追加・削除と mex を対数時間で処理するよ (2) + 区間 mex クエリ

rsk0315.hatenablog.com

エゴサしたらこの記事がわりと読まれているっぽかったものの、やや実装が重い気がしたので*1別の解法を書きます。

tl; dr

セグ木。

定義

有限集合 \(S\subseteq \N\) における mex というのは、\(S\) に含まれない最小の自然数 \(\min_{i\in \N\setminus S} i \) です。

やりたいこと

  • 追加:\(S\gets S\cup\{i\}\) で更新
  • 削除:\(S\gets S\setminus\{i\}\) で更新
  • mex:\(\min_{i\in \N\setminus S} i\) を出力

これを (amortized) \(O(\log(|S|))\) 時間とかで。

アイディア

クエリ数が \(n\) のときのことを考えます。このとき、mex が \(n\) を超えることはないことに注意します。 \(S = \{0, 1, \dots, n-1\}\) のとき最大値 \(n\) になります。

そこで、bool の配列 \(a = (a_0, a_1, \dots, a_{n-1})\) を \(a_i = (i\in S)\) が保たれるように管理します。

求めたい mex は、\(\wedge_{i\in[0, x)} a_i\) が true になる最大の \(x\) です(このとき \(a_x\) が false となり、定義から \(x\notin S\) となるので)。 よって、\(a\) をセグ木で管理し、この \(x\) を二分探索(ACL で言うところの max_right)で求めればよいです。

\(n\) 以上の値を \(S\) に入れると(クエリ数 \(n\) の下で)mex は \(n\) 未満になることに注意すると、\(n\) 以上の値の更新クエリは無視してよいです。

クエリ数の仮定の除去

クエリ先読みができないときとか、そういう仮定が気に食わないとかで、\(n\) の決め打ちをしたくないことはあります。 そこで、いつものテクを使います。

まず、\(n = 1\) と思って処理します。 \(|S| = n\) になったら \(n\gets 2\cdot n\) で更新し、今までの \(|S|\) に基づいて \(a\) を再構築します。無視されていたもののうち \([n, 2n)\) の値が新しく考慮されることになります。

再構築は \(O(n)\) 時間でできるので、(償却)計算量を悪化させません。

あるいは、mex が \(|S|\) になる限り倍々に増やすとかでもよいです。

実装例:Rust, C++

冷静になると、(\(n\) を倍々にできるので)\(\{0, 1, \dots, n\}\setminus S\) を std::set で管理して upper_bound とかすればよくないですか?

セグ木を使う場合でも、\( (i\in S\cap\{0, 1, \dots, n-1\}, i)\) の pair を持って全体の \(\min\) を計算する手もあります。

発展編

ひとつめ

もう少しリッチなクエリを処理したくなるときもあります。 \(S\) を集合ではなく配列とします。

  • 初期化:\(S \gets ()\)
  • 末尾への追加:\(S \gets S \concat\, (x)\)
    • \(\concat\) は [1,2,3] ++ [4,5] == [1,2,3,4,5] のイメージ。
    • \( (x)\) は 1 要素の配列のこと。
  • 区間 mex:\(\mex\,\{ S_i \mid i\in[l, r) \}\)
    • さっきのは \(l=0\) と見なせる。

さっきと同様にとりあえずクエリ数を \(n\) とします。また、一旦は \(r = |S|\) として考えます。 \(a = (a_0, a_1, \dots, a_{n-1})\) を管理し、\(S\) における \(i\) の最後の出現位置を \(a_i\) とします。出現していないときは \(-\infty\) とします*2

mex クエリの答えは、\(\wedge_{i\in[0, x)} (a_i \ge l)\) となる最大の \(x\) で、\( (\min_{[0, x)} a_i) \ge l\) となる最大の \(x\) を求めればよいです。セグ木でにぶたんです。

\(n\) の仮定の除去はさっきと同様にします。 \(r\) に関してはちょっと大変です。セグ木に対して「\(|S| = r\) だったときの値は?」という質問をする必要があるので、永続セグ木を使えばよいです。

37zigen.com

書きながら思い出したのですが、検索したら見つかりました。

例題:

atcoder.jp

解法(ネタバレ) 考察の過程で、以下で定まる配列 \(g\) を求めたくなります。

\[ g_i = \begin{cases} 0, & \text{if }i = 0; \\ \mex_{j\in[1, c_i]} g_{i-j}, & \text{otherwise}. \end{cases} \]

\(g_i = x\) を \(g \gets g \concat\, (x)\) だと見なせて、\(l = i - c_i\) に対応します。

最終的な答えは、以下により定まります(\(\oplus\) は xor)。

\[ \bigoplus_{i\in[0, n)} g_i\cdot (a_i \bmod 2). \]

atcoder.jp

ふたつめ

次に、更新クエリ \(S_i \gets x\) を考えます。これはわかりません。

\(S_i\gets x\) というのは「過去に遡り、\(|S| = i\) のときに \(S\gets S\concat\,(S_i)\) ではなく \(S\gets S\concat\, (x)\) をしたことにする」というのに対応します。

この形式の「過去に遡り、これこれの操作をしたことにする」というクエリを処理するデータ構造は retroactive data structure と呼ばれています*3

なので、セグ木を retroactive にすればよいのですが、これってできるんですかね。

なんか このポスター の General Approach のところを見ると、non-retroactive DS を retroactive にするような話があるような気がするのですが、一般的なテクはなかったような気がする (https://erikdemaine.org/papers/Retroactive_TALG/paper.pdf :: Theorem 2) ので、わかりません。何らかの文脈があるのかも。

ねむいので今日はここまでです。あとでまた読みます。

めも

他のアプローチとして、更新できる WM でなんとかなったりしない? んーむずかしいかも。

更新できる方の Mo は使えそう。ちなみにまだ理解してません。

おわり

ねこ。

*1:昔の実装を見たら気のせいでした。

*2:実装上は、最後の出現位置が \(j\) のとき \(a_i = j+1\)、出現しないとき \(a_i = 0\) とかにするといいかもしれません。人によるかも。

*3:より正確には「操作列に対する操作の insert/delete を行える」データ構造な気もします。