有关强联通分量(SCC)

目录:

      *SCC的概念和DFS生成树相关

      *Tarjan算法求强联通分量 

一:SCC的概念和DFS生成树相关

强联通分量:假设有一个有向图G,若其子图G*中任意两点均可达,则称G*是一个SCC

*对于任意孤点,我们也成其是一个SCC

(个人理解:SCC就是有向图中的环 + 孤点)

DFS生成树:即对一个图进行DFS的过程中,依照DFS的顺序构建的一颗生成树

DFS生成树上的边分为两种:树边和非树边,接下来介绍全部的3种非树边

前向边:由祖宗节点连向后代节点的边

横叉边:连接兄弟节点或者兄弟节点的后代的边

返祖边:由后代节点连向祖宗节点的边

     如图:红色是前向边,紫色是横叉边,蓝色是返祖边

      由此,我们可以得到一个重要性质:在DFS生成树中,当且仅当返祖边可以构成环

二:Tarjan算法求SCC

根据刚才的结论,Tarjan算法的核心就是找返祖边,算法具体流程如下:

需要维护的值:

dfn:这个点被打上的时间戳

low:这个点能访问到最小的dfn

st:一个栈,记录每一个未确定所属SCC的点

vis:表示这个点是否在栈中

一:遍历每一个没有被访问过的节点DFS

二:对每个点打上标记:

打上时间戳

初始化low值为这个点本身的时间戳

将这个点入栈

标记这个点在栈中

dfn[u] = ++ cnt;
st[++ top] = u;
vis[u] = 1;
low[u] = cnt;

三:遍历这个点的儿子节点,分三种情况

设这个点是u,儿子是v

1):儿子节点并未被访问过(dfn[v]=0),即u->v是一条树边

那我们直接对v点dfs,同时更新u的low值为min(low[u],low[v]) 

(因为如果v点能回溯到比u更早的节点,那u点一定也可以通过u->v回溯到比自己更早的节点)

2):儿子被访问过且在栈中(vis[v]=1),即u->v是一条返祖边

说明这个时候已经出现了环,我们只需要更新low[u]=min(low[u],low[v]),至于这么做的目的,我们在最后一步操作讲

3):儿子被访问过且不在栈中,即u->v是一条前向边或者横叉边

因为如果是前向边或者横叉边的话,说明这两个点的信息会被他们各自的其他边更新得到,即这条边没有任何价值(这是因为SCC是有向图中的概念,如果是双联通的话就有用了)

for(auto itr:v[u]){
	if(!dfn[itr]){//未访问 
		tarjan(itr);//DFS
		low[u] = min(low[u],low[itr]);
	}
	else if(vis[itr]){//在栈中 
		low[u] = min(low[u],low[itr]);
	}
}

      (我习惯用vector,换成链式前向星也是没有任何问题的,auto就是优化版本的迭代器这里的for就是在遍历所有儿子)

四:弹出栈

并不是每个点进完栈就要立刻弹出栈,当且仅当这个点是一个孤点或者环头(即在一个环中dfn值最小的点)时,需要将其和在它其后进栈的点都弹出栈(这些点在同一SCC内,这样说明它们已经找到了所属SCC,所以弹出栈)

    if(low[u] == dfn[u]){//判定孤点或环头 
		++ num;//给SCC编号 
		while(st[top] != u && top){//同一SCC弹出栈 
			vis[st[top]] = 0;//因为出栈 
			-- top;
		}
		if(st[top] == u) {//环头出栈 
			vis[u] = 0;
			-- top;
		}
	}

好了以上就是Tarjan算法求强联通分量的全部操作了

接下来看一下模版题洛谷P3387

首先我们考虑,如果这个图是DAG应该怎么做:

那就是一个很简单的dp,即dp[v] = max(dp[v],dp[u] + w[v]) (简单的拓扑排序基础我就不说了)

那如果有环呢?很简单,因为每个点经过只算一次,所以我们只需要把每个环都缩成一个点,这个新点的权值和为这个环内所有点的点权。然后这个图就变成了一个DAG,按照刚才的办法做就可以了

附上代码:

#include<bits/stdc++.h>
using namespace std;

#define _for(i,a,b) for(int i = a ; i <= b ; i ++)
#define for_(i,a,b) for(int i = a ; i >= b ; i --)

const int maxn = 1e5 + 10;
int dfn[maxn],low[maxn],vis[maxn],st[maxn],w[maxn],val[maxn],id[maxn],rd[maxn],dist[maxn];
int n,m,tp,cnt,num,ans;
vector<int>v[maxn];
vector<int>v2[maxn];

