3.4 强连通分量
部分来自:上总介的博客:图论——强连通分量
强连通分量
- 强连通图
- 如果一个有向图中,存在一条回路,所以节点至少被经过一次,这样的图称为强连通图
- 强连通分量
- 在强连通图的基础上加上一些节点和路径,使得当前的图不再强连通,则原来的强连通部分为强连通分量
Tarjan求强连通分量
基本思路
- 由强连通分量的性质可知,在以dfs序遍历图时,当且仅当该节点指向的边存在标记时存在回环,这个回环就构成了一个强连通分量
- 如何知道该强连通分量中的节点都是谁呢?
- 可以使用栈储存走过的节点,当有回环时进行弹栈操作直至弹出元素恰好是起始节点,这样就储存了一个强连通分量
- 时间复杂度O(N+E)
代码实现
- 储存:
- dfn数组:记录搜索顺序
- low数组:记录该节点所能回溯到的最浅深度,相当于所在强连通分量的序号(初始化时每个强连通分量孤立)
- instack数组:记录该节点是否入栈
- 栈:记录每个强连通分量里的节点
- 搜索:
- 使用dfs遍历整张图,标记每个走过的节点(在实现过程中,可以使用dfn数组是否有值标记是否走过)
- 第一步:每访问到一个节点,给它打上时间戳,初始化dfn、low,并将该节点入栈
++cnt;
dfn[u]=low[u]=cnt;
s.push(u);
ins[u]=1;
- 第二步:更新low数组。从该节点向下遍历,当遇到无标记点时,访问该点,当搜索结束回溯到该节点时,尝试加入子节点所在的强连通分量;当有标记且已经入栈时,证明该点已被访问但没有存在于子的强连通分量中,也要尝试加入子节点所在的强连通分量中;其余情况不应进行操作
- 通过更新到更浅的强连通分量方法尝试加入子节点所在的强连通分量:
low[u]=min(low[u],low[v])
for(int i:edge[u]){
int v=i;
if(!dfn[v]){
Tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(ins[v])
low[u]=min(low[u],low[v]);
}
- 第三步:储存强连通分量。当当前节点的dfs序(dfn)==强连通分量(low)时,证明该点为一个强连通分量的起始元素,开始弹栈,储存强连通分量
if(dfn[u]==low[u]){
++cntb;
int node;
do{//强连通分量至少是一个点,所以至少弹一次
node=s.top();
s.pop();
ins[node]=0;
belong[cntb].push_back(node);
}while(node!=u);
}
- 这样就求出来啦
- 时间复杂度O(n+m),因为每个点、边只会被遍历一次,每个点也只会进出栈一次
完整代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,m,cnt,cntb;
vector<int>edge[101];
vector<int>belong[101];
bool ins[101];
int dfn[101];
int low[101];
stack<int>s;
void Tarjan(int u){
++cnt;
dfn[u]=low[u]=cnt;
s.push(u);
ins[u]=1;
for(int i:edge[u]){
int v=i;
if(!dfn[v]){
Tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(ins[v])
low[u]=min(low[u],low[v]);
}
//low数组更新好之后判断强连通分量
if(dfn[u]==low[u]){
++cntb;
int node;
do{//强连通分量至少是一个点,所以至少弹一次
node=s.top();
s.pop();
ins[node]=0;
belong[cntb].push_back(node);
}while(node!=u);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
edge[u].push_back(v);
}
Tarjan(1);
printf("%d\n",cntb);
for(int i=1;i<=cntb;i++){
printf("%d ",belong[i].size());
for(int j:belong[i]){
printf("%d ",j);
}
puts("");
}
return 0;
}
例题
例题1:有向图缩点
1. 思路
- 很容易想到做一个dp,用
f
i
f_i
fi表示走到i节点的最大权值和
d p v = m a x ( d p v , d p u + w e i g h t v ) , v ∈ s o n u dp_v=max(dp_v,dp_u+weight_v),v\in son_u dpv=max(dpv,dpu+weightv),v∈sonu - 时间复杂度为O(nm)
- 考虑优化:
- 显然,对于一个强连通分量中的节点,走过它们时走遍强连通分量是最优的
- 所以首先将强连通分量缩为一个节点
- 因为新图一定是DAG,所以接着进行拓扑排序
拓扑排序:
- 对有向无环图(DAG)的一种线性遍历顺序,其保证对于每一条边 ( x , y ) (x,y) (x,y),拓扑序列中x总在y之前
- 实现:统计所有节点入度,以入度为0的节点为始点,每次找到一个入度为0的节点时将其加入队列,删去该节点与连接它的边,继续寻找入度为0的节点直至所有点都被删除
- 上述遍历方法即按拓扑序遍历
//拓扑排序
queue<int>q;
for(int i=1;i<=tot;i++){
if(!ru[i]) q.push(i),f[i]=sum[i];
}
while(!q.empty()){
int x=q.front();
q.pop();
for(int v:e2[x]){
f[v]=max(f[v],f[x]+sum[v]);
if(!--ru[v]){
q.push(v);
}
}
}
- 最后对新形成的图求解即可
- 时间复杂度O(n+m)
2. 如何维护缩点后的新权值 - 引入一种新的储存强连通分量的方法:
- 定义数组col[i]表示i点所在的强连通分量的编号(与low[i]不同),tot用于记录强连通分量个数
- 弹栈时,将col[x]设为++tot,并将弹出的每个节点v的col赋值为tot即可
if(dfn[x]==low[x]){ col[x]=++tot; while(x!=s.top()){ int v=s.top(); col[v]=tot; s.pop(); } s.pop();//注意这里要多pop一次,即把早已被统计的x弹出 }
- 在上述过程中,更新col时同时更新sum即可
3. 代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+100;
int dfn[N],low[N],col[N],n,m,tot,Time;
vector<int>e[N];
stack<int>s;
int w[N],sum[N],ru[N],f[N];
vector<int>e2[N];
inline void Tarjan(int x){
dfn[x]=++Time;
low[x]=Time;
s.push(x);
for(int v:e[x]){
if(!dfn[v]){
Tarjan(v);
low[x]=min(low[x],low[v]);//利用子节点更新
}else if(!col[v]){
low[x]=min(low[x],low[v]);//尝试将孤立点加入
}
}
if(dfn[x]==low[x]){
col[x]=++tot;
sum[tot]+=w[x];
while(x!=s.top()){
int v=s.top();
col[v]=tot;
sum[tot]+=w[v];
s.pop();
}
s.pop();//注意这里要多pop一次,即把早已被统计的x弹出
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&w[i]);
}
for(int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
e[a].push_back(b);
}
for(int i=1;i<=n;i++){
if(!dfn[i])
Tarjan(i);
}
for(int i=1;i<=n;i++){//合并强连通
for(int v:e[i]){
if(col[i]!=col[v]){
e2[col[i]].push_back(col[v]);
ru[col[v]]++;
}
}
}
//拓扑排序
queue<int>q;
for(int i=1;i<=tot;i++){
if(!ru[i]) q.push(i),f[i]=sum[i];
}
while(!q.empty()){
int x=q.front();
q.pop();
for(int v:e2[x]){
f[v]=max(f[v],f[x]+sum[v]);
if(!--ru[v]){
q.push(v);
}
}
}
int ans=0;
for(int i=1;i<=tot;i++){
ans=max(ans,f[i]);
}
printf("%d\n",ans);
return 0;
}
例题2:受欢迎的牛G
- 思路:
- 根据喜欢关系,我们可以建出一张有向图,a->b表示a喜欢b
- 观察此图,我们发现一个强连通分量中的每两个点之间都互相喜欢,可以缩为一个点
- 对于缩点之后的新图,我们发现,出度为0的强连通分量中的所有牛都可以当明星,但如果存在多个出度为0的点,则没有人能当,因为这几个强连通分量之间不互相喜欢
- 代码
#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 10;
int dfn[N], low[N], col[N], ot[N], sum[N];
int T, tot, n, m, ans;
vector<int> e1[N];
stack<int> s;
void tarjan(int x) {
dfn[x] = ++T;
low[x] = T;
s.push(x);
for (auto v : e1[x]) {
if (!dfn[v]) {
tarjan(v);
low[x] = min(low[x], low[v]);
} else if (!col[v]) {
low[x] = min(low[x], low[v]);
}
}
if (dfn[x] == low[x]) {
col[x] = ++tot;
sum[tot]++;
while (x != s.top()) {
int v = s.top();
s.pop();
col[v] = tot;
sum[tot]++;
}
s.pop();
}
return;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int a, b;
cin >> a >> b;
e1[a].push_back(b);
}
for (int i = 1; i <= n; i++) {
if (!dfn[i])
tarjan(i);
}
for (int i = 1; i <= n; i++) {
for (auto v : e1[i]) {
if (col[i] != col[v]) {
ot[col[i]]++;
}
}
}
ans=0;
for (int i = 1; i <= tot; i++) { //枚举强连通分量
//cerr << ot[i];
if (ot[i] == 0) {
if (!ans) {
ans = sum[i];
// cerr<<sum[i]<<' ';
} else {
ans = 0;
// break;
}
}
}
printf("%d\n", ans);
return 0;
}