Mex 杂题选做

Mex 杂题选做

定义 \(\mathrm{mex}(S)\) 为最小的没有出现在 \(S\) 中的自然数。

P6852 Mex

给出 \(m\) 个条件,每个条件形如 \((l, r, k)\),表示区间 \([l,r]\)\({\rm mex}\)\(k\)

构造一个满足所有条件的 \(0 \sim n\) 的排列,或者告知无解。

\(n, m \leq 5 \times 10^5\)

考虑一个信息对排列的限制,最终小于 \(k\) 的数都必须出现在 \([l,r]\) 中, \(k\) 一定不能出现在 \([l,r]\) 中。

把所有数从小到大放到排列里面,设数 \(x\) 能放的位置集合是 \(S(x)\) ,即所有 \(\operatorname{mex} > x\) 的区间交去掉 \(\operatorname{mex} = x\) 的区间并。

可以发现 \(S(x)\) 要么是 \(S(x+1)\) 的子集(多出来的部分在两边),要么和 \(S(x+1)\) 不交,只要在能放的位置里随便找一个放即可。

实现时可以利用一些性质优化码量,若两个区间 \([l_1, r_1]\)\([l_2, r_2]\) 均满足 \(\mathrm{mex}\)\(k\)\(r_1 \leq l_2\) ,则由于排列的性质显然无解,因此可以直接将 \(\operatorname{mex} = x\) 的区间并转化为最小的左端点和最大的右端点组成的区间。

时间复杂度线性。

#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7;

struct Interval {
    int l, r;

    inline Interval operator | (const Interval &rhs) const {
        return (Interval) {min(l, rhs.l), max(r, rhs.r)};
    }

    inline Interval operator & (const Interval &rhs) const {
        return (Interval) {max(l, rhs.l), min(r, rhs.r)};
    }
} cup[N], cap[N];

int c[N], ans[N], sta[N];
bool vis[N];

int n, m, top;

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 0; i <= n; ++i)
        cup[i] = (Interval) {n, 0}, cap[i] = (Interval) {0, n};

    for (int i = 1; i <= m; ++i) {
        int l, r, k;
        scanf("%d%d%d", &l, &r, &k);
        cup[k] = cup[k] | (Interval) {l, r};

        if (k)
            cap[k - 1] = cap[k - 1] & (Interval) {l, r};
        else
            ++c[l], --c[r + 1];
    }

    for (int i = n - 1; ~i; --i) {
        cap[i] = cap[i] & cap[i + 1];

        if (cap[i].l > cap[i].r)
            return puts("-1"), 0;
    }

    for (int i = 1; i <= n; ++i)
        c[i] += c[i - 1];

    for (int i = cap[0].l; i <= cap[0].r; ++i)
        if (!c[i])
            vis[sta[++top] = i] = true;

    if (!top)
        return puts("-1"), 0;

    ans[sta[top--]] = 0;
    
    for (int i = cap[0].l; i <= cap[0].r; ++i)
        if (!vis[i])
            vis[sta[++top] = i] = true;

    for (int i = 1; i <= n; ++i) {
        for (int j = cap[i].l; j <= cap[i].r && j < cup[i].l && !vis[j]; ++j)
            vis[sta[++top] = j] = true;

        for (int j = cap[i].r; j >= cap[i].l && j > cup[i].r && !vis[j]; --j)
            vis[sta[++top] = j] = true;

        if (!top || (cup[i].l <= sta[top] && sta[top] <= cup[i].r))
            return puts("-1"), 0;

        ans[sta[top--]] = i;

        for (int j = max(cap[i].l, cup[i].l); j <= cap[i].r && !vis[j]; ++j)
            vis[sta[++top] = j] = true;

        for (int j = min(cap[i].r, cup[i].r); j >= cap[i].l && !vis[j]; --j)
            vis[sta[++top] = j] = true;
    }

    for (int i = 0; i <= n; ++i)
        printf("%d ", ans[i]);

    return 0;
}

CF1139E Maximize Mex

\(n\) 个学生和 \(m\) 个社团,每个学生有一个能力值,且所属一个社团。

一共有 \(d\) 天,每天会有一个人退团,然后需要从每个社团中各至多选一个人,最大化选出的人的能力值的 \(\text{mex}\)

\(d,m \leq n \leq 5000\)

不难发现最优方案一定是贪心先选能力值小的人,直到有一个能力值不能被表示。

将题目抽象成一个二分图问题:将能力值当成左部点,社团当成右部点,将社团里每个人的能力值和该社团连边,不断增加能力值找增广路。时间复杂度 \(O(nmd)\) ,无法通过。

