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

news/2025/3/18 16:47:19/文章来源:https://www.cnblogs.com/maple276/p/18342011

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


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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/777799.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用 C# 和 ONNX 來玩转Phi-3 SLM

LLM 席卷世界刷新 AI 的认知之后,由于 LLM 需要的硬件要求实在太高,很难在普通设备上运行,因此 SLM 逐漸受到重視,Phi-3 SLM 是由 Microsoft 所开发的模型,可以在你的电脑、手机等设备来运行,小型语言模型 (SLM) 和 ONNX 的结合改变了 AI 互操作性的游戏规则。让我们展…

到底什么是@RestController

@RestController是Spring框架的一个注解,通常用于标识一个类是RESTful服务的Controller。 @RestController经常用来处理HTTP请求,是SpringMVC中用于构建RESTful Web服务的注解,是@Controller注解的变体 通过@RestController注解,SpringMVC可以识别出这个类是一个控制器这个…

【已解决】戴尔 Inspiron 5457 和 5557 启动报错:Invalid configuration information - please run SETUP program.

场景 我使用的是戴尔15-5557的笔记本电脑,在某次开机的时候系统提示:Invalid configuration information - please run SETUP program.虽然的点击Continue可以正常进入系统,但是每次开机都要来上这么一下真的很烦人,于是想着解决一下。 试错过程 1. BIOS调整刚开始以为是自…

6-3使用GPU训练模型

当参数迭代过程成为训练时间的主要瓶颈时,我们通常的方法是应用GPU来进行加速。深度学习的训练过程常常非常耗时,一个模型训练几个小时是家常便饭,训练几天也是常有的事情,有时候甚至要训练几十天。 训练过程的耗时主要来自于两个部分,一部分来自数据准备,另一部分来自参…

从 0 开始安装和运行 Cangjie (仓颉)

在 Windows 上运行你的第一个仓颉应用程序标准方法 参照 配置开发环境 的描述,下载 Cangjie for DevEco Studio 插件 DevEco Studio NEXT Developer Beta2-Cangjie Plugin(5.0.3.500)。然后在 DevEco Studio NEXT Developer Beta2 中从本地磁盘安装插件,选择下载好的 "d…

启动静态文件中间件

在启动项 Program.cs 文件中添加//app.UseStaticFiles(); // 启动静态文件中间件,默认访问wwwroot app.UseStaticFiles(new StaticFileOptions() {//修改默认访问地址为MyStaticFiles文件FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRoo…

变异系数在fNIRS数据分析中有效性讨论

问题变异系数又称CV,它是无量纲的,定义为概率分布离散程度的归一化程度。在概率论中常常用来作为比较两组数据离散程度的指标,其在fNIRS信号这类连续信号中常常用来检测通道的信号质量好坏,阈值包括0.1、0.15和0.25等,阈值越大越宽松,当某个通道的CV值超过设定的阈值时判…

使用python对Excel表格某个区域保存为图片

实际工作中,我们经常会把表格某个区域(如:A1:F5)或某个图形保存为图片,如何用python自动做到这一点?不知屏幕前的小伙伴有没有遇到过类似的需求,此刻脑海里有木有一丢丢思路。python操作excel的第三方库有很多,个个都有各自的绝招和擅长的应用场景,简单罗列一下:pye…

位段 -- 内存布局详解-浅谈C语言

目录位段的介绍位段使用示例:位段的内存分配Example内存分配解析: ## 位段 位段的介绍位段(二进制位):就是按位存储位段(bit-field)是C语言中的一种特殊数据类型,它允许将一个字节分成几个部分,并为每个部分指定特定的位数,以便在内存中存储和访问这些部分。 其中位段相…

chrome查看web socket消息

1. 打开目标页面 2. f12--》网络--》WS,然后刷新页面( 如果不刷页面,就会看不到 websocket 请求,因为 websocket 是长连接,页面加载后只发出一次连接请求,不像 http 接口,不用刷新页面,待会儿也能看到,因为 http 接口是短连接,调用一次发出一次请求 ),如下所示:

位段 -- 内存布局详解C语言

目录位段的介绍位段使用示例:位段的内存分配Example内存分配解析: ## 位段 位段的介绍位段(二进制位):就是按位存储位段(bit-field)是C语言中的一种特殊数据类型,它允许将一个字节分成几个部分,并为每个部分指定特定的位数,以便在内存中存储和访问这些部分。 其中位段相…

Java流程控制04:循环结构

顺序结构的程序语句只能被执行一次。如果您想要同样的操作执行多次,就需要使用循环结构。 Java中有三种主要的循环结构:while 循环 do…while 循环 for 循环1. while 循环 while是最基本的循环,它的结构为: while (布尔表达式) { // 循环内容 }只要布尔表达式为 true,循环…