G. Penacony - Codeforces Round 962 (Div. 3)

https://codeforces.com/contest/1996/problem/G

题目大意

给出 $n \le 2 \cdot 10^5$ 个点的一个简单环,给出 $m \le 2 \cdot 10^5$ 组点。要求选最少的边,使得 $m$ 组点之间,都可以通过选的边连通。给的每组点 $u < v$。

简要题解

一般环形问题先想直线问题,再想断开环。

直线问题非常好解决,相当于求 $m$ 个线段的覆盖。

断开环。注意到最多只需要 $n - 1$ 条边,因此我们直接枚举这个断开的位置,然后动态的维护覆盖。注意到一组线段 $u \to v$ 如果切点在 $u, v$ 之间则必然需要选绕过起点终点的方案,否则必然选 $u \to v$ 之间的覆盖(因为切点在外面会切断另一种选法),因此其实切点确定了,每组关系选择的方案也就确定了。

发现不下传标记的线段树可以维护这个覆盖,然后就做完了。(同样的线段树用于矩形面积交、并的扫描线线段树)

复杂度

$T$:$O(nlogn)$

$S$:$O(n)$

代码实现

#include <bits/stdc++.h>
using namespace std;

int io_=[](){ ios::sync_with_stdio(false); cin.tie(nullptr); return 0; }();

using LL = long long;
using ULL = unsigned long long;
using LD = long double;
using PII = pair<int, int>;
using VI = vector<int>;
using MII = map<int, int>;

template<typename T> void cmin(T &x,const T &y) { if(y<x) x=y; }
template<typename T> void cmax(T &x,const T &y) { if(x<y) x=y; }
template<typename T> bool ckmin(T &x,const T &y) { 
    return y<x ? (x=y, true) : false; }
template<typename T> bool ckmax(T &x,const T &y) { 
    return x<y ? (x=y, true) : false; }
template<typename T> void cmin(T &x,T &y,const T &z) {// x<=y<=z 
    if(z<x) { y=x; x=z; } else if(z<y) y=z; }
template<typename T> void cmax(T &x,T &y,const T &z) {// x>=y>=z
    if(x<z) { y=x; x=z; } else if(y<z) y=z; }

// mt19937 rnd(chrono::system_clock::now().time_since_epoch().count());
// mt19937_64 rnd_64(chrono::system_clock::now().time_since_epoch().count());

/*
---------1---------2---------3---------4---------5---------6---------7---------
1234567890123456789012345678901234567890123456789012345678901234567890123456789
*/

// enumerate the split point!

struct SegT {
  int n;
  vector<int> cover, sum;

  SegT(int n) : n(n), cover(n << 2), sum(n << 2) {}

  int L, R, V;
  void update_(int o, int l, int r) {
    if (L <= l && r <= R) {
      cover[o] += V;

      if (cover[o]) sum[o] = r - l + 1;
      else {
        if (l == r) {
          sum[o] = 0;
        } else {
          sum[o] = sum[o << 1] + sum[o << 1 | 1];
        }
      }

      return;
    }
    int mid = (l + r) >> 1;
    if (L <= mid) update_(o << 1, l, mid);
    if (mid < R) update_(o << 1 | 1, mid + 1, r);

    if (!cover[o]) sum[o] = sum[o << 1] + sum[o << 1 | 1];
  }

  void update(int l, int r, int v) {
    L = l; R = r; V = v;
    update_(1, 0, n - 1);
  }
};

void solve() {
  int n, m; cin >> n >> m;
  vector<vector<int>> st(n), ed(n);

  SegT segt(n);
  int u, v;
  for (int i = 0; i < m; i++) {
    cin >> u >> v; u--; v--;
    st[u].push_back(v);
    ed[v].push_back(u);

    segt.update(u, v - 1, 1);
  }

  int ans = segt.sum[1];

  for (int i = 0; i < n - 1; i++) {
    for (int r : st[i]) {
      segt.update(i, r - 1, -1);
      segt.update(r, n - 1, 1);
      if (i) {
        segt.update(0, i - 1, 1);
      }
    }
    for (int l : ed[i]) {
      segt.update(i, n - 1, -1);
      if (l) {
        segt.update(0, l - 1, -1);
      }
      segt.update(l, i - 1, 1);
    }

    cmin(ans, segt.sum[1]);
  }
  cout << ans << '\n';
}