注意到多一个人的情况肯定不比少一个人的情况差,因此可以从最后一天倒序考虑,从人员退出转化成人员加入,这样以后枚举能力值的时候就不需要每次从 \(0\) 开始枚举了,时间复杂度降为 \(O(dm)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int p[N], c[N], k[N], ans[N];
int match[N], vis[N];
bool used[N];

int n, m, d, Answer, Tag;

bool Hungary(int u, const int tag) {
    for (int v : G.e[u]) {
        if (vis[v] == tag)
            continue;
        
        vis[v] = tag;
        
        if (match[v] == -1 || Hungary(match[v], tag))
            return match[v] = u, true;
    }
    
    return false;
}

signed main() {
    scanf("%d%d", &n, &m);
    
    for (int i = 1; i <= n; ++i)
        scanf("%d", p + i);
    
    for (int i = 1; i <= n; ++i)
        scanf("%d", c + i);
    
    scanf("%d", &d);
    
    for (int i = 1; i <= d; ++i)
        scanf("%d", k + i), used[k[i]] = true;
    
    for (int i = 1; i <= n; ++i)
        if (!used[i])
            G.insert(p[i], c[i]);

    memset(match + 1, -1, sizeof(int) * m);
    
    for (int i = d; i; --i) {
        while (Hungary(Answer, ++Tag))
            ++Answer;
        
        ans[i] = Answer, G.insert(p[k[i]], c[k[i]]);
    }
    
    for (int i = 1; i <= d; ++i)
        printf("%d\n", ans[i]);
    
    return 0;
}

P5631 最小mex生成树

给定一张无向连通图,求边权集合的 \(\mathrm{mex}\) 最小的 MST。

\(n \le 10^6\)\(m \leq 2\times 10^6\)\(w \leq 10^5\)

考虑如何判断一个答案的合法性,若枚举的数为 \(x\) ,需要判断答案是否不大于 \(x\) ,那么我们只要不加入边权为 \(x\) 的边即可。

分治配合可撤销并查集即可做到 \(O(m \log n \log V)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7, V = 1e5 + 7;

struct DSU {
    int fa[N], siz[N], sta[N];

    int top;
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1), fill(siz + 1, siz + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        x = find(x), y = find(y);

        if (x == y)
            return;

        if (siz[x] < siz[y])
            swap(x, y);

        siz[fa[y] = x] += siz[y], sta[++top] = y;
    }

    inline void restore(int k) {
        while (top > k) {
            int x = sta[top--];
            siz[fa[x]] -= siz[x], fa[x] = x;
        }
    }
} dsu;

vector<pair<int, int> > e[V];

int n, m;

int solve(int l, int r) {
    if (l == r)
        return dsu.siz[dsu.find(1)] == n ? l : -1;

    int mid = (l + r) >> 1, ori = dsu.top;

    for (int i = mid + 1; i <= r; ++i)
        for (auto it : e[i])
            dsu.merge(it.first, it.second);

    int res = solve(l, mid);
    dsu.restore(ori);

    if (~res)
        return res;

    for (int i = l; i <= mid; ++i)
        for (auto it : e[i])
            dsu.merge(it.first, it.second);

    return solve(mid + 1, r);
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        e[w].emplace_back(u, v);
    }

    dsu.prework(n);
    printf("%d", solve(0, V - 1));
    return 0;
}

P4137 Rmq Problem / mex

给定 \(a_{1\ sim n}\)\(m\) 次询问区间 \(\mathrm{mex}\)

\(n, m \leq 2 \times 10^5\)

用主席树存前缀每个数最后出现的位置,查询时答案即为第 \(r\) 棵树上最后出现的位置 \(< l\) 的最小数,不难线段树二分做到 \(O((n + m) \log V)\) ,离散化后可以做到 \(O((n + m) \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;

int a[N];

int n, m;

namespace SMT {
const int S = N << 5;

int lc[S], rc[S], mn[S];
int rt[N];

int tot;

int update(int x, int nl, int nr, int pos, int k) {
    int y = ++tot;
    lc[y] = lc[x], rc[y] = rc[x], mn[y] = mn[x];

    if (nl == nr)
        return mn[y] = k, y;

    int mid = (nl + nr) >> 1;

    if (pos <= mid)
        lc[y] = update(lc[x], nl, mid, pos, k);
    else
        rc[y] = update(rc[x], mid + 1, nr, pos, k);

    mn[y] = min(mn[lc[y]], mn[rc[y]]);
    return y;
}

int query(int x, int nl, int nr, int k) {
    if (!x || nl == nr)
        return nl;

    int mid = (nl + nr) >> 1;

    return mn[lc[x]] < k ? query(lc[x], nl, mid, k) : query(rc[x], mid + 1, nr, k);
}
} // namespace SMT

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i), SMT::rt[i] = SMT::update(SMT::rt[i - 1], 0, 2e5, a[i], i);

    while (m--) {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", SMT::query(SMT::rt[r], 0, 2e5, l));
    }

    return 0;
}

CF1870E Another MEX Problem

给出 \(a_{1 \sim n}\) ,选出一些不交子段,最大化每个子段的 \(\mathrm{mex}\) 的异或和。

\(n \leq 5000\)

由于异或并不好做最优性 DP,故考虑可行性 DP。设 \(f_{i, j}\) 表示 \([1, i]\) 中选取子段时答案是否可以是 \(j\) ,直接枚举所有区间是 \(O(n^3)\) 的。

对于区间 \([l,r]\),若不存在子区间的 \(\mathrm{mex}\)\([l, r]\)\(\mathrm{mex}\) 相等,则称 \([l,r]\) 为极小 \(\mathrm{mex}\) 区间。则极小 \(\mathrm{mex}\) 区间至多只有 \(3n\) 个。

证明:当 \(l = r\) 时,显然该区间为极小 \(\mathrm{mex}\) 区间,这样的区间有 \(n\) 个,下面讨论 \(l < r\) 的情况。

极小 \(\mathrm{mex}\) 区间 \([l,r]\) 显然满足 \(a_l\ne a_r\),否则删端点即可得到更小的 \(\mathrm{mex}\) 区间。

不妨设 \(a_l>a_r\),则 \([l + 1, r - 1]\)\(\mathrm{mex}\)\(a_r - 1\)

由于固定 \(l\)\(\mathrm{mex}\) 不降,因此至多有一个 \(r\) 满足条件。同理对于 \(a_l<a_r\) 的情况,每个 \(r\) 也至多有一个 \(l\) 满足条件。

因此最多只有 \(3n\) 个极小 \(\mathrm{mex}\) 区间。

求解极小 \(\mathrm{mex}\) 区间,一个暴力的做法是对每个 \(l\) 找到最小的 \(r\) 满足 \(a_l>a_r\)\(\mathrm{mex}(l, r) > a_l\) 。反方向也是一样。需要特殊处理一下 \(l = r\) 的情况,时间复杂度 \(O(n^2)\)

由于不用恰好划分,因此只要枚举极小 \(\mathrm{mex}\) 区间,时间复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 7;

