P5787 二分图 /【模板】线段树分治
知识点: 线段树分治,并查集
题意简述
给定 \(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\) 在同一并查集中,则加入该边后相当于出现一个奇环,破坏了二分图的结构,之后再加任意数量的边都不为二分图。
再考虑消失的限制,并找不到什么可简单维护的方式。
考虑一种新的科技:线段树分治。
有这样的一种模型:
- 给定一些仅在 给定时间范围 内有效的操作。
- 询问某个时间点所有操作的贡献。
考虑离线操作,对时间轴建立线段树,将每个操作转化为线段树上的区间标记操作。
查询时遍历整棵线段树,到达每个节点时执行相应的操作,到达叶节点统计贡献,回溯时撤销操作的影响。
再回到本题,对时间轴建立线段树。
维护区间内存在的边集,用 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;
}