首页 > 其他分享 >边分治维护强连通分量(CF1989F,P5163)

边分治维护强连通分量(CF1989F,P5163)

时间:2024-08-04 17:27:59浏览次数:16  
标签:连通 ll 分治 CF1989F mid P5163 low vv now

这里的边分治和树上的点分治边分治不一样,是维护强连通分量用的,每条边有一个出现时间,通过将每条边按连通关系分流重新排列,从而维护每个时间点整张图的连通性。


具体的,这个算法是维护这样的一类问题:

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;
}

标签:连通,ll,分治,CF1989F,mid,P5163,low,vv,now
From: https://www.cnblogs.com/maple276/p/18342011

相关文章

  • 树分治
    点分治点分治适合处理大规模的树上路径信息问题。LuoguP4178Tree给定一棵有\(n\)个点的带权树,给出\(k\),询问树上距离小于等于\(k\)的点对数量。分治地考虑每一个子树,求出重心,对答案有贡献的是三种点对,依次解决。重心-子树上的点直接累加贡献即可。子树上的点......
  • cdq分治
    cdq分治主要思想为分治,分为三个部分:左区间内部。左区间对右区间。右区间内部。一个保险的标准顺序是先处理左区间,再处理左区间对右区间的贡献,最后处理右区间,这样就可以保证时序性了。注意这种写法在处理左区间对右区间贡献是要先按标号排序分出正确的左右区间,如果是先递归......
  • 解密编程的八大法宝(四)(附二分查找、分治法和图论算法(深度和广度优先搜索、最短路径、最
    算法题中常见的几大解题方法有以下几种::暴力枚举法(BruteForce):这是最基本的解题方法,直接尝试所有可能的组合或排列来找到答案。这种方法适用于问题规模较小的情况,但在大多数情况下效率不高。贪心算法(GreedyAlgorithm):贪心算法在每一步都选择当前看起来最优的解,希望最终能......
  • 点分治板子
    #define_CRT_SECURE_NO_WARNINGS#include<iostream>#include<vector>#include<algorithm>#include<math.h>#include<sstream>#include<string>#include<string.h>#include<iomanip>#include<stdlib.h>#inc......
  • 线段树分治小结
    一般来说,这种题目是给定一些信息,比如说边,然后这些信息会在一个时间段内出现。一般来说需要离线,然后建立一棵以维护每个时刻信息的线段树,具体来说就是在每个节点维护一个vector。#121.「离线可过」动态图连通性以经典例题来说,我们根据每条边出现的时刻将它插入到线段树上对应时......
  • 树分治、动态树分治学习笔记
    点分治点分治适合处理大规模的树上路径信息问题,选取重心,将当前的树拆分为几颗子树,然后递归子树求解问题,但是今天的重点不在这里边分治与点分治类似,选取一条边,均匀地将树分成两个部分,但是对于一个点有多个儿子时,时间复杂度就会非常大,于是我们可以将其转化,这里有两种方法\(1.\)......
  • 归并分治_小和问题
    归并分治原理:1)思考一个问题在大范围上的答案,是否等于,左部分的答案+右部分的答案+跨越左右产生的答案2)计算“跨越左右产生的答案”时,如果加上左、右各自有序这个设定,会不会获得计算的便利性3)如果以上两点都成立,那么该问题很可能被归并分治解决4)求解答案的过程中只需......
  • C141 线段树分治+线性基 P3733 [HAOI2017] 八纵八横
    视频链接:C141线段树分治+线性基P3733[HAOI2017]八纵八横_哔哩哔哩_bilibili   P3733[HAOI2017]八纵八横-洛谷|计算机科学教育新生态(luogu.com.cn)//线段树分治+线性基O(q*logq*logL*logL)#include<iostream>#include<cstring>#include<algorithm>......
  • cdq分治 提高篇
    优化动态规划序列首先要会最长上升子序列的转移,这里就不说了。我们\(i\)位置的初始值为\(a_i\),可能变成的最大值为\(mx_i\),可能变成的最小值为\(mn_i\)。然后如果\(j\)要转移到\(i\),则需要满足:\(j<i,mx_j\lea_i,a_j\lemn_i\)。然后考虑把\([l,mid]\)按照\(mx\)......
  • sol-cf1989F
    非常Educational的好题。题目条件非常难看,考虑转化为如下形式:对于每一行每一列建立一个虚点表示该行该列。总点数\(n+m\)。如果要求\(a_{i,j}\)为红色,则\(i\)向\(j+n\)连一条有向边,否则\(j+n\)向\(i\)连一条有向边。容易发现,如果图是一个DAG的话那么我们显然......