vector<pair<int, int> > interval[N];

int a[N];
bool f[N][N << 1], exist[N];

int T, n;

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d", &n);

        for (int i = 1; i <= n; ++i)
            scanf("%d", a + i);

        for (int i = 1; i <= n; ++i)
            interval[i].clear();

        for (int i = 1; i <= n; ++i)
            if (!a[i])
                interval[i].emplace_back(i, 1);

        for (int i = 1; i <= n; ++i) {
            memset(exist, false, sizeof(bool) * (n + 1));
            int mex = 0;

            for (int j = i; j <= n; ++j) {
                exist[a[j]] = true;

                while (exist[mex])
                    ++mex;

                if (mex > a[i] && a[j] < a[i]) {
                    interval[j].emplace_back(i, mex);
                    break;
                }
            }
        }

        for (int i = 1; i <= n; ++i) {
            memset(exist, false, sizeof(bool) * (n + 1));
            int mex = 0;

            for (int j = i; j; --j) {
                exist[a[j]] = true;

                while (exist[mex])
                    ++mex;

                if (mex > a[i] && a[j] < a[i]) {
                    interval[i].emplace_back(j, mex);
                    break;
                }
            }
        }
        
        int limit = n * 2;
        memset(f[0], false, sizeof(bool) * limit);
        f[0][0] = true;

        for (int i = 1; i <= n; ++i) {
            memcpy(f[i], f[i - 1], sizeof(bool) * limit);

            for (auto it : interval[i])
                for (int j = 0; j <= limit; ++j)
                    if ((j ^ it.second) <= limit)
                        f[i][j] |= f[it.first - 1][j ^ it.second];
        }

        for (int i = limit; ~i; --i)
            if (f[n][i]) {
                printf("%d\n", i);
                break;
            }
    }

    return 0;
}

P9970 [THUPC 2024 初赛] 套娃

给定 \(a_{1 \sim n}\),对于每个 \(1\leq k\leq n\),求所有长度为 \(k\) 的子区间的 \(\mathrm{mex}\) 组成集合的 \(\mathrm{mex}\)

\(n \leq 10^5\)

下面介绍一个 \(O(n \log n)\) 求所有极小 \(\mathrm{mex}\) 区间的方法。

称满足 \(\mathrm{mex} = x\) 的极小区间 \([l,r]\)\(\mathrm{mex}_x\) 区间。考虑一个 \(\mathrm{mex}_x\) 区间,不妨设 \(a_l>a_r\),则 \(a_r\)\((l,r)\) 中没有出现。而删去 \(a_l\) 之后,\(\mathrm{mex}\) 变为 \(a_l\)

不妨设 \(\mathrm{mex}_{a_l}\) 区间 \([l + 1, r]\) 对应的 \(\mathrm{mex}\) 极小子区间为 \([L,R]\) ,则 \(a_r\) 一定在 \([L,R]\) 中出现,于是 \(R=r\) 。这说明 \(\mathrm{mex}_x\) 区间必定为一个 \(\mathrm{mex}_{t}\) 区间向一端扩展得到,其中 \(t < x\)

考虑对 \(x=0, 1, \cdots, n + 1\) 依次求出所有 \(\mathrm{mex}_x\) 极小区间,用 vector 维护 \(\mathrm{mex}_x\) 极小区间。

每次先扩展所有 \(\text{mex}_{x-1}\) 区间,设一个扩展完后 \(\mathrm{mex} = y\),则把新区间丢到 \(y\)vector 里。

此时所有 \(\mathrm{mex}_x\) 极小区间都在 \(x\)vector 里了,但注意扩展完的区间不一定极小,于是排除掉即可。

注意初始化极小 \(\mathrm{mex}_{0/1}\) 区间,使用单次 \(O(\log n)\) 的查询区间 \(\mathrm{mex}\) 方法,时间复杂度 \(O(n\log n)\)

求出所有极小区间后,考虑所有对应 \(\mathrm{mex}_x\) 的极小子区间 \([l,r]\) 的大区间的形态。

\(l\) 左侧第一个 \(x\) 的位置为 \(L-1\)\(r\) 右侧第一个 \(x\) 的位置为 \(R+1\)

则所有对应的大区间为:左端点在 \([L,l]\),右端点在 \([r,R]\) 的所有区间。于是 \(\forall len\in [r-l+1,R-L+1]\),存在长为 \(len\) 的区间 \(\mathrm{mex} = x\)

于是问题就转化成了:维护 \(n\) 个集合,区间插入一个数,最终对所有集合求 \(\mathrm{mex}\) 。把插入操作差分,用一个 set 动态维护 \(\mathrm{mex}\) 即可。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;

vector<pair<int, int> > interval[N], upd[N];
vector<int> place[N];

int a[N];

int n;

namespace SMT {
const int S = N << 5;

int lc[S], rc[S], mn[S];
int rt[N];

int tot;

inline int insert(int x, int nl, int nr, int pos, int k) {
    int y = ++tot;
    lc[y] = lc[x], rc[y] = rc[x], mn[y] = mn[x];
    
    if (nl == nr)
        return mn[y] = k, y;
    
    int mid = (nl + nr) >> 1;
    
    if (pos <= mid)
        lc[y] = insert(lc[x], nl, mid, pos, k);
    else
        rc[y] = insert(rc[x], mid + 1, nr, pos, k);
    
    mn[y] = min(mn[lc[y]], mn[rc[y]]);
    return y;
}

int query(int x, int nl, int nr, int k) {
    if (!x || nl == nr)
        return nl;
    
    int mid = (nl + nr) >> 1;
    return mn[lc[x]] < k ? query(lc[x], nl, mid, k) : query(rc[x], mid + 1, nr, k);
}
} // namespace SMT

