一年之计在于春,一天之计在于晨
内容安排:图的表示
图的强连通分量算法:
Kosaraju算法
Tarjan算法
Gabow算法
图的实现 (1) 邻接矩阵! 开一个二维数组 G。G[i][j]表示边(i,j)的权。如果边(i,j)不存在,就令 G[i][j]=INF(当然,(i,i)不 是一条边,G[i][i]也等于 INF)。邻接矩阵最大的缺点就是内存空间占用太大,内存浪费严重。 (2) 边目录! 设置三个数组 u[M]、v[M]、w[M],分别表示起点、终点和权。一般情况下,从文件中读取的图都是用边目录来表示的。(3) 邻接表(链表)! 用一个列表列出所有与现结点之间有边存在的结点名称。
struct edge
{
int u,v,w;
struct edge *next;
} mem[M]; // mem相当于动态内存分配。
int size=-1;
#define NEW(p) p=&mem[++size]; p->next=NULL
struct edge *adj[N]; // adj[i]代表以i为起点的边。
……
memset(adj, 0, sizeof(adj));
for (int e=0; e
{
struct edge *p;
NEW(p);
cin>>(p->u)>>(p->v)>>(p->w);
p->next=adj[p->u];
adj[p->u]=p;
}
如果想检查从 a 出发的所有边,那么可以:
for (edge *e=adj[a]; e!=NULL; e=e->next)
{
// e->u是起点,e->v是终点,e->w是权
}
(4) 邻接表(静态数组)! 注意,在这个“邻接表”里放置的元素是边的序号,不是点的序号。所以还要和边目录配合使用.
int first[N]; // first[u]表示从u出发的第一条边的序号
int u[M],v[M],w[M], next[M]; // next[e]表示编号为e的下一条边的序号
memset(first, -1, sizeof(first));
for (int e=0; e
{
cin>>u[e]>>v[e]>>w[e];
next[e]=first[u[e]]; // 插入一条边
first[u[e]]=e;
}
如果想检查从 a 出发的所有边,那么可以:
for (int e=first[a]; e!=-1; e=next[e])
{
// u[e]是起点,v[e]是终点,w[e]是权
}
(5) 邻接表(STL)! 头文件:这种邻接表也要和边目录配合使用。只需把(4)中的代码换成以下代码:
vector<int> g[N]; // g[u][i]表示从u出发的第i条边的序号
int u[M],v[M],w[M]; // 同样要和边目录配合使用
for (int e=0; e<m; e++)
{
cin>>u[e]>>v[e]>>w[e];
g[u].push_back(e);
}
如果想检查从 a 出发的所有边,那么可以:
for (int i=0; i
{
int &e=g[a][i];
// u[e]是起点,v[e]是终点,w[e]是权
}
强连通分量的定义: 有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。 如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
int dfn[N], top;
int color[N], cnt;
void dfs1(int k)
{
color[k] = 1;
for(int i=0; i
if(G[k][i]!=INF && !color[i])
dfs1(i);
dfn[top++] = k; // 出栈顺序逆序,记录第top个出栈的顶点为k
}
void dfs2(int k)
{
color[k] = cnt; // 本次DFS染色的点,都属于同一个scc,用num数组做记录
for(int i=0; i
if(G[i][k]!=INF && !color[i]) // 注意,我们在访问原矩阵的对称矩阵
dfs2(i);
}
int Kosaraju() // 返回强连通分量个数
{
top=cnt=0;
memset(color, 0, sizeof(color));
for(int i=0; i// DFS求得拓扑排序
if(!color[i]) dfs1(i);
/*
我们本需对原图的边反向,但由于我们使用邻接矩阵储存图,
所以反向的图的邻接矩阵,即原图邻接矩阵的对角线对称矩阵,
所以我们什么都不用做,只需访问对称矩阵即可
*/
memset(color, 0, sizeof(color));
for(int i=n-1; i>=0; i--)
if(!color[dfn[i]]){ // 按照拓扑序进行第二次DFS
cnt++;
dfs2(dfn[i]);
}
return cnt;
}
强连通分量(Tarjan 算法) [邻接表] 该算法的效率要高于 Kosaraju 算法。任何一个强连通分量,必定是对原图的深度优先搜索树的子树(记住这句话)。那么,我们只要确定每个 强连通分量的子树的根,然后根据这些根从树的最低层开始,一个一个的拿出强连通分量。我们维护两个数组,一个是 index,一个是 low。其中 index[i]表示顶点 i 的开始访问时间。low[i]是 此节点能够到达的最前面的位置,初始化为 index[i],维护时 low[i]取它与 low[j]的最小值,其中 j 是与顶点 i 邻接但未删除 的顶点。在一次深搜的回溯过程中,如果发现 low[i]=index[i],那么,当前顶点就是一个强连通分量的根(因 为如果它不是强连通分量的根,那么它一定是属于另一个强连通分量,而且它的根是当前顶点的祖宗,那么存 在包含当前顶点的到其祖宗的回路,可知 low[i]一定被更改为一个比 index[i]更小的值)。拿出强连通分量的方法很简单。如果当前结点为一个强连通分量的根,那么它的强连通分量一定是以该根 为根结点(剩下结点)的子树。在深度优先遍历的时候维护一个堆栈,每次访问一个新结点,就压入堆栈。因为当前结点是这个强连通分量中最先被压入堆栈的,那么在当前结点以后压入堆栈的并且仍在堆栈中的 结点都属于这个强连通分量。 算法实现——对于所有未访问的结点 x,都进行以下操作:① 初始化 index[x]和 low[x];② 对于 x 所有的邻接顶点 v:如果没有访问过,则用同样方法访问 v,同时维护 low[x];如果访问过,但没有删除,就维护 low[x]。③ 如果 index[x]=low[x],那么输出相应的强连通分量
enum _flag { NOTVIS=0, VIS, OVER } flag[N];
// NOTVIS、VIS、OVER分别表示顶点没有被访问过、顶点被访问过但未删除、顶点已被删除的状态。
int color[N]; // color[i]表示顶点i所属的强连通分量
int stack[N], top; // 堆栈,辅助作用
int low[N]; // 很关键,与其邻接但未删除顶点的最小访问时间
int index[N]; // 顶点访问时间
void DFS(int x, int &sig, int &count) // 深搜过程,该算法的主体都在这里
{
stack[++top] = x;
flag[x] = VIS;
low[x] = index[x] = ++sig;
for (edge *e=adj[x]; e!=NULL; e=e->next)
{
int &v=e->v;
if (flag[v]==NOTVIS)
{
DFS(v, sig, count);
if (low[v]
}
else if (flag[v]==VIS && index[v]
low[x]=index[v];
// 该部分的index应该是low,但是根据算法的属性,使用index也可以,且时间更少
}
if (low[x]==index[x])
{
count++;
int t;
do
{
t=stack[top--];
color[t] = count;
flag[t] = OVER;
} while (t!=x);
}
}
int Tarjan()
{
int sig, count;
memset(flag, 0, sizeof(flag));
sig=count=top=0;
for (int i=0; i
if (flag[i]==NOTVIS)
DFS(i,sig,count);
return count;
}
强连通分量(Gabow 算法) [邻接表]
Gabow 算法与 Tarjan 算法的核心思想实质上是相通的,就是利用强连通分量必定是 DFS 的一棵子树这 个重要性质,通过找出这个子树的根来求解强分量。 具体实现是利用一个栈 S 来保存 DFS 遇到的所有树边的另一端顶点,在找出强分量子树的根之后,弹出 S 中的顶点一一进行编号。 与 Tarjan 算法不同的是,Tarjan 算法通过一个 low 数组来维护各个顶点能到达的最小前序编号,而 Gabow 算法通过维护另一个栈来取代 low 数组,将前序编号值更大的顶点都弹出,然后通过栈顶的那个顶点 来判断是否找到强分量子树的根
。
int pre[N];
int color[N];
int S[N], P[N]; // 两个栈,S用来保存所有结点,P用来维护路径
int top_s, top_p;
int cnt, id;
void DFS(int x)
{
int v;
pre[x] = cnt++; // 对前序编号编号
S[++top_s]=x; // 将路径上遇到的树边顶点入栈
P[++top_p]=x;
for (edge *e=adj[x]; e!=NULL; e=e->next)
{
v=e->v;
if (pre[v] == -1) // 如果以前未遇到当前顶点,则对其进行DFS
DFS(v);
else if (color[v] == - 1) // 如果当前顶点不属于强分量,
while (pre[P[top_p]] > pre[v]) // 就将路径栈P中大于当前顶点pre值的顶点都弹出
top_p--;
}
if (P[top_p] == x) // 如果P栈顶元素等于x,则找到强分量的根——x
{
top_p--;
id++;
do
{
v = S[top_s--]; // 把S中的顶点弹出编号
color[v] = id;
} while (v != x);
}
}
int Gabow()
{
top_s=top_p=-1;
memset(pre,-1,sizeof(pre));
memset(color,-1,sizeof(color));
cnt=id=0;
for (int v=0; v
if (pre[v] == -1)
DFS(v);
return id; // 返回id的值,这恰好是强连通分量的个数
}
补充:
1. 判断两点是否连通:
方法一:使用 Floyd 算法,时间复杂度 O(n^3)
方法二:从起点出发,使用 DFS 遍历,可以找到与它连通的其他点。时间复杂度 O(n^2)
方法三:(仅用于无向图)使用并查集。时间复杂度 O(an)(a 是一个小常数)。
bool f[N][N]; // 如果存在一条从i出发,到j结束的路径,f[i][j]=true。
……
// 预处理——可以在读图时完成
for (int i=0; i<n; i++)
for (int j=0; j<n; j++)
f[i][j] = (G[i][j]!=INF);
for (int k=0; k<n; k++)
for (int i=0; i<n; i++)
for (int j=0; j<n; j++)
f[i][j] = f[i][j] || (f[i][k] && f[k][j]);