跳转至

MEX⚓︎

\text{MEX}\text{Minimum Excludant})指的是一个集合中没有出现的最小非负整数。换句话说,\text{MEX} 是从 0 开始、不属于给定的集合的最小整数。
\text{MEX}(S) = \min\{x \geq 0 \mid x \notin S\}

\text{MEX}与博弈论

在博弈论中,\text{MEX} 常用于计算 \text{Grundy} 数(格兰迪数),以判断当前局势是否为必胜态

\text{MEX} 的一些性质:

  1. 最大可能值: 对于一个包含 n 个非负整数的集合 S\text{MEX}(S) 的最大可能值是 n
  2. 最小可能值: 对于任何非空集合 S\text{MEX}(S) 的最小可能值是 0
  3. 单调性: 如果集合 A 是集合 B 的子集,那么 \text{MEX}(A) ≤ \text{MEX}(B)。这意味着向集合中添加一个元素可能会导致 \text{MEX} 值的增加,但不会减少
  4. 子集性质: 如果 S 是一个包含 n 个非负整数的集合,并且 \text{MEX}(S) = m,那么集合 \{0, 1, 2, \ldots, m-1\} 必须是 S 的子集
  5. 补集性质: 如果 S 是全集 U = \{0, 1, 2, \ldots, m\} 的子集,那么 \text{MEX}(S) 是补集 U \setminus S 的最小元素
MEX的计算

时间复杂度 O(n),空间复杂度 O(n)

C++
int mex(const std::vector<int> &nums) {
  int n = nums.size();
  // n个数的数组,mex最大可能是n
  std::vector<bool> present(n + 1, false);
  for (int num : nums) {  // 标记数组中出现过的数字
    if (num <= n) { present[num] = true; }
  }
  for (int i = 0; i <= n; ++i) {  // 找到第一个未出现的数字
    if (!present[i]) { return i; }
  }
  return n;  // 如果0到n-1都出现了,返回n
}

时间复杂度 O(n),空间复杂度 O(1)

C++
int mex(std::vector<int> &nums) {
  int n = nums.size();
  for (int i = 0; i < n; i++) {
    if (nums[i] <= 0) { nums[i] = n + 1; }  // 将负数和零替换为n+1
  }
  for (int i = 0; i < n; i++) {
    // 使用负号标记出现过的数字
    if (abs(nums[i]) <= n && nums[abs(nums[i]) - 1] > 0) { nums[abs(nums[i]) - 1] *= -1; }
  }
  for (int i = 0; i < n; i++) {
    if (nums[i] > 0) { return i + 1; }  // 第一个正数的索引+1即为mex
  }
  return n + 1;
}

双指针方法利用了数组的特性,将有效数字放在左边,垃圾数字放在右边。时间复杂度 O(n),空间复杂度 O(1)

C++
int mex(std::vector<int> &nums) {
  // left 左边的区域已经放好 1~left 的数字
  int left = 0;
  // [right....]垃圾区
  int right = nums.size();
  while (left < right) {
    // 如果当前位置正好是 left+1,扩展 mex 区域
    if (nums[left] == left + 1) {
      left++;
    } else if (nums[left] <= left || nums[left] > right || nums[nums[left] - 1] == nums[left]) {
      // 如果当前数字无效(<= left、> right、重复),丢到垃圾区
      --right;
      swap(nums[left], nums[right]);
    } else {
      // 把当前数字放到它应该在的位置
      swap(nums[left], nums[nums[left] - 1]);
    }
  }
  return left + 1;
}

动态查询\text{MEX}

动态更新查询区间\text{MEX}需要权值线段树维护

Rmq Problem / mex

有一个长度为 n 的数组 a_1,a_2,\dots,a_n。 有 m 个查询,每个查询给出两个整数 lr,要求你找出子区间 [l,r] 内的 \text{MEX}

Hint

维护每一个权值在原数组中最后一次出现的下标。对于查询操作 [l,r],如果某个权值的最后出现位置小于 l,则该权值不在区间 [l,r] 内出现。于是维护一个线段树,节点保存该区间内所有权值的最后出现位置的最小值。然后可以通过线段树二分查找小于 l 的最小权值,即为 \text{MEX}

由于 n 个数的 \text{MEX} 最大可能为 n,所以对于大于 n 的数,可以直接将其视为不存在。对于每个区间右端点 r,只需要将 a_r 插入到线段树中,并更新其最后出现位置为 r

C++
#include <algorithm>
#include <cstdint>
#include <iostream>
#include <vector>
using namespace std;

struct node {
  int64_t left;
  int64_t right;
  int64_t last_pos;
};

vector<node> nodes;  // 所有节点

struct p_segment_tree {
  p_segment_tree() {
    nodes.clear();               // 清空节点
    nodes.emplace_back(node{});  // 占位, 节点编号从1开始
  }

  // 更新当前区间元素的最小最后出现位置
  static void push_up(int64_t i) {
    int64_t left_min_pos  = nodes[nodes[i].left].last_pos;
    int64_t right_min_pos = nodes[nodes[i].right].last_pos;
    nodes[i].last_pos     = std::min(left_min_pos, right_min_pos);
  }

  static int64_t clone(int64_t i) {
    int64_t new_node = nodes.size();  // 新节点编号
    nodes.emplace_back(nodes[i]);     // 克隆当前节点
    return new_node;
  }

  // 修改节点的值, 返回新版本的根节点
  int64_t update(int64_t index, int64_t pos, int64_t i, int64_t l, int64_t r) {
    int64_t new_node = clone(i);  // 克隆当前节点
    if (l == r) {
      nodes[new_node].last_pos = pos;  // 更新最后出现位置
      return new_node;
    }
    int64_t mid = l + ((r - l) / 2);
    if (index <= mid) {
      nodes[new_node].left = update(index, pos, nodes[i].left, l, mid);
    } else {
      nodes[new_node].right = update(index, pos, nodes[i].right, mid + 1, r);
    }
    push_up(new_node);
    return new_node;
  }

  // root_u为版本u的根节点
  int64_t query(int64_t pos, int64_t root_u, int64_t l, int64_t r) {
    if (l == r) { return l; }
    int64_t mid          = l + ((r - l) / 2);
    int64_t left_min_pos = nodes[nodes[root_u].left].last_pos;
    // 如果左侧的最小最后出现位置小于pos, 说明在左子树中存在小于pos的值
    if (left_min_pos < pos) { return query(pos, nodes[root_u].left, l, mid); }
    // 否则左子树的值都在 pos 以后出现了, 去右子树找
    return query(pos, nodes[root_u].right, mid + 1, r);
  }
};

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);
  cout.tie(nullptr);

  int64_t n, m;
  cin >> n >> m;
  vector<int64_t> nums(n + 1);
  for (int64_t i = 1; i <= n; ++i) { cin >> nums[i]; }
  p_segment_tree pst;
  vector<int64_t> roots(n + 1);
  for (int64_t i = 1; i <= n; ++i) {
    if (nums[i] < 0 || nums[i] > n) {  // 数字越界, 不更新
      roots[i] = roots[i - 1];
      continue;
    }
    roots[i] = pst.update(nums[i], i, roots[i - 1], 0, n);
  }
  for (int64_t i = 0; i < m; ++i) {
    int64_t l, r;
    cin >> l >> r;
    int64_t ans = pst.query(l, roots[r], 0, n);
    cout << ans << "\n";
  }
  return 0;
}

评论