signed main() {
    scanf("%d", &n);
    
    for (int i = 0; i <= n; ++i)
        place[i].emplace_back(0);
    
    for (int i = 1; i <= n; ++i) {
        scanf("%d", a + i);
        place[a[i]].emplace_back(i);
        SMT::rt[i] = SMT::insert(SMT::rt[i - 1], 0, n, a[i], i);
    }
    
    for (int i = 0; i <= n; ++i)
        place[i].emplace_back(n + 1);
    
    for (int i = 1; i <= n; ++i)
        interval[!a[i]].emplace_back(i, i);
    
    for (int i = 1; i <= n; ++i) {
        for (auto it : interval[i - 1]) {
            int l = it.first, r = it.second,
                L = *prev(lower_bound(place[i - 1].begin(), place[i - 1].end(), l)),
                R = *upper_bound(place[i - 1].begin(), place[i - 1].end(), r);
            
            if (L)
                interval[SMT::query(SMT::rt[r], 0, n, L)].emplace_back(L, r);
            
            if (R <= n)
                interval[SMT::query(SMT::rt[R], 0, n, l)].emplace_back(l, R);
        }
        
        sort(interval[i].begin(), interval[i].end(), [](const pair<int, int> &a, const pair<int, int> &b) { 
            return a.first == b.first ? a.second < b.second : a.first > b.first;
        });

        vector<pair<int, int> > vec;
        int lstr = n + 1;
        
        for (auto it : interval[i])
            if (it.second < lstr)
                vec.emplace_back(it), lstr = it.second;
        
        interval[i] = vec;
    }
    
    for (int i = 0; i <= n; ++i)
        for (auto it : interval[i]) {
            int l = it.first, r = it.second,
                L = *prev(lower_bound(place[i].begin(), place[i].end(), l)) + 1,
                R = *upper_bound(place[i].begin(), place[i].end(), r) - 1;
            upd[r - l + 1].emplace_back(i, 1), upd[R - L + 2].emplace_back(i, -1);
        }

    set<int> absent;
    vector<int> cnt(n + 1);
    
    for (int i = 0; i <= n; ++i)
        absent.emplace(i);

    auto add = [&](int x) {
        if (!cnt[x])
            absent.erase(x);
        
        ++cnt[x];
    };

    auto del = [&](int x) {
        --cnt[x];
        
        if (!cnt[x])
            absent.emplace(x);
    };
    
    for (int i = 1; i <= n; ++i) {
        for (auto it : upd[i])
            if (it.second == 1)
                add(it.first);
            else
                del(it.first);
        
        printf("%d ", *absent.begin());
    }
    
    return 0;
}

P10169 [DTCPC 2024] mex,min,max

给定序列 \(a_{1 \sim n}\) 和常数 \(k\),求有多少子区间 \([l,r]\) 满足 \(\mathrm{mex} + \min + k \geq \max\)

\(n \leq 5 \times 10^5\)

注意到区间 \(\min = 0\) 和区间 \(\mathrm{mex} = 0\) 恰有一者成立,记:

  • \(A\)\(\mathrm{mex}(l, r) + k \geq \max(l, r)\) 的区间数量。
  • \(B\)\(\min(l, r) + k \geq \max(l, r)\) 的区间数量。
  • \(C\)\(\max(l, r) \leq k\) 的区间数量。

简单容斥可得 \(ans = A + B - C\)

对于 \(A\) ,枚举所有极小区间,对于一个 \(\operatorname{mex} = p\) 的极小区间 \([l, r]\) ,设其极大区间为 \([L, R]\) ,这样 \(\mathrm{mex} + k\) 就固定了。二分出极大的区间 \([l', r']\) 使得 \(\mathrm{mex}(l, r) + k \leq \max(l', r')\) 。那么 \(\forall pl \in [l', l], pr \in [r, r']\)\([pl, pr]\) 是合法区间。

发现这样求合法区间不漏但是会重,用扫描线维护面积并即可做到 \(O(n \log n)\)

对于 \(B\) ,固定左端点后合法右端点区间是左端点开始的一段,在 ST 表上倍增可以做到 \(O(n \log n)\)

对于 \(C\) ,向右移动右端点时合法左端点区间单调,直接用指针维护可行区间即可做到 \(O(n)\)

总时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e5 + 7, LOGN = 19;

struct Line {
    int l, r, op;
};

vector<pair<int, int> > interval[N];
vector<Line> upd[N];
vector<int> place[N];

int a[N];

int n, k;

namespace SMT {
const int SIZE = N << 5;

int lc[SIZE], rc[SIZE], mn[SIZE];
int rt[N];

int tot;

inline int insert(int x, int nl, int nr, int pos, int k) {
    int y = ++tot;
    lc[y] = lc[x], rc[y] = rc[x], mn[y] = mn[x];
    
    if (nl == nr) {
        mn[y] = k;
        return y;
    }
    
    int mid = (nl + nr) >> 1;
    
    if (pos <= mid)
        lc[y] = insert(lc[x], nl, mid, pos, k);
    else
        rc[y] = insert(rc[x], mid + 1, nr, pos, k);
    
    mn[y] = min(mn[lc[y]], mn[rc[y]]);
    return y;
}

inline int query(int x, int nl, int nr, int k) {
    if (!x || nl == nr)
        return nl;
    
    int mid = (nl + nr) >> 1;
    return mn[lc[x]] < k ? query(lc[x], nl, mid, k) : query(rc[x], mid + 1, nr, k);
}
} // namespace SMT