void tarjan(int u){//SCC缩点
	st[++ tp] = u;
	vis[u] = 1;
	low[u] = dfn[u] = ++ cnt;
	for(auto itr:v[u]){
		if(!dfn[itr]) tarjan(itr),low[u] = min(low[u],low[itr]);
		else if(vis[itr]) low[u] = min(low[u],low[itr]);
	}
	if(dfn[u] == low[u]){
		++ num;
		while(st[tp] != u){
			vis[st[tp]] = 0;
			val[num]+= w[st[tp]];//更新每个缩完的新点权值
			id[st[tp]] = num;//记录每个点所属SCC
			-- tp;
		}
		vis[st[tp]] = 0;
		val[num] += w[st[tp]];
		id[st[tp]] = num;
		-- tp;
	}
}
void topo(){
	queue<int>q;
	_for(i  , 1 , num) {
		if(!rd[i]) q.push(i),dist[i] = val[i];
	}
	while(!q.empty()){
		int u = q.front();
		q.pop();
		for(auto itr:v2[u]){
			rd[itr] --;
			dist[itr] = max(dist[itr],dist[u] + val[itr]);//简单图上DP
			if(rd[itr] == 0) q.push(itr);
		}
	}
	_for(i , 1 , num ) ans = max(ans,dist[i]);
	cout << ans;
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin >> n >> m;
	_for(i , 1 , n) cin >> w[i];
	_for(i , 1 , m){
		int x,y;
		cin >> x >> y;
		v[x].push_back(y);
	}
	_for(i , 1 ,  n){
		if(!dfn[i]) tarjan(i);
	}
	_for(i , 1 , n){
		for(auto itr:v[i]){
			if(id[itr] != id[i]){//如果不在同一SCC中,把这两个SCC连边
				v2[id[i]].push_back(id[itr]);
				rd[id[itr]] ++;
			}
		}
	}
	topo();

	return 0;
}

<think>好的,我现在需要帮助用户找到强连通分量(SCC算法的C语言实现。用户提到了“strongly connected components algorithm”,所以我应该考虑常见的算法,比如Tarjan算法或者Kosaraju算法。根据提供的引用,特别是引用4和5,里面提到了Tarjan算法的证明和核心思想,所以可能用户更倾向于Tarjan算法。 首先,我需要回忆一下Tarjan算法的基本步骤。根据引用5,Tarjan使用深度优先搜索(DFS)和堆栈来追踪顶点,记录每个顶点的深度和最小后向边。当发现某个顶点的low值等于其发现时间时,开始弹出堆栈中的节点,形成一个SCC。 接下来,我需要确保用C语言正确实现这个算法。数据结构方面,图通常可以用邻接表来表示。每个节点需要一个链表来存储其邻接节点。此外,需要一个栈结构来保存当前路径中的节点。还需要记录每个节点的索引(index)、最低链接值(low_link)、是否在栈中的标记等。 然后,考虑具体的函数实现。主函数可能包括初始化图、执行DFS遍历。在DFS过程中,更新每个节点的index和low_link值,并在发现low_link等于index时,弹出栈中的节点作为SCC。 需要注意的是,C语言中没有内置的栈结构,需要手动实现。同时,节点的标记(如已访问、在栈中)可以用数组来维护。还要处理内存分配和释放,避免内存泄漏。 可能遇到的问题包括递归深度过深导致栈溢出,这时候可以考虑改用迭代方式实现DFS。另外,测试用例需要覆盖各种图结构,比如有多个SCC的情况,确保算法正确性。 用户可能需要代码示例,所以需要提供一个简洁但完整的实现,包括图的构建、Tarjan算法的应用和结果输出。同时,要注释关键步骤,帮助用户理解。 最后,根据引用2,SCC的定义是相互可达的节点集合,因此算法必须确保这一点。引用4中的证明提到通过矛盾来确认同一SCC中的节点具有相同的low值,这验证了Tarjan算法的正确性,所以实现时需要确保low值的正确更新。 总结步骤:定义图结构,实现栈,DFS遍历,维护index和low_link,检测SCC并输出。确保代码符合C语言标准,没有语法错误,并进行必要的测试。</think>以下是基于Tarjan算法的强连通分量(SCC)算法C语言实现,该算法时间复杂度为$O(V+E)$,采用深度优先搜索策略[^5]: ```c #include <stdio.h> #include <stdlib.h> #define MAX 100 typedef struct Node { int vertex; struct Node* next; } Node; typedef struct { int index; int low_link; int on_stack; } VertexInfo; Node* graph[MAX]; VertexInfo vertices[MAX]; int stack[MAX], top = -1; int index = 0, scc_count = 0; void add_edge(int u, int v) { Node* newNode = (Node*)malloc(sizeof(Node)); newNode->vertex = v; newNode->next = graph[u]; graph[u] = newNode; } void strongconnect(int v) { vertices[v].index = index; vertices[v].low_link = index++; vertices[v].on_stack = 1; stack[++top] = v; Node* current = graph[v]; while(current != NULL) { int w = current->vertex; if(vertices[w].index == -1) { strongconnect(w); vertices[v].low_link = (vertices[v].low_link < vertices[w].low_link) ? vertices[v].low_link : vertices[w].low_link; } else if(vertices[w].on_stack) { vertices[v].low_link = (vertices[v].low_link < vertices[w].index) ? vertices[v].low_link : vertices[w].index; } current = current->next; } if(vertices[v].low_link == vertices[v].index) { printf("SCC %d: ", ++scc_count); int w; do { w = stack[top--]; vertices[w].on_stack = 0; printf("%d ", w); } while(w != v); printf("\n"); } } void tarjan_scc(int n) { for(int i=0; i<n; i++) { vertices[i].index = -1; vertices[i].on_stack = 0; } for(int i=0; i<n; i++) { if(vertices[i].index == -1) { strongconnect(i); } } } int main() { // 示例图初始化 add_edge(0,1); add_edge(1,2); add_edge(2,0); add_edge(1,3); add_edge(3,4); add_edge(4,5); add_edge(5,3); tarjan_scc(6); // 6个顶点的图 return 0; } ``` 该实现包含以下关键组件: 1. 邻接表存储图结构[^3] 2. 递归DFS遍历记录索引和low值 3. 堆栈跟踪当前路径节点 4. 当发现$low\_link == index$时输出SCC[^4]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值