看了许多博客,还是这个最清楚:强连通算法–Tarjan个人理解+详解
墙裂安利大家看看上面的博客!讲的非常清楚!(但是文章的代码好像有一点点小错误。
Tarjan算法主要解决了有向图中有几个强连通分量的问题,基于Tarjan算法,可以对在同一连通分量的点染色,进而缩点,生成有向无环图。
例:给定有向图,求:
- 至少要选几个顶点,才能做到从这些顶点出发,可以到达全部顶点
- 至少要加多少条边,才能使得从任何一个顶点出发,都能到达全部顶点
思路 缩点之后, DAG上面有多少个入度为0的顶点,问题1的答案就是多少;在DAG上要加几条边,才能使得DAG变成强连通的,问题2的答案就是多少。
加边的方法:
假定有 n 个入度为0的点,m个出度为0的点,如何加边?
把所有入度为0的点编号 0,1,2,3,4 …N -1
每次为一个编号为i的入度0点可达的出度0点,添加一条出边,连到编号为(i+1)%N 的那个入度0点,这需要加n条边。
若 m <= n,则加了这n条边后,已经没有入度0点,则问题解决,一共加了n条边
若 m > n,则还有m-n个入度0点,则从这些点以外任取一点,和这些点都连上边,即可,这还需加m-n条边。
所以,max(m,n)就是第二个问题的解
附上代码:
#include<stdio.h>//代码仅供参考
#include<string.h>
#include<vector>
#include<algorithm>
#include<stack>
using namespace std;
#define maxn 1000000
struct E{
int next,v;
}edge[maxn];
int head[maxn],k;
int vis[maxn];
int DFN[maxn],LOW[maxn],cnt,sig,color[maxn];
int in_cnt,out_cnt,in[maxn],out[maxn];
int n,m;
stack<int>S;
void init(){
memset(head,-1,sizeof(head));
memset(vis,0,sizeof(vis));
memset(DFN,0,sizeof(DFN));
memset(LOW,0,sizeof(DFN));
memset(color,0,sizeof(color));
memset(in,0,sizeof(in));
memset(out,0,sizeof(out));
while(!S.empty())S.pop();
k = 0,cnt = 0,sig = 0;
}
void add(int u,int v){//链式前向星
edge[++k].next = head[u];
edge[k].v = v;
head[u] = k;
}
void Tarjan(int u){
DFN[u] = LOW[u] = ++cnt;
vis[u] = 1;
S.push(u);
for(int i = head[u];i != -1;i = edge[i].next){
int v = edge[i].v;
if(!DFN[v]){
Tarjan(v);
LOW[u] = min(LOW[u],LOW[v]);
}
else if(vis[v] == 1){
LOW[u] = min(LOW[u],LOW[v]);
}
}
if(DFN[u] == LOW[u]){
sig++;
do{
int x = S.top();
color[x] = sig;
S.pop();
vis[x] = -1;
printf("%d",x);
if(x == u)break;
}while(1);
printf("\n");
}
}
void Slove(){
for(int i = 1; i <= n;++i){
if(vis[i] == 0)Tarjan(i);
}
printf("强连通分量共%d个\n",sig);
}
//1) 至少要选几个顶点,才能做到从这些顶点出发,可以到达全部顶点
//2) 至少要加多少条边,才能使得从任何一个顶点出发,都能到达全部顶点
void scc(){
if(sig == 1){
printf("%d %d\n",1,0);
}
else{
for(int i = 1; i <= n; ++i){
for(int j = head[i]; j != -1;j = edge[j].next){
int v = edge[j].v;
if(color[i] != color[v]){
in[color[v]]++;
out[color[i]]++;
}
}
}
for(int i = 1; i <= sig;++i){
if(!in[i])in_cnt++;
if((!out[i]))out_cnt++;
}
printf("至少要选 %d 个顶点,才能从这些顶点出发,可达全部顶点\n至少要加 %d 条边,使得从任何一个顶点出发,到达全部顶点\n",in_cnt,max(in_cnt,out_cnt));
}
}
int main()
{
while(~scanf("%d",&n)){
scanf("%d",&m);
init();
for(int i = 1; i <= m;++i){
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
}
Slove();
for(int i = 1; i <= n;++i){
printf("颜色%d,结点%d\n",color[i],i);
}
scc();
}
}
一组测试用例
相关练习:
POJ2186找大牛
POJ1236
强连通图缩点的应用:最小点基
题目大意:
给你N个炸弹,对应已知其坐标和爆炸范围,以及引爆这个炸弹需要的花费,对应如果引爆了炸弹a,没有引爆炸弹b,但是b炸弹在a炸弹的作用范围之内,那么b炸弹也会被引爆,问将所有炸弹都引爆需要的最小花费。
思路:把可以引爆的炸弹间连边,建立有向图;如果有环,使用Tarjan算法缩点染色(同环内可以相互引爆);找到入度为0的分量,找出其中花费最小的点作为引爆这个分量及其相连分量的点累加。
综上所述,Tarjan强连通缩点染色之后,找到度为0的节点,并且在其中找到花费最小的炸弹,累加即可。
具体的:在DFN[u] == LOW[v]时更新分量的最小花费
关于最小点基和最小权点基:最小点基,最小权点基(Summer Holiday HDU - 1827)(强连通分量的应用)