namespace ST {
int mn[N][LOGN], mx[N][LOGN];
int LOG[N];

inline void prework() {
    LOG[0] = -1;
    
    for (int i = 1; i <= n; ++i)
        LOG[i] = LOG[i >> 1] + 1;
    
    for (int i = 1; i <= n; ++i)
        mn[i][0] = mx[i][0] = a[i];
    
    for (int j = 1; j <= LOG[n]; ++j)
        for (int i = 1; i + (1 << j) - 1 <= n; ++i) {
            mn[i][j] = min(mn[i][j - 1], mn[i + (1 << (j - 1))][j - 1]);
            mx[i][j] = max(mx[i][j - 1], mx[i + (1 << (j - 1))][j - 1]);
        }
}

inline int querymin(int l, int r) {
    int k = LOG[r - l + 1];
    return min(mn[l][k], mn[r - (1 << k) + 1][k]);
}

inline int querymax(int l, int r) {
    int k = LOG[r - l + 1];
    return max(mx[l][k], mx[r - (1 << k) + 1][k]);
}
} // namespace ST

namespace SGT {
int s[N << 3], len[N << 3];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

inline void pushup(int x, int l, int r) {
    len[x] = (s[x] ? r - l + 1 : len[ls(x)] + len[rs(x)]);
}

void update(int x, int nl, int nr, int l, int r, int k) {
    if (l <= nl && nr <= r) {
        s[x] += k, pushup(x, nl, nr);
        return;
    }
    
    int mid = (nl + nr) >> 1;
    
    if (l <= mid)
        update(ls(x), nl, mid, l, r, k);
    
    if (r > mid)
        update(rs(x), mid + 1, nr, l, r, k);
    
    pushup(x, nl, nr);
}
} // namespace SGT

inline ll solve1() {
    for (int i = 1; i <= n; ++i)
        interval[!a[i]].emplace_back(i, i);
    
    for (int i = 1; i <= n; ++i) {
        for (auto it : interval[i - 1]) {
            int l = it.first, r = it.second,
                L = *prev(lower_bound(place[i - 1].begin(), place[i - 1].end(), l)),
                R = *upper_bound(place[i - 1].begin(), place[i - 1].end(), r);
            
            if (L)
                interval[SMT::query(SMT::rt[r], 0, n, L)].emplace_back(L, r);
            
            if (R <= n)
                interval[SMT::query(SMT::rt[R], 0, n, l)].emplace_back(l, R);
        }
        
        sort(interval[i].begin(), interval[i].end(), [](const pair<int, int> &a, const pair<int, int> &b) { 
            return a.first == b.first ? a.second < b.second : a.first > b.first;
        });

        vector<pair<int, int> > vec;
        int lstr = n + 1;
        
        for (auto it : interval[i])
            if (it.second < lstr)
                vec.emplace_back(it), lstr = it.second;
        
        interval[i] = vec;
    }
    
    for (int i = 0; i <= n; ++i)
        for (auto it : interval[i]) {
            int l = it.first, r = it.second, lim = i + k;
            
            if (ST::querymax(l, r) > lim)
                continue;
            
            int L = *prev(lower_bound(place[i].begin(), place[i].end(), l)) + 1,
                R = *upper_bound(place[i].begin(), place[i].end(), r) - 1;
            
            auto BinarySearchL = [](int L, int R, int lim) {
                int l = L, r = R, ans = R;
                
                while (l <= r) {
                    int mid = (l + r) >> 1;
                    
                    if (ST::querymax(mid, R) <= lim)
                        r = mid - 1, ans = mid;
                    else
                        l = mid + 1;
                }
                
                return ans;
            };

            auto BinarySearchR = [](int L, int R, int lim) {
                int l = L, r = R, ans = L;
                
                while (l <= r) {
                    int mid = (l + r) >> 1;
                    
                    if (ST::querymax(L, mid) <= lim)
                        l = mid + 1, ans = mid;
                    else
                        r = mid - 1;
                }
                
                return ans;
            };
            
            int nl = BinarySearchL(L, l, lim), nr = BinarySearchR(r, R, lim);
            upd[nl].emplace_back((Line) {r, nr, 1}), upd[l + 1].emplace_back((Line) {r, nr, -1});
        }
    
    ll ans = 0;
    
    for (int i = 1; i <= n; ++i) {
        for (Line it : upd[i])
            SGT::update(1, 1, n, it.l, it.r, it.op);
        
        ans += SGT::len[1];
    }
    
    return ans;
}

inline ll solve2() {
    ll ans = 0;

    for (int i = 1; i <= n; ++i) {
        int r = i;

        for (int j = __lg(n - i + 1); ~j; --j)
            if (r + (1 << j) <= n && ST::querymax(i, r + (1 << j)) - ST::querymin(i, r + (1 << j)) <= k)
                r += 1 << j;

        ans += r - i + 1;
    }
    
    return ans;
}

inline ll solve3() {
    ll ans = 0;
    
    for (int l = 0, r = 1; r <= n; ++r) {
        if (a[r] > k)
            l = r;
        
        ans += r - l;
    }
    
    return ans;
}

signed main() {
    scanf("%d%d", &n, &k);
    
    for (int i = 0; i <= n; ++i)
        place[i].emplace_back(0);
    
    for (int i = 1; i <= n; ++i) {
        scanf("%d", a + i);
        place[a[i]].emplace_back(i);
        SMT::rt[i] = SMT::insert(SMT::rt[i - 1], 0, n, a[i], i);
    }
    
    for (int i = 0; i <= n; ++i)
        place[i].emplace_back(n + 1);
    
    ST::prework();
    printf("%lld", solve1() + solve2() - solve3());
    return 0;
}

CF817F MEX Queries

维护一个初始为空的集合,\(m\) 此操作,操作有三种:

  • \([l, r]\) 中在集合中没有出现过的数添加到集合中。
  • \([l, r]\) 中在集合中出现过的数从集合中删掉。
  • \([l, r]\) 中在集合中没有出现过的数添加到集合中,并把 \([l,r]\) 中在集合中出现过的数从集合中删掉。

每次操作后求集合 \(\mathrm{mex}\)

