這裡的邊分治和樹上的點分治邊分治不一樣,是維護強連通分量用的,每條邊有一個出現時間,透過將每條邊按連通關係分流重新排列,從而維護每個時間點整張圖的連通性。
具體的,這個演算法是維護這樣的一類問題:
n 個點,m 條邊按時間順序依次加入,每加入一條邊,你需要回答一些問題,比如在這個時間點,圖中有多少強連通分量,或者某個點所在強連通分量的大小。
暴力的做法是每加入一條邊就跑一遍tarjan演算法,當邊比較稠密時,縮點合併比較頻繁,每次縮點就會讓總點數減小,只用縮點後的圖繼續加邊,複雜度似乎說的過去。
但強連通分量比較多時,跑一遍tarjan甚至縮不了任何點,比如一張DAG,就可以輕鬆卡到 n^2。這樣暴力複雜度高的原因在於,很多無法成為強連通分量裡的邊遍歷了好幾次,每次都沒有被縮掉,極大地浪費了時間。
按連通性進行邊分治:
我們希望多次進行tarjan演算法時,那些連不出強連通分量的邊可以少遍歷幾次,而是儘量讓它們在形成強連通分量的時候被遍歷,因為這樣可以把這些點和邊縮掉,以後也不用遍歷。
一個很妙的分治方式:我們將當前計算的時間線 \([l,r]\) 分一半,只保留前一半時間的邊,跑一遍tarjan演算法,將強連通分量找到,把那些已經在強連通分量中的邊放進前一半時間 \([l,mid]\) ,把沒有進入強連通分量的邊放進後一半時間 \([mid+1,r]\) 。
這樣做的意義是:在 mid 時刻某些邊還沒有進入強連通分量,說明在之前的時刻也一定沒有進入,這條邊就是一條廢邊,在前 mid 的時間裡對我們要維護的東西(連通塊)沒有任何作用,我們把它丟進後半的時間,也就是在計算前面的時間段內,tarjan演算法根本不會去跑這條邊,這就節省了暴力做法多次遍歷同一條邊的複雜度。而反過來,在mid時刻形成的強連通分量,必定是在 \([l,mid]\) 內形成的,但我們不知道具體形成時間。所以我們再分治下去,計算 \([l,mid]\) 這個時間段內的連通情況,以及後半段 \([mid+1,r]\) 的連通情況,在後半段,新加入的邊和前半段丟進來的廢邊有可能會形成新的強連通分量。
我們將每條邊不斷地向下分,形成類似線段樹的分治結構,這個和線段樹分治+可撤銷並查集的科技非常相似,都是按時間分治,有異曲同工之處。
我們放一張圖,圖中的邊權是這條邊加入的時間:
當我們的時間分治到線段樹的葉子時,也就是代表這個時刻,由於我們一直將同一個強連通分量的邊放在同一側,在葉子處的邊一定是形成了新的強連通分量,我們可以放心地將葉子處的這些點進行合併處理。
注意到時間段 6-10 處的 1,2,3 號點已經縮成了一個點,這是由於我們優先走線段樹的左子樹,在 3 時刻,我們就已經用並查集將1,2,3三個點連線在一起,並用1號點當做這個強連通分量的代表點,之後的連線1,2,3的邊,統一改為連線 1 號點。
在做題時看到題解裡說要用並查集維護縮點,於是去學習了一下,回來發現這個維護和那個維護不是同一回事,有一個並查集代替tarjan的縮點演算法,實現起來也比較簡單,感興趣可以看下:一個代替tarjan的縮點演算法:並查集維護縮點。可是這裡所說的並查集僅僅是維護一下每個強連通分量有哪些點,並沒有替換掉tarjan演算法。
這張圖中還有一個要注意的點:雖然我們只有9條邊,但是分治的時間段是1-10,這是因為時間點10是用於 “垃圾存放” 的,因為我們每個時間段都把邊分成了兩種,把廢邊扔到右側,保證在遍歷到葉子時一定是找到了一個強連通分量,但是仍然有一些邊最終也沒有進入強連通分量(如此圖中的7號邊),我們每次都將它分流到右側,因此被放在了垃圾回收的地方,多出來的這個時刻 10 也不需要我們記錄答案。
注意到每層都是 m 條邊,一共log層,因此複雜度是 \(O(mlogm)\)。
下面是演算法的核心程式碼:(此模板用於記錄每個時刻強連通分量數量,程式碼下附帶樣例與上圖相同)
int find(int x) { return fa[x]==x ? x : fa[x]=find(fa[x]); }
inline void merge(int x,int y) {
x = find(x); y = find(y);
if(x==y) return ;
fa[x] = y;
_ans--;
}
void tarjan(int u) {
dfn[u] = low[u] = ++tim;
st[++cnt] = u; in[u] = 1;
for(int v : e[u]) {
if(!dfn[v]) {
tarjan(v);
low[u] = min(low[u],low[v]);
}
else if(in[v]) low[u] = min(low[u],dfn[v]);
}
if(low[u]==dfn[u]) {
while(1) {
int v = st[cnt--]; in[v] = 0;
belong[v] = u;
if(v==u) break;
}
}
}
void solve(ll now,ll l,ll r) {
if(l==r) {
for(E vv : d[now]) merge(vv.x, vv.y);
vector<E>().swap(d[now]);
ans[l] = _ans;
return ;
}
ll mid = l+r >> 1;
for(E &vv : d[now]) {
e[vv.x = find(vv.x)].clear();
e[vv.y = find(vv.y)].clear();
dfn[vv.x] = dfn[vv.y] = 0;
}
for(E vv : d[now]) if(vv.ti<=mid) e[vv.x].push_back(vv.y);
for(E vv : d[now]) {
if(vv.ti<=mid) {
if(!dfn[vv.x]) tarjan(vv.x);
if(!dfn[vv.y]) tarjan(vv.y);
if(belong[vv.x]==belong[vv.y]) d[ls].push_back(vv);
else d[rs].push_back(vv);
}
else {
d[rs].push_back(vv);
}
}
vector<E>().swap(d[now]);
solve(ls, l, mid);
solve(rs, mid+1, r);
}
void chushihua() {
_ans = 0;
tim = 0;
}
int main() {
int x,y;
T = read();
while(T--) {
chushihua();
n = read(); m = read();
_ans = n;
for(int i=1;i<=n;i++) fa[i] = i;
for(int i=1;i<=m;i++) {
x = read(); y = read();
d[1].push_back({x,y,i});
}
solve(1, 1, m+1);
for(int i=1;i<=m;i++) cout<<ans[i]<<"\n";
}
return 0;
}
/*
1
7 9
1 3
2 1
3 2
3 4
5 6
4 5
5 7
6 4
4 2
*/
例題1:CF1989F
題意:一個方陣,可以橫著刷紅漆,豎著刷藍漆,你可以不花費代價一行一列刷,或花費 k^2 的代價同時刷 k 次,相交處顏色自己定。Q次詢問,每次增加一個格子的顏色限制,問最小花費。
Solution:
我們把每一行,每一列的刷漆動作都看成一個點,一個格子顏色的限制暗示了一個順序:這個顏色的動作必須晚於另一個顏色的動作。這個限制也就是拓撲序的一條邊,連線這一行一列兩個動作的點。
這些邊限制了我們選點的順序,而當這些邊形成了環,我們就無法找出一個拓撲序,就需要進行同時刷 k 次的操作。根據經驗得出 k 就是這些邊強連通分量的大小,這個強連通分量的貢獻也就是 size 的平方。
因此題意轉化為了:每個時刻加入一條邊,詢問當前時刻所有 size>1 的強連通分量的 size 平方和。可以直接套用邊分治縮點的模板。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
#define ls now<<1
#define rs now<<1|1
using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
ll T;
ll n,m,Q,tot;
ll ans[N], _ans;
ll fa[N],siz[N];
struct E{
ll x,y,ti;
};
vector <E> d[N<<2];
vector <ll> e[N];
ll tim,dfn[N],low[N],belong[N];
ll in[N],st[N],cnt;
ll find(ll x) { return fa[x]==x ? x : fa[x]=find(fa[x]); }
inline void merge(ll x,ll y) {
x = find(x); y = find(y);
if(x==y) return ;
if(siz[x]>1) _ans -= siz[x] * siz[x];
if(siz[y]>1) _ans -= siz[y] * siz[y];
fa[x] = y;
siz[y] += siz[x];
_ans += siz[y] * siz[y];
}
void tarjan(ll u) {
dfn[u] = low[u] = ++tim;
st[++cnt] = u; in[u] = 1;
for(ll v : e[u]) {
if(!dfn[v]) {
tarjan(v);
low[u] = min(low[u],low[v]);
}
else if(in[v]) low[u] = min(low[u],dfn[v]);
}
if(low[u]==dfn[u]) {
while(1) {
ll v = st[cnt--]; in[v] = 0;
belong[v] = u;
if(v==u) break;
}
}
}
void solve(ll now,ll l,ll r) {
if(l==r) {
for(E v : d[now]) merge(v.x, v.y);
vector<E>().swap(d[now]);
ans[l] = _ans;
return ;
}
ll mid = l+r >> 1;
for(ll i=0;i<d[now].size();i++) {
d[now][i].x = find(d[now][i].x);
d[now][i].y = find(d[now][i].y);
}
for(E v : d[now]) e[v.x].clear(), e[v.y].clear(), dfn[v.x] = dfn[v.y] = 0;
for(E v : d[now]) if(v.ti<=mid) e[v.x].push_back(v.y);
for(E v : d[now]) {
if(v.ti<=mid) {
if(!dfn[v.x]) tarjan(v.x);
if(!dfn[v.y]) tarjan(v.y);
if(belong[v.x]==belong[v.y]) d[ls].push_back(v);
else d[rs].push_back(v);
}
else {
d[rs].push_back(v);
}
}
vector<E>().swap(d[now]);
solve(ls, l, mid);
solve(rs, mid+1, r);
}
int main() {
ll x,y; char cz[2];
n = read(); m = read(); Q = read();
tot = n+m;
for(ll i=1;i<=tot;i++) fa[i] = i, siz[i] = 1;
for(ll i=1;i<=Q;i++) {
x = read(); y = read();
scanf("%s",cz);
if(cz[0]=='R') d[1].push_back({y+n, x, i});
else d[1].push_back({x, y+n, i});
}
solve(1, 1, Q+1);
for(ll i=1;i<=Q;i++) cout<<ans[i]<<"\n";
return 0;
}
例題2:P5163
題意:給你一張 n 個點 m 條邊的有向圖,q 個操作:操作一,刪除某條邊;操作二,增加某個點權值;操作三,詢問某個點所在強連通分量內前 k 大點權和。
Solution:
我們的邊分治縮點模板是每個時刻加入一條邊,這題是刪除某條邊,我們倒著來做,從最後一個操作開始往上,就變成了加邊。由於最後一個操作時邊還有剩餘,我們也給剩餘的邊設定一個不一樣加入時間,省去討論的麻煩。
將每個修改點權的操作和詢問掛在某條邊加入的時間點,在分治到葉子結點時處理這些操作。記住我們正著掛,在遍歷時要倒著遍歷。
這題要維護的東西比上一個例題要複雜,但也是很模板的東西,而且和我們的邊分治過程程式碼交叉很少,所以不難寫。
可以用平衡樹,啟發式合併,空間稍微小點但是時間多個log,不如權值線段樹合併,也可以維護前 k 大的權值。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
#define ls L[now]
#define rs R[now]
using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
ll n,m,Q;
map <ll,ll> f;
struct E{
ll x,y,ti;
};
vector <E> d[N<<2];
vector <ll> e[N];
ll X[N],Y[N];
struct Qr{
ll cz,x,y;
};
vector <Qr> g[N];
ll ans[N],cntq;
ll a[N];
ll ma_val = 1e9;
ll siz[N*34],t[N*34],rt[N],tot,L[N*34],R[N*34];
inline void pushup(ll now) {
t[now] = t[ls] + t[rs];
siz[now] = siz[ls] + siz[rs];
}
void insert(ll &now,ll l,ll r,ll x,ll v) {
// if(l==1 && r==ma_val) cout<<""
if(!now) now = ++tot;
if(l==r) { siz[now] += v; t[now] += l*v; return ; }
ll mid = (l+r) >> 1;
if(x<=mid) insert(ls, l, mid, x, v);
else insert(rs, mid+1, r, x, v);
if(!siz[ls]) ls = 0;
if(!siz[rs]) rs = 0;
pushup(now);
}
ll query(ll now,ll l,ll r,ll k) {
if(siz[now]<=k) return t[now];
if(l==r) { return k*l; }
ll mid = (l+r) >> 1;
if(siz[rs]>=k) return query(rs, mid+1, r, k);
else return query(ls, l, mid, k-siz[rs]) + t[rs];
}
ll merge_tree(ll r1,ll r2,ll l,ll r) {
if(!r1 || !r2) return r1 + r2;
if(l==r) {
siz[r1] += siz[r2];
t[r1] += t[r2];
return r1;
}
ll mid = (l+r) >> 1;
L[r1] = merge_tree(L[r1], L[r2], l, mid);
R[r1] = merge_tree(R[r1], R[r2], mid+1, r);
pushup(r1);
return r1;
}
ll pa[N];
ll find(ll x) { return pa[x]==x ? x : pa[x]=find(pa[x]); }
inline void merge(ll x,ll y) {
x = find(x); y = find(y);
if(x==y) return ;
pa[x] = y;
rt[y] = merge_tree(rt[x], rt[y], 1, ma_val);
}
ll tim,dfn[N],low[N],belong[N];
ll in[N],st[N],cnt;
void tarjan(ll u) {
dfn[u] = low[u] = ++tim;
st[++cnt] = u; in[u] = 1;
for(ll v : e[u]) {
if(!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(in[v]) low[u] = min(low[u], dfn[v]);
}
if(low[u]==dfn[u]) {
while(1) {
ll v = st[cnt--]; in[v] = 0;
belong[v] = u;
if(v==u) break;
}
}
}
void calc(ll ti) {
for(ll i=g[ti].size()-1; i>=0; i--) {
Qr vv = g[ti][i];
if(vv.cz==2) {
insert(rt[find(vv.x)], 1, ma_val, a[vv.x], -1);
a[vv.x] -= vv.y;
insert(rt[find(vv.x)], 1, ma_val, a[vv.x], 1);
}
else {
ans[++cntq] = query(rt[find(vv.x)], 1, ma_val, vv.y);
}
}
}
void solve(ll now,ll l,ll r) {
if(l==r) {
for(E vv : d[now]) merge(vv.x, vv.y);
vector<E>().swap(d[now]);
calc(l);
return ;
}
ll mid = (l+r) >> 1;
for(E &vv : d[now]) {
e[vv.x = find(vv.x)].clear();
e[vv.y = find(vv.y)].clear();
dfn[vv.x] = dfn[vv.y] = 0;
}
for(E vv : d[now]) if(vv.ti<=mid) e[vv.x].push_back(vv.y);
for(E vv : d[now]) {
if(vv.ti<=mid) {
if(!dfn[vv.x]) tarjan(vv.x);
if(!dfn[vv.y]) tarjan(vv.y);
if(belong[vv.x]==belong[vv.y]) d[now<<1].push_back(vv);
else d[now<<1|1].push_back(vv);
}
else {
d[now<<1|1].push_back(vv);
}
}
vector<E>().swap(d[now]);
solve(now<<1, l, mid);
solve(now<<1|1, mid+1, r);
}
int main() {
ll cz,x,y;
n = read(); m = read(); Q = read();
ll nowtim = m;
for(ll i=1;i<=n;i++) a[i] = read(), pa[i] = i;
for(ll i=1;i<=m;i++) X[i] = read(), Y[i] = read();
for(ll i=1;i<=Q;i++) {
cz = read();
if(cz==1) {
x = read(); y = read();
f[x*n+y] = 1;
d[1].push_back({x,y,nowtim});
nowtim--;
}
if(cz==2) {
x = read(); y = read();
a[x] += y;
g[nowtim].push_back({2,x,y});
}
if(cz==3) {
x = read(); y = read();
g[nowtim].push_back({3,x,y});
}
}
for(ll i=1;i<=m;i++) {
if(f[X[i]*n+Y[i]]) continue;
d[1].push_back({X[i],Y[i],nowtim});
nowtim--;
}
for(ll i=1;i<=n;i++) {
insert(rt[i], 1, ma_val, a[i], 1);
}
calc(0);
solve(1, 1, m+1);
for(ll i=cntq;i>=1;i--) cout<<ans[i]<<"\n";
return 0;
}