区間MEXの重要な性質とそのアルゴリズム

はじめに

この記事では、数列における区間MEX(Minimum Excluded Value)の重要な性質について考察します。MEXとは、数列に含まれていない最小の非負整数を指します。特に、「極小MEX区間」と呼ばれる概念に焦点を当て、その数がO(n)に収まることを証明し、効率的なアルゴリズムを提案します。

極小MEX区間の定義と重要性

極小MEX区間とは、区間の左端または右端を1つ削除するとMEX値が変化する区間のことです。この性質は、数列の区間MEX情報を効率的に管理するための基礎となります。

すべての区間[l,r]を座標(l,r)として平面直角座標系上にプロットすると、極小MEX区間の座標からO(n)個の矩形を構築できます。これにより、任意の部分区間のMEX関連情報を高速にクエリできるようになります。

極小MEX区間がO(n)個であることの証明

極小MEX区間がO(n)個しか存在しないことを証明します。まず、極小MEX区間を2つのグループに分類します:

  • グループ1: al < ar を満たす区間
  • グループ2: al > ar を満たす区間

ここで、al = ar の場合、その区間は極小MEX区間ではないことに注意してください。

グループ1に着目し、各右端点rに対して対応する左端点lが一意であることを示します。仮にl' < l ≤ rなるl'が存在すると、mex(l',r) ≥ mex(l,r)となります。グループ1の定義よりal' < arであり、al'がMEXを増加させないため、[l',r]は極小MEX区間ではありません。

右端点の数はn個しかないため、グループ1の区間数はn以下です。同様にグループ2の区間数もn以下です。したがって、極小MEX区間の総数は2n以下、すなわちO(n)個であることが証明されました。

アルゴリズムの実装

極小MEX区間を効率的に求めるためのアルゴリズムを実装します。まず、永続セグメント木を用いて区間MEXを高速に計算できるようにします。

namespace PersistentSegmentTree {
    struct Node {
        int left, right;
        int min_val;
    } tree[MAX_NODES * 60];
    
    int create_node(int x) {
        static int idx = 0;
        ++idx;
        tree[idx] = tree[x];
        return idx;
    }
    
    void update(int& x, int l, int r, int pos, int val) {
        x = create_node(x);
        if (l == r) {
            tree[x].min_val = val;
            return;
        }
        int mid = (l + r) >> 1;
        if (pos <= mid) {
            update(tree[x].left, l, mid, pos, val);
        } else {
            update(tree[x].right, mid + 1, r, pos, val);
        }
        tree[x].min_val = min(tree[tree[x].left].min_val, tree[tree[x].right].min_val);
    }
    
    int query(int x, int l, int r, int threshold) {
        if (l == r) return l;
        int mid = (l + r) >> 1;
        if (tree[tree[x].left].min_val < threshold) {
            return query(tree[x].left, l, mid, threshold);
        } else {
            return query(tree[x].right, mid + 1, r, threshold);
        }
    }
    
    struct SegmentTree {
        int root;
        struct Iterator {
            int pos;
            SegmentTree* tree;
            void operator=(int v) {
                update(tree->root, 0, MAX_N + 1, pos, v);
            }
            operator int() {
                return query(tree->root, 0, MAX_N + 1, pos);
            }
        };
        Iterator operator[](int p) {
            return {p, this};
        }
    };
}

MEX値0と1の区間を事前に処理します。次に、極小MEX区間の特徴を活かして構築します。区間[l,r]のMEXがvである極小区間について、lより小さい最初の位置Lとrより大きい最初の位置Rを見つけます。

vector> mex_intervals[MAX_VALUES];

namespace MexCalculator {
    vector<int> positions[MAX_VALUES];
    PersistentSegmentTree::SegmentTree seg_trees[MAX_VALUES];
    
    int compute_mex(int l, int r) {
        return seg_trees[r][l];
    }
    
    int find_prev(int pos, int val) {
        auto it = lower_bound(positions[val].begin(), positions[val].end(), pos);
        if (it == positions[val].begin()) return -1;
        return *(--it);
    }
    
    int find_next(int pos, int val) {
        auto it = upper_bound(positions[val].begin(), positions[val].end(), pos);
        if (it == positions[val].end()) return -1;
        return *it;
    }
    
    void process_mex_intervals(int v) {
        sort(mex_intervals[v].begin(), mex_intervals[v].end());
        vector> filtered;
        for (auto [l, r] : mex_intervals[v]) {
            while (!filtered.empty() && r <= filtered.back().second) {
                filtered.pop_back();
            }
            filtered.push_back({l, r});
        }
        mex_intervals[v] = filtered;
        
        for (auto [l, r] : mex_intervals[v]) {
            const int L = find_prev(l, v);
            const int R = find_next(r, v);
            if (L != -1) {
                mex_intervals[compute_mex(L, r)].push_back({L, r});
            }
            if (R != -1) {
                mex_intervals[compute_mex(l, R)].push_back({l, R});
            }
        }
    }
    
    void solve() {
        for (int i = 1; i <= n; i++) {
            seg_trees[i] = seg_trees[i - 1];
            seg_trees[i][array[i]] = i;
            positions[array[i]].push_back(i);
            
            if (array[i] != 0) {
                mex_intervals[0].push_back({i, i});
            } else {
                mex_intervals[1].push_back({i, i});
            }
        }
        
        for (int i = 1; i <= n + 1; i++) {
            process_mex_intervals(i);
        }
    }
}

アルゴリズムの分析

このアルゴリズムの時間計算量はO(n log n)です。セグメント木の更新とクエリはO(log n)時間かかり、各MEX値について区間を処理する部分もO(n log n)時間で完了します。空間計算量はO(n log n)です。

このアルゴリズムを用いることで、数列のすべての極小MEX区間を効率的に列挙できます。これにより、数列の区間MEXに関する問題を高速に解決することが可能になります。

タグ: MEX セグメント木 永続データ構造 アルゴリズム 区間クエリ

7月15日 22:49 投稿