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\) 个格子的方案数,则:
时间复杂度 \(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;
}