复习:mex 小学习
\(\text{mex}(S)\) 中被定义为 \(S\) 没有出现过的最小自然数。
\(\text{mex}(S)=x\) 意味着 \([0,x-1]\subseteq S\),同时 \(x \notin S\)。
总有 \(|S|\ge \text{mex}(S)\)。
一般而言有两种考虑角度,一种是从 \(0\) 开始的极长连续段长度,另一种是满足 \(x\notin S\) 的 \(x\) 最小值。
mex 有时候会带来一些单调性,例如在“最小 mex 生成树中”,从大到小枚举第一个“删去所有边权为 \(x\) 的边后图仍然连通”的 \(x\),即为答案。因为若此时 \([0,x-1] \not\subseteq S\),答案必 \(<x\)。这个过程可以用线段树分治维护。
所以“最小化 mex” 一类题目中常见的方法是从小往大尝试贪心地钦定一个数不存在,而“最大化”则是钦定一个前缀存在。
还有另外一个性质是 \(\text{mex}(S\setminus x)=\min(\text{mex}(S),x)\),即从 \(S\) 中删去 \(x\) 后相当于 mex 对 \(x\) 取 \(\min\)。
从这可以看出对 mex 的维护来说,删除远比加入简单,因此很多问题都可以优先从删除的角度考虑。
更常见的情况是在序列上询问区间 \(\text{mex}\) 相关问题。
对于区间求 mex,常见的方法是:
- 用值域线段树维护每个颜色上一次出现的位置。
- 在 \(r\) 处按上一次出现位置是否 \(\ge l\),在线段树上二分。
- 可以在线地用持久化线段树或者离线下来扫描线维护。
mex 在序列上还有很多其他性质,例如,固定左/右端点,mex 不降。
另一种常见的操作是:对 \(r\) 扫描线,同时维护所有 \(l\) 对应的 mex。然而如上文所说,维护加数是复杂的。
更好的方法是令 \(r\) 从右往左扫描,此时只需要删数:
- 欲删去 \(r\),记 \(a_r\) 上一次出现的位置是 \(p\),区间 \([p+1,r]\) 对 \(p\) 取 \(\min\) 即可。
- 利用上一个单调的性质,可以通过结合线段树二分做到 \(\log n\) 每次移动。
- 由于取 \(\min\) 等价于找到边界后区间覆盖,也可以用 ODT 直接维护。
mex 在区间上更深刻的性质是“极小 mex 区间”,区间 \(I\) 极小即不存在 \(I'\subset I\) 满足 \(\text{mex}(I')=\text{mex}(I)\)。
一个性质是“极小 mex 区间”只有 \(2n\) 个,证明考虑:
- 对 \([l,r]\),不妨先令 \(a_l \ge a_r\)。
- \(\text{mex}(l+1,r)=\min(\text{mex}(l,r), a_l)\),若 \(\min\) 取前者,删去 \(l\) 不劣。
- 否则 \(\text{mex}(l+1,r)=a_l\),由 \(\text{mex}\) 全相等,只有最小的 \(r\) 使得区间极小。每个 \(l\) 至多贡献 \(1\) 个 \([l,r]\)。
- \(a_l<a_r\) 情况同理,总共至多 \(2n\) 个。
欲处理出所有的“极小 mex 区间”,考虑对上一段 线段树/ODT 的算法改进:
- 线段树需要对每种不同值额外递归到叶子找端点。具体地,可以传入引用变量 \(H\) 表示当前右侧的最大值,每次符合条件后优先递归右侧,找到端点后更新 \(H\),然后跳过所有同值段。复杂度均摊 \(\log n\)。
- 用 ODT 可能更简单,还可以额外给出每种 mex 等价类的 \(I=[l,r]\) 满足的 \(l\in [l_1,l_2], r\in [r_1,r_2]\) 信息。
upd on 26.11.04:放一个今天写的板子(
struct node { mutable int p, v; }; // 右端点位于 p,等价类 mex 为 v
inline bool operator < (const node& x, const node& y) { return x.p < y.p; }
std::set<node> set;
void split(int x) {
// split x-1], [x
if(x == 1) return ;
auto p = set.lower_bound({x, 0}), q = p--;
if(q != set.begin() && p->p == x-1) return ;
set.insert({x-1, q->v});
}
struct Range { int x, y, e; };
std::vector<Range> rgex;
// ckmin [x,y) to v, remove y
void domin(int x, int y, int v) {
split(x), split(y);
// remove y
auto q = --set.end();
if(q->v) rgex.push_back({y, y, q->v});
set.erase(q);
// ckmin
auto p = set.lower_bound({x, 0});
int pos = 0;
while(p != set.end() && p->v >= v) {
auto q = p++; pos = q->p;
if(q->v) rgex.push_back({ pos, y, q->v });
set.erase(q);
}
if(pos) set.insert({pos, v});
}
void init() {
int mex = 0;
static int buc[N], tmp[N], pre[N], las[N];
for(int i = n; i >= 1; --i) {
++buc[v[i]];
while(buc[mex]) ++mex;
tmp[i] = mex;
}
for(int i = 1; i <= n; ++i) pre[i] = las[v[i]], las[v[i]] = i;
for(int i = 2; i <= n+1; ++i)
if(i == n+1 || tmp[i] != tmp[i-1])
set.insert({i-1, tmp[i-1]});
for(int i = n; i >= 1; --i) domin(pre[i]+1, i, v[i]);
}
本文来自博客园,作者:CuteNess,转载请注明原文链接:https://www.cnblogs.com/CuteNess/p/23442852

浙公网安备 33010602011771号