邊分治維護強連通分量(CF1989F,P5163)

maple276發表於2024-08-04

這裡的邊分治和樹上的點分治邊分治不一樣,是維護強連通分量用的,每條邊有一個出現時間,透過將每條邊按連通關係分流重新排列,從而維護每個時間點整張圖的連通性。


具體的,這個演算法是維護這樣的一類問題:

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]\) 的連通情況,在後半段,新加入的邊和前半段丟進來的廢邊有可能會形成新的強連通分量。

我們將每條邊不斷地向下分,形成類似線段樹的分治結構,這個和線段樹分治+可撤銷並查集的科技非常相似,都是按時間分治,有異曲同工之處。

我們放一張圖,圖中的邊權是這條邊加入的時間:

bfzsd.png

當我們的時間分治到線段樹的葉子時,也就是代表這個時刻,由於我們一直將同一個強連通分量的邊放在同一側,在葉子處的邊一定是形成了新的強連通分量,我們可以放心地將葉子處的這些點進行合併處理。

注意到時間段 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;
}

相關文章