P5787 二分图 /【模板】线段树分治

知识点: 线段树分治,并查集

原题面:Luogu darkbzoj


题意简述

给定 \(n\) 个节点,有 \(m\) 条出现一段时间后后消失的边,第 \(i\) 条边 \((x_i,y_i)\) 的存在时间为 \([l_i,r_i]\)
给定参数 \(k\),判断 \(1\sim k\) 内每个时刻时,整个图是不是二分图。
\(n,k = 10^5\)\(m=2\times 10^5\)\(1\le u_i,v_i\le n\)\(0\le l\le r\le k\)


分析题意

先考虑所有 \(r_i = k\) 的情况,即仅有加边操作,所有的边出现后不消失。
判断二分图,想到这个题:P1525 关押罪犯
考虑使用扩展域并查集进行维护。
具体地,将点 \(i\) 拆成点 \(i\)\(i+n\),分别表示在同一独立集,与不在同一独立集。
对于一条边 \((x_i,y_i)\),将 \(x_i\)\(y_i+n\) 合并,\(x_i+n\)\(y_i\) 合并。
在加入一条边 \((x_i,y_i)\) 之前,若发现 \(x_i,y_i\) 在同一并查集中,则加入该边后相当于出现一个奇环,破坏了二分图的结构,之后再加任意数量的边都不为二分图。


再考虑消失的限制,并找不到什么可简单维护的方式。
考虑一种新的科技:线段树分治。

有这样的一种模型:

  1. 给定一些仅在 给定时间范围 内有效的操作。
  2. 询问某个时间点所有操作的贡献。

考虑离线操作,对时间轴建立线段树,将每个操作转化为线段树上的区间标记操作。
查询时遍历整棵线段树,到达每个节点时执行相应的操作,到达叶节点统计贡献,回溯时撤销操作的影响。


再回到本题,对时间轴建立线段树。
维护区间内存在的边集,用 vector 进行维护。

查询时从根节点出发开始遍历,递归处理时将当前节点存在的边进行合并,判断是否为二分图。
若到达某个点是不为二分图,则该点维护的时间区间内原图均不为二分图。
到达叶节点时,若仍为二分图,输出 Yes。

回溯时,发现并查集并不支持删除操作。
考虑使用一个栈记录下对并查集的操作,将父子关系再改回去。
则不可使用路径压缩,否则操作次数爆炸,为保证复杂度,应进行按秩合并。

总复杂度 \(O(m\log n\log k)\)

参考:题解 P5787 【二分图 _【模板】线段树分治】 - xht37 的洛谷博客


爆零小技巧

注意扩展域并查集对空间的需求。


代码实现

//知识点:线段树分治,并查集 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
#define ll long long
const int kMaxn = 2e5 + 10;
//=============================================================
struct Edge {
  int u, v;
} e[kMaxn << 1];
struct Stack {
  int u, add;
} st[kMaxn];
int n, m, k, top, fa[kMaxn], height[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void GetMax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void GetMin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
int Find(int now_) {
  while (now_ != fa[now_]) now_ = fa[now_];
  return now_;
}
void Union(int u_, int v_) {
  int fu = Find(u_), fv = Find(v_);
  if (height[fu] > height[fv]) std :: swap(fu, fv);
  st[++ top] = (Stack) {fu, height[fu] == height[fv]};
  fa[fu] = fv;
  if (height[fu] == height[fv]) height[fv] ++;
}
namespace SegmentTree {
  #define ls (now_<<1)
  #define rs (now_<<1|1)
  #define mid ((L_+R_)>>1)
  std :: vector <int> t[kMaxn << 2];
  void Modify(int now_, int L_, int R_, int l_, int r_, int val_) {
    if (l_ <= L_ && R_ <= r_) {
      t[now_].push_back(val_);
      return ;
    }
    if (l_ <= mid) Modify(ls, L_, mid, l_, r_, val_);
    if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, val_);
  }
  void Solve(int now_, int L_, int R_) {
    bool ans = true;
    int last_top = top;
    for (int i = 0, size = t[now_].size(); i < size; ++ i) {
      Edge now = e[t[now_][i]];
      if (Find(now.u) == Find(now.v)) {
        for (int i = L_; i <= R_; ++ i) printf("No\n");
        ans = false;
        break;
      }
      Union(now.u, now.v + n);
      Union(now.v, now.u + n);
    }
    
    if (ans) {
      if (L_ == R_) {
        printf("Yes\n"); 
      } else {
        Solve(ls, L_, mid);
        Solve(rs, mid + 1, R_);
      }
    }
    
    for (; top > last_top; -- top) {
      height[fa[st[top].u]] -= st[top].add;
      fa[st[top].u] = st[top].u;
    }
  }
}
//=============================================================
int main() {
  n = read(), m = read(), k = read();
  for (int i = 1; i <= m; ++ i) {
    e[i] = (Edge) {read(), read()};
    int l = read() + 1, r = read();
    SegmentTree :: Modify(1, 1, k, l, r, i);
  }
  for (int i = 1; i <= 2 * n; ++ i) {
    fa[i] = i;
    height[i] = 1;
  }
  SegmentTree :: Solve(1, 1, k);
  return 0;
}
posted @ 2020-09-04 21:33  Luckyblock  阅读(360)  评论(0编辑  收藏  举报