\(m \leq 10^5\)\(1 \leq l \leq r \leq 10^{18}\)

\(l_i, r_i, r_{i + 1}\) 离散化,用线段树维护区间或、与、异或操作,求 \(\mathrm{mex}\) 直接线段树二分即可,时间复杂度 \(O(m \log m)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 3e5 + 7;

struct Update {
    int op;
    ll l, r;
} upd[N];

int m;

namespace SMT {
int s[N << 2], len[N << 2], tag[N << 2], rev[N << 1];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

inline void pushup(int x) {
    s[x] = s[ls(x)] + s[rs(x)];
}

inline void spread(int x, int k) {
    if (k == 1)
        s[x] = len[x], tag[x] = 1;
    else if (k == 2)
        s[x] = 0, tag[x] = 2;
    else
        s[x] = len[x] - s[x], tag[x] = 3 - tag[x];
}

inline void pushdown(int x) {
    if (tag[x])
        spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 0;
}

void build(int x, int l, int r) {
    len[x] = r - l + 1;

    if (l == r)
        return;

    int mid = (l + r) >> 1;
    build(ls(x), l, mid), build(rs(x), mid + 1, r);
}

void update(int x, int nl, int nr, int l, int r, int k) {
    if (l <= nl && nr <= r) {
        spread(x, k);
        return;
    }

    pushdown(x);
    int mid = (nl + nr) >> 1;

    if (l <= mid)
        update(ls(x), nl, mid, l, r, k);

    if (r > mid)
        update(rs(x), mid + 1, nr, l, r, k);

    pushup(x);
}

int query(int x, int l, int r) {
    if (l == r)
        return l;

    pushdown(x);
    int mid = (l + r) >> 1;
    return s[ls(x)] < mid - l + 1 ? query(ls(x), l, mid) : query(rs(x), mid + 1, r);
}
} // namespace SMT

signed main() {
    scanf("%d", &m);
    vector<ll> vec = {1};

    for (int i = 1; i <= m; ++i) {
        scanf("%d%lld%lld", &upd[i].op, &upd[i].l, &upd[i].r);
        vec.emplace_back(upd[i].l), vec.emplace_back(upd[i].r), vec.emplace_back(upd[i].r + 1);
    }

    sort(vec.begin(), vec.end());
    vec.erase(unique(vec.begin(), vec.end()), vec.end());
    SMT::build(1, 0, vec.size() - 1);

    for (int i = 1; i <= m; ++i) {
        int l = lower_bound(vec.begin(), vec.end(), upd[i].l) - vec.begin(),
            r = lower_bound(vec.begin(), vec.end(), upd[i].r) - vec.begin();
        SMT::update(1, 0, vec.size() - 1, l, r, upd[i].op);
        printf("%lld\n", vec[SMT::query(1, 0, vec.size() - 1)]);
    }

    return 0;
}

CF1148H Holy Diver

给定一个最初为空的数组,\(n\) 次操作,每次给出 \(a,l,r,k\) ,在数组末尾插入 \(a\) ,然后查询 \([l, r]\) 有多少子区间 \(\mathrm{mex} = k\)

\(n \leq 2 \times 10^5\) ,强制在线

考虑扫描线扫右端点 \(R\),维护一些极长的段 \([l, r]\) 表示 \([l \sim r, R]\)\(\mathrm{mex}\) 都是同一个值。

加入一个数 \(x = a_R\),就找到 \(\mathrm{mex}\)\(x\) 的左端点区间 \([l, r]\) ,现在以这一段的点为左端点,以 \(R\) 右端点的区间 \(\text{mex}\) 不再是 \(x\) ,那么考虑分裂这个区间,即不断找到最小的数 \(y\) 使得 \(p_y < r\)\(p_y\)\(y\) 最后一次出现的位置),那么 \([p_y + 1, r]\) 这段的 \(\mathrm{mex}\) 就是 \(y\) 。暴力更新直到整段都被分裂完毕,然后再加入 \(R\) 即可。因为一个位置最多被分裂一次,所以分裂的总段数是 \(O(n)\) 的。

下面考虑查询,根据极小 \(\mathrm{mex}\) 区间理论,一个位置 \(a_x\) 的贡献是左端点在 \([l_1, r_1]\),右端点在 \([l_2, r_2]\) 的区间。考虑单独对每个 \(\mathrm{mex}\) 开一个主席树维护左端点,每个位置的贡献都是关于 \(R\) 的一个一次函数。每次修改时继承上一个版本后再修改,查询时只要找到 \(\leq r\) 的版本的树上 \(\geq l\) 的贡献和即可。

为了快速查询 \(\leq r\) 的版本的树,可以对每个值开一个 map 维护各个版本的根,由上文修改次数是 \(O(n)\) 级别的。

为了方便可以写标记永久化,总时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7;

map<int, int> rt[N];
pair<int, int> interval[N];

int p[N];

int n;

namespace SGT {
int mn[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

inline void pushup(int x) {
    mn[x] = min(mn[ls(x)], mn[rs(x)]);
}

void update(int x, int nl, int nr, int pos, int k) {
    if (nl == nr) {
        mn[x] = k;
        return;
    }

    int mid = (nl + nr) >> 1;

    if (pos <= mid)
        update(ls(x), nl, mid, pos, k);
    else
        update(rs(x), mid + 1, nr, pos, k);

    pushup(x);
}

int query(int x, int nl, int nr, int k) {
    if (nl == nr)
        return nl;

    int mid = (nl + nr) >> 1;
    return mn[ls(x)] < k ? query(ls(x), nl, mid, k) : query(rs(x), mid + 1, nr, k);
}
} // namespace SGT

namespace SMT {
const int S = N << 6;

struct Line {
    ll k, b;

    inline Line operator + (const Line &rhs) const {
        return (Line){k + rhs.k, b + rhs.b};
    }