int main() {
  int t = 1; 
  cin >> t;
  while (t--) {
    solve();
  }
  return 0;
}

做法 2

更进一步的,我们考虑对于某一个答案的覆盖,一些段是由线段 $a_i, b_i$ 覆盖的,而剩下的所有是由 $0 \to a_i$ 和 $b_i \to n$ 覆盖的,注意这里两种覆盖是全集,因此一组 $a_i \to b_i$ 的覆盖的集合就可以表示选取的方案。同样的其补集,一组被切断的边同样可以表示这个覆盖。考虑这个覆盖有多个不选的边 $a_1, a_2, \cdots$ 则我们说,这些所有不选边对应的割边方案一致。

证明:考虑割断 $a_1$ 位置的集合 $S_1$,割断 $a_2$ 的集合 $S_2$。割断 $a_1$ 时 $a_2$ 也是不选的。考虑 $e \in S_1$ 但 $e \notin S_2$。这里由两个位置都不选,可以得出 $e$ 应该不覆盖 $a_1$ 和 $a_2$,否则这两段的覆盖关系必然是相反的。但从 $e \in S_1$ 但 $e \notin S_2$,可以得出 $e$ 覆盖 $a_1$ 不覆盖 $a_2$。矛盾。

于是我们只要统计每个位置的集合出现的频率,选最高的频率即可。

这里就用到了 Xor Hash。给每个段一个随机值,则集合就是其 xor hash。64 位随机值的容错率已经足够了。

#include <bits/stdc++.h>
using namespace std;

int io_=[](){ ios::sync_with_stdio(false); cin.tie(nullptr); return 0; }();

using LL = long long;
using ULL = unsigned long long;
using LD = long double;
using PII = pair<int, int>;
using VI = vector<int>;
using MII = map<int, int>;

template<typename T> void cmin(T &x,const T &y) { if(y<x) x=y; }
template<typename T> void cmax(T &x,const T &y) { if(x<y) x=y; }
template<typename T> bool ckmin(T &x,const T &y) { 
    return y<x ? (x=y, true) : false; }
template<typename T> bool ckmax(T &x,const T &y) { 
    return x<y ? (x=y, true) : false; }
template<typename T> void cmin(T &x,T &y,const T &z) {// x<=y<=z 
    if(z<x) { y=x; x=z; } else if(z<y) y=z; }
template<typename T> void cmax(T &x,T &y,const T &z) {// x>=y>=z
    if(x<z) { y=x; x=z; } else if(y<z) y=z; }

// mt19937 rnd(chrono::system_clock::now().time_since_epoch().count());
mt19937_64 rnd_64(chrono::system_clock::now().time_since_epoch().count());

/*
---------1---------2---------3---------4---------5---------6---------7---------
1234567890123456789012345678901234567890123456789012345678901234567890123456789
*/

void solve() {
  int n, m; cin >> n >> m;
  vector<ULL> x(n);
  int u, v;
  for (int i = 0; i < m; i++) {
    cin >> u >> v; u--; v--;
    ULL h = rnd_64();
    x[u] ^= h;
    x[v] ^= h;
  }

  map<ULL, int> cnt;
  ULL cur = 0;
  for (int i = 0; i < n; i++) {
    cur ^= x[i];
    cnt[cur]++;
  }

  int mx = 0;
  for (auto&& [v, c] : cnt) {
    cmax(mx, c);
  }
  cout << n - mx << '\n';
}

int main() {
  int t = 1; 
  cin >> t;
  while (t--) {
    solve();
  }
  return 0;
}
Prev: F. Bomb - Codeforces Round 962 (Div. 3)
Next: D. Maximize the Root - Educational Codeforces Round 168