【BZOJ4025】二分圖(可撤銷並查集+線段樹分治)
題目:
分析:
定理:一個圖是二分圖的充要條件是不存在奇環。
先考慮一個弱化的問題:保證所有邊出現的時間段不會交叉,只會包含或相離。
還是不會?再考慮一個更弱化的問題:邊只會出現不會消失。
當加邊的時候,若\((u,v)\)不連通:一定不會構成奇環,將它加入。
若\((u,v)\)已經聯通,則不加入這條邊,而是查詢\(u\)和\(v\)兩點間的距離。若為偶數則加上這條邊後會形成奇環。一個奇環不可能分成數個偶環,所以從此以後都不再是二分圖。若為奇數則直接忽略這條邊,因為如果將來某條邊會與這條邊形成奇環,則用當前\(u\)到\(v\)之間的路徑(長度為奇數)替代這條邊(長度為\(1\)
按照上述做法,最終我們會造出原圖的一棵生成樹。
用帶權並查集維護連通性和兩點間距離的奇偶性。記錄每個結點到它並查集上的父親的距離(注意並查集的形態不一定和原樹相同。加入邊\((u,v)\)時在並查集上連線的是它們所在集合的根\(f(u)\)和\(f(v)\))。
當加入邊\((u,v)\)時,設\(u\)和\(v\)所在並查集的根為\(x\)和\(y\),要把\(x\)合併進\(y\),\(x\)的父親被賦值成\(y\)。腦補一下此時\(x\)到\(y\)的路徑是\(x\)到\(u\),邊\((u,v)\),然後\(v\)到\(y\),所以暴力爬鏈分別求出\(u\)
查詢時把兩點到根距離異或起來就是它們之間距離的奇偶性。(樹上兩點間路徑是唯一的,多餘的路徑被走了偶數次不影響奇偶性。)
如果邊會消失呢?刪除一條已經被加入的邊時,由於保證不存在出現時間交叉的情況,刪除的一定是最晚加入的邊。因此把對並查集的每次修改都存到一個棧中,撤銷的時候按修改順序的逆序復原即可。這種神奇的資料結構叫“可撤銷並查集”
為了保證\(O(\log n)\)的時間複雜度,需要按秩合併。同時由於要可撤銷,不能路徑壓縮破壞樹形。放上可撤銷並查集的程式碼。
namespace UFS { int fa[N], top, rk[N]; bool dis[N]; struct node { int x, y, f, r; bool d; }stack[N]; int f(const int x) { return x == fa[x] ? x : f(fa[x]); } inline void init() { for (int i = 1; i <= n; i++) fa[i] = i, dis[i] = 0, rk[i] = 1; } inline bool dist(int x) { return x == fa[x] ? dis[x] : dist(fa[x]) ^ dis[x]; } inline void merge(const int u, const int v) { int x = f(u), y = f(v); if (rk[x] > rk[y]) swap(x, y); int tmp = fa[x]; stack[top++] = (node){x, y, tmp, rk[y], dis[x]}; if (rk[x] == rk[y]) ++rk[y]; dis[x] ^= dist(u) ^ dist(v) ^ 1; fa[x] = y; } inline void undo(const int bck) { while (top > bck) { fa[stack[top - 1].x] = stack[top - 1].f; rk[stack[top - 1].y] = stack[top - 1].r; dis[stack[top - 1].x] = stack[top - 1].d; --top; } } }
考慮原問題。可以把每條邊的出現時間拆成若干個區間,使這些區間互不交叉。然而,如果直接貪心地拆,這些區間的總數會被極端資料卡到\(m^2\)級別(如果所有邊出現的區間均為\([k,k+m](1\leq k \leq m)\),則第\(k\)條邊將被分成\(k\)個區間,共有\(\frac{m(m+1)}{2}\)個區間)。
這種方式的缺陷在於邊的順序不夠靈活。比如上述極端資料中的第\(m\)條邊,在\([m+1,2m]\)這個區間中每個時刻都要被刪一次再加入一次。如果把它第一個加入(即放在撤銷棧的底部),就省了很多操作。
這裡引入“線段樹分治”。建立一棵線段樹,每個結點記錄哪些邊能完全覆蓋這個結點所代表區間(不重複記錄。即如果一個結點記錄了邊\(e\),那麼它子樹上的結點都不必記錄\(e\))。這樣每條邊最多分成\(\log T\)個區間。最後深搜線段樹,搜到一個結點時加入這個結點記錄的所有邊,回溯時撤銷。如果到葉子結點仍然沒有出現奇環,則此時刻是二分圖。可以參考程式碼理解。每個邊最多被加入或撤銷\(\log T\)次,每次加入需要\(\log m\)時間,總複雜度\(O(n\log^2n)\)。
程式碼:
一個優化:搜到一個結點時如果已經是二分圖了,就不必再往下搜。
#include <cstdio>
#include <algorithm>
#include <cctype>
#include <cstring>
using namespace std;
namespace zyt
{
template<typename T>
inline void read(T &x)
{
bool f = false;
char c;
x = 0;
do
c = getchar();
while (c != '-' && !isdigit(c));
if (c == '-')
f = true, c = getchar();
do
x = x * 10 + c - '0', c = getchar();
while (isdigit(c));
if (f)
x = -x;
}
template<typename T>
inline void write(T x)
{
static char buf[20];
char *pos = buf;
if (x < 0)
x = -x;
do
*pos++ = x % 10 + '0';
while (x /= 10);
while (pos > buf)
putchar(*--pos);
}
inline void write(const char *const s)
{
printf("%s", s);
}
const int N = 1e5 + 10, M = 2e5 + 10, B = 17;
int n, m, T;
namespace UFS
{
int fa[N], top, rk[N];
bool dis[N];
struct node
{
int x, y, f, r;
bool d;
}stack[N];
int f(const int x)
{
return x == fa[x] ? x : f(fa[x]);
}
inline void init()
{
for (int i = 1; i <= n; i++)
fa[i] = i, dis[i] = 0, rk[i] = 1;
}
inline bool dist(int x)
{
return x == fa[x] ? dis[x] : dist(fa[x]) ^ dis[x];
}
inline void merge(const int u, const int v)
{
int x = f(u), y = f(v);
if (rk[x] > rk[y])
swap(x, y);
int tmp = fa[x];
stack[top++] = (node){x, y, tmp, rk[y], dis[x]};
if (rk[x] == rk[y])
++rk[y];
dis[x] ^= dist(u) ^ dist(v) ^ 1;
fa[x] = y;
}
inline void undo(const int bck)
{
while (top > bck)
{
fa[stack[top - 1].x] = stack[top - 1].f;
rk[stack[top - 1].y] = stack[top - 1].r;
dis[stack[top - 1].x] = stack[top - 1].d;
--top;
}
}
}
int ecnt, head[1 << (B + 1)];
bool ans[N];
struct edge
{
int to, next;
}e[M * 18];
struct ed
{
int u, v, st, ed;
}arr[M];
void add(const int a, const int b)
{
e[ecnt] = (edge){b, head[a]}, head[a] = ecnt++;
}
namespace Segment_Tree
{
void insert(const int rot, const int lt, const int rt, const int ls, const int rs, const int x)
{
if (ls <= lt && rt <= rs)
{
add(rot, x);
return;
}
int mid = (lt + rt) >> 1;
if (ls <= mid)
insert(rot << 1, lt, mid, ls, rs, x);
if (rs > mid)
insert(rot << 1 | 1, mid + 1, rt, ls, rs, x);
}
void solve(const int rot, const int lt, const int rt)
{
using namespace UFS;
int bck = top;
bool flag = true;
for (int i = head[rot]; ~i; i = e[i].next)
{
int now = e[i].to, x = f(arr[now].u), y = f(arr[now].v);
if (x == y && !(dist(arr[now].u) ^ dist(arr[now].v)))
{
flag = false;
break;
}
else if (x != y)
merge(arr[now].u, arr[now].v);
}
if (flag)
{
if (lt == rt)
ans[lt] = true;
else
{
int mid = (lt + rt) >> 1;
solve(rot << 1, lt, mid);
solve(rot << 1 | 1, mid + 1, rt);
}
}
undo(bck);
}
}
int work()
{
using namespace Segment_Tree;
read(n), read(m), read(T);
UFS::init();
memset(head, -1, sizeof(head));
for (int i = 1; i <= m; i++)
{
read(arr[i].u), read(arr[i].v), read(arr[i].st), read(arr[i].ed);
insert(1, 1, T, arr[i].st + 1, arr[i].ed, i);
}
solve(1, 1, T);
for (int i = 1; i <= T; i++)
if (ans[i])
write("Yes\n");
else
write("No\n");
return 0;
}
}
int main()
{
return zyt::work();
}