    inline Line operator * (const ll &x) const {
        return (Line){k * x, b * x};
    }

    inline ll operator () (const ll &x) const {
        return k * x + b;
    }
} s[S], tag[S];

int lc[S], rc[S];

int tot;

int build(int l, int r) {
    int x = ++tot;

    if (l == r)
        return x;

    int mid = (l + r) >> 1;
    lc[x] = build(l, mid), rc[x] = build(mid + 1, r);
    return x;
}

int update(int x, int nl, int nr, int l, int r, Line k) {
    int y = ++tot;
    lc[y] = lc[x], rc[y] = rc[x], s[y] = s[x], tag[y] = tag[x];

    if (l <= nl && nr <= r) {
        tag[y] = tag[y] + k;
        return y;
    }

    s[y] = s[y] + k * (min(r, nr) - max(l, nl) + 1);
    int mid = (nl + nr) >> 1;

    if (l <= mid)
        lc[y] = update(lc[x], nl, mid, l, r, k);

    if (r > mid)
        rc[y] = update(rc[x], mid + 1, nr, l, r, k);

    return y;
}

ll query(int x, int nl, int nr, int l, int r, ll k) {
    if (l <= nl && nr <= r)
        return (s[x] + tag[x] * (nr - nl + 1))(k);

    int mid = (nl + nr) >> 1;
    ll res = tag[x](k) * (min(r, nr) - max(l, nl) + 1);

    if (l <= mid)
        res += query(lc[x], nl, mid, l, r, k);

    if (r > mid)
        res += query(rc[x], mid + 1, nr, l, r, k);

    return res;
}
} // namespace SMT

inline void update(int x, int l, int r, int t) {
    int root = rt[x].rbegin()->second;
    rt[x][t] = SMT::update(root, 1, n, l, r, {1, 1 - t});

    if (interval[x] == make_pair(0, 0))
        interval[x] = make_pair(l, r);
    else
        interval[x] = make_pair(min(interval[x].first, l), max(interval[x].second, r));
}

signed main() {
    scanf("%d", &n);

    for (int i = 0, r = SMT::build(1, n); i <= n; ++i)
        rt[i][0] = r;

    ll lstans = 0;

    for (int i = 1; i <= n; ++i) {
        int x, l, r, k;
        scanf("%d%d%d%d", &x, &l, &r, &k);
        x = (x + lstans) % (n + 1), l = (l + lstans) % i + 1, r = (r + lstans) % i + 1, k = (k + lstans) % (n + 1);

        if (l > r)
            swap(l, r);

        SGT::update(1, 0, n, x, p[x] = i);

        if (interval[x].first) {
            int r = interval[x].second;

            while (r >= interval[x].first) {
                int y = SGT::query(1, 0, n, r);
                update(y, max(interval[x].first, p[y] + 1), r, i), r = p[y];
            }

            int root = rt[x].rbegin()->second;
            rt[x][i] = SMT::update(root, 1, n, interval[x].first, interval[x].second, {-1, i - 1});
            interval[x] = make_pair(0, 0);
        }

        update(!x, i, i, i);
        printf("%lld\n", lstans = SMT::query(prev(rt[k].upper_bound(r))->second, 1, n, l, r, r));
    }

    return 0;
}

[ARC170C] Prefix Mex Sequence

给出一个长度为 \(n\) 的 01 序列 \(s_{1 \sim n}\) ,求满足 \(a_i \in [0, m]\)\([[a_i \neq \mathrm{mex}_{j = 1}^{i - 1} a_j] \neq s_i]\) 的序列 \(a_{1 \sim n}\) 的数量 \(\bmod 998244353\)

\(n \leq 5000\)\(m \leq 10^9\)

先考虑 \(m \geq n\) 的情况,由于 \(\mathrm{mex} \leq n \leq m\) ,因此对于 \(s_i = 0\) 的位置有 \(m\) 种方案,\(s_i = 1\) 的位置有一种方案,直接计算即可。

否则考虑将问题转化为在一排 \(0 \sim m\)\(m + 1\) 个格子中填数,\(s_i = 0\) 相当于不能填最靠左的空格子(填过的还可以填),\(s_i = 1\) 相当于只能填最靠左的空格子。

\(f_{i, j}\) 表示前 \(i\) 次填数操作填掉了 \(j\) 个格子的方案数,则:

\[f_{i, j} = \begin{cases} f_{i - 1, j - 1} & s_i = 1 \\ j \times f_{i - 1, j} + (m - j + 1) \times f_{i - 1, j - 1} & s_i = 0 \end{cases} \]

时间复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 5e3 + 7;

int a[N], f[N][N];

int n, m;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    if (m >= n)
        return printf("%d", mi(m, count(a + 1, a + n + 1, 0))), 0;

    ++m, f[0][0] = 1;

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            f[i][j] = (a[i] ? f[i - 1][j - 1] : 
                add(1ll * j * f[i - 1][j] % Mod, 1ll * (m - j) * f[i - 1][j - 1] % Mod));

    int ans = 0;

    for (int i = 1; i <= m; ++i)
        ans = add(ans, f[n][i]);

    printf("%d", ans);
    return 0;
}

CF1930H Interactive Mex Tree

这是一道交互题。

给定一棵 \(n\) 个点的树,需要给出两个 \(1 \sim n\) 的排列 \(p_1, p_2\)

接下来 \(q\) 次询问,每次询问时会交互库会生成一个 \(0 \sim n - 1\) 的排列 \(a\) 作为点权(不会给出),然后询问 \(x, y\) 路径上的 \(\mathrm{mex}\)

可以查询至多 \(5\) 次,每次查询需要给出 \(x, l, r\) ,交互库会返回 \(\min_{i = l}^r a_{p_{x, i}}\)

\(n \leq 10^5\)\(q \leq 10^4\)\(nq \leq 3 \times 10^6\)

由于点权是排列,因此查询路径 \(\mathrm{mex}\) 等价于查询路径外的点权 \(\min\) 。由于 \(\min\) 是可以重复贡献的信息,因此无需保证查询点集不交。

先考虑 \(x\)\(y\) 祖先的情况。令第一个排列为 dfs 序,则可以查询 \([1, p_{1, x} - 1]\)\([p_{1, y} + 1, n]\) 求出一部分 \(x, y\) 路径外的信息,但是发现这样无法查询到 \(x, y\) 路径上挂到的子树的信息。

考虑令第二个排列为 dfs 出栈序,则前面没有查询到的点均在 \(y\) 之前出栈,且 \(x, y\) 路径上的点均在 \(y\) 之后出栈,因此只要再查询 \([1, p_{2, y} - 1]\) 即可。

接下来考虑一般情况,记 \(z = \mathrm{LCA}(x, y)\)\(w\) 表示 \(z\)\(y\) 方向上的儿子,考虑如下五次询问:

  • \((1, 1, p_{1, z} - 1)\)
  • \((2, 1, p_{2, x} - 1)\) :这可以覆盖 \(z \to fa_x\) 链上先于 \(x\) 遍历的子树。
  • \((1, p_{1, x} + 1, p_{1, w} - 1)\) :这可以覆盖 \(x\) 子树以及 \(x, w\) 之间遍历的子树。
  • \((2, p_{2, pre} + 1, p_{2, y} - 1)\) :其中 \(pre\)\(z\)\(w\) 的前一个儿子,这可以去掉 \(pre, y\) 之间先于 \(y\) 遍历的子树。
  • \((1, p_{1, y} + 1, n)\)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;

struct Graph {
    vector<int> e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int fa[N], p1[N], p2[N], id1[N], id2[N];

int n, q, cnt1, cnt2;

inline int query(int x, int l, int r) {
    if (l > r)
        return n;

    printf("? %d %d %d\n", x, l, r), fflush(stdout);
    int res;
    scanf("%d", &res);
    return res;
}

void dfs(int u, int f) {
    fa[u] = f, id1[p1[u] = ++cnt1] = u;

    if (f)
        G.e[u].erase(find(G.e[u].begin(), G.e[u].end(), f));

    for (int v : G.e[u])
        dfs(v, u);

    id2[p2[u] = ++cnt2] = u;
}

inline int LCA(int x, int y) {
    while (x != y) {
        if (p1[x] > p1[y])
            x = fa[x];
        else
            y = fa[y];
    }

    return x;
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d", &n, &q);
        G.clear(n);

        for (int i = 1; i < n; ++i) {
            int u, v;
            scanf("%d%d", &u, &v);
            G.insert(u, v), G.insert(v, u);
        }

        cnt1 = cnt2 = 0, dfs(1, 0);

        for (int i = 1; i <= n; ++i)
            printf("%d ", id1[i]);

        puts("");

        for (int i = 1; i <= n; ++i)
            printf("%d ", id2[i]);

        puts(""), fflush(stdout);

        while (q--) {
            int x, y;
            scanf("%d%d", &x, &y);
            int z = LCA(x, y);

            if (p1[x] > p1[y])
                swap(x, y);

            if (x == z) {
                int ans = min(min(query(1, 1, p1[x] - 1), query(1, p1[y] + 1, n)), query(2, 1, p2[y] - 1));
                printf("! %d\n", ans), fflush(stdout);
            } else {
                int i = 0;

                while (i + 1 < G.e[z].size() && p1[G.e[z][i + 1]] <= p1[y])
                    ++i;

                int ans = min(min(min(min(query(1, 1, p1[z] - 1), query(2, 1, p2[x] - 1)), query(1, p1[y] + 1, n)),
                    query(2, p2[G.e[z][i - 1]] + 1, p2[y] - 1)), query(1, p1[x] + 1, p1[G.e[z][i]] - 1));
                printf("! %d\n", ans), fflush(stdout);
            }

            scanf("%*d");
        }
    }

    return 0;
}

CF1375D Replace by MEX

给定序列 \(a\) ,每次可以选择一个位置将其变为全局 \(\mathrm{mex}\) ,试用 \(\leq 2n\) 次操作将序列变得不降。

\(n \leq 1000\)\(a_i \in [0, n]\)

考虑将序列变为 \(0, 1, \cdots, n - 1\)

  • 若当前 \(\mathrm{mex} < n\) ,则令 \(a_{\mathrm{mex} + 1} \gets \mathrm{mex}\) 即可。
  • 否则找到任意一个 \(a_i \neq i - 1\) 的位置,令 \(a_i \gets n\) ,然后重复上述操作即可。

由于一次操作二后必执行两次操作一,而一次操作一会归位一个位置,因此操作次数 \(\leq 2n\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;

int a[N];
bool vis[N];

int n;

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d", &n);

        for (int i = 1; i <= n; ++i)
            scanf("%d", a + i);

        vector<int> ans;

        while (!is_sorted(a + 1, a + n + 1)) {
            memset(vis, false, sizeof(bool) * (n + 1));

            for (int i = 1; i <= n; ++i)
                vis[a[i]] = true;

            int mex = 0;

            while (vis[mex])
                ++mex;

            if (mex < n)
                ans.emplace_back(mex + 1), a[mex + 1] = mex;
            else {
                for (int i = 1; i <= n; ++i)
                    if (a[i] != i - 1) {
                        ans.emplace_back(i), a[i] = mex;
                        break;
                    }
            }
        }

        printf("%d\n", (int)ans.size());

        for (int it : ans)
            printf("%d ", it);

        puts("");
    }
    
    return 0;
}
posted @ 2025-01-13 22:50  wshcl  阅读(122)  评论(0)    收藏  举报