概念定义:
在图论中,连通图基于连通的概念。
1. 连通(无向图):
若顶点Vi能通过路径到达Vj,那么称为Vi和Vj是连通的
对无向图:若从顶点Vi到顶点Vj有路径相连(当然从j到i也一定有路径),则称i和j是连通的。
2.强连通和弱连通(有向图)
若顶点Vi和Vj能通过路径单向到达,那么称为Vi和Vj是弱连通的。
若顶点Vi和Vj能通过路径相互到达,那么称为Vi和Vj是强连通的。
3.连通图和强连通图
如果图中任意两点都是连通的,那么图被称作连通图。
如果此图是有向图,任意两点强连通,则称为强连通图 (注意:需要双向都有路径)。
3.连通分量和强连通分量
无向图 G(V,E)中:一个极大连通子图称为 G的一个连通分量(或连通分支)连通图只有一个连通分量,即其自身;非连通的无向图有多个连通分量。
有向图 G=(V,E) 中:一个极大强连通子图称为 G的一个强连通分量。若对于V中任意两个不同的顶点 x和 y,都存在从x到 y以及从 y到 x的路径,则称 G是强连通图。强连通图只有一个强连通分量,即是其自身;非强连通的有向图有多个强连分量。
无向图中,要求得最大连通子图,十分简单,用DSF历遍每一个点,外部再套一层循环即可。但是对于有向图,DFS不能直接求最大连通子图,因为两个节点之间并不是双向联通的,从a->b,不一定可以从b->a,这个时候我们介绍一种新的算法,tarjan
判断一幅图是否连通或强连通
无向图:
对于无向图,只要所有节点可以相互连接即可,用DFS历遍一次,再判断所有的点是否都被访问即可判断复杂度是O(V)
void dfs(int U)
{
visit[s]=1;
for(int i=0;i<G[u].size();i++)//访问子节点
{
int v=G[u][v].id;//取出子节点
if(!visit[v]) dfs(v);
}
}
bool travel()
{
for(int i=1;i<=n;i++)
{
if(!visit) return 0;//不能一次性遍历,说明不连通
}
return 1;//全部访问
}
有向图:
这里有两种方法:
(1)用DFS历遍外层再套一层循环,对每一个点DFS历遍,每次历遍完判断是否有未访问到的,未访问则不强连通,复杂度是O(V^2)
void dfs(int U)
{
visit[s]=1;
for(int i=0;i<G[u].size();i++)//访问子节点
{
int v=G[u][v].id;//取出子节点
if(!visit[v]) dfs(v);
}
}
bool travel2()//判断有向图是否强连通
{
for(int j=1;j<=n;j++)
{
dfs(j);
for(int i=1;i<=n;i++)
{
if(!visit) return 0;//不能一次性遍历,说明不连通
}
fill(visit,visit+maxn,0);
}
return 1;//全部访问
}
(2)tarjan()算法求强连通分量,如果该图只存在一个强连通分量,那么该图强连通
void tarjan(int s)
{
st.push(s);//入栈
visit[s]=1;//标记入栈
dfn[s]=low[s]=index++;//标记时间
for(int i=0;i<G[s].size();i++)
{
int v=G[s][i];//取出子节点
if(!dfn[v])//子节点未访问
{
tarjan(v);
low[s]=min(low[s],low[v]);
}
else if(visit[v])//访问过且已经在栈中
{
low[s]=min(low[s],dfn[v]);
}
}
if(low[s]==dfn[s])//这是最大连通子图的根节点
{
int num=0;
int u;
do
{
u=st.top();//取出首元素
num++;
st.pop();//弹出
visit[u]=0;//同时标记出栈
}
while(s!=u);
ans+=num*(num-1)/2;//C(n,2)个连通点
}
}
for(int i=1;i<=n;i++)
{
if(!dfn[i]) tarjan(i);//当未访问过某个节点是tarja(i)
}
cout<<ans<<endl;
求一幅图的连通分量或强连通分量
(1)无向图也很简单,DFS历遍一次,外部套一层循环,对未访问的节点继续DFS:
void DFS(int u)//dfs历遍判断无向图是否连通
{
visit[u]=1;
cout<<u<<' ';
for(int i=0;i<G[u].size();i++)//访问子节点
{
int v=G[u][v];//取出子节点
if(!visit[v]) dfs(v);
}
}
bool travel3()//求有向图的连通分量
{
for(int i=1;i<=n;i++)
{
if(!visit[i])
{
cout<<endl;
DFS(i);//不能一次性遍历,说明不连通,继续DFS
}
}
}
(2)求有向图的强连通分量tarjan()
首先我们考虑为什么一个图会强连通,这是因为图中含有环,因为有环一个点到达另一个点而另一个点沿着环回路回到这个点。那么我们如何利用这个特点去求最大连通子图呢?
1. 设想如果我们给每一个节点按照访问顺序标记,当沿着某条路访问时一个节点的子节点又出现在这条路中,那么不就形成了一个环吗!!
2. 我们可以给这个最大连通子图标号,设根节点的序号最小,当一个节点的子节点又出现在这条路中我们把这个子节点的标号重置为根节点的编号(最小编号),然后由子节点返回自身的标号,父节点接收到这个标号也重置为当中较小的编号,那么整条回路的标号不就都重置为根节点的标号了吗!!
这样我们可以得到一个连通图,下面是细节:
- 对每一个节点有两层标号,一个是dfn[s],表示标记的时间,一个是low[s]表示根节点的时间序号
- 用栈来存储最大连通子图
- 每次访问一个节点,标记时间dfn[s]=low[s]=index++,入栈,然后访问子节点,如果子节点没有访问过,则tarjan()访问之,并将s点的low[s]重置为min(low[s],low[v]),如果访问过而且在栈中,那么说明找到了环,重置low[s]=min(low[s],low[v]);
- 当if(low[s]==dfn[s])//这是最大连通子图的根节点,弹出s以及s之后所有的节点,表示最大连通子图
这样算法的时间代价是O(V+E)
tarjan()
void tarjan(int s)
{
st.push(s);//入栈
dfn[s]=low[s]=index++;//标记时间
for(访问子节点)
{
if(子节点未访问)
{
tarjan(v);
low[s]=min(low[s],low[v]);
}
else if(访问过且已经在栈中)//存在环
{
low[s]=min(low[s],low[v]);
}
}
if(low[s]==dfn[s])//最大连通子图的根节点
{
弹栈,弹出s以及s之后所有的节点,表示最大连通子图;
}
}
下面是CCF高速公路题的解法
#include <iostream>
#include <algorithm>
#include <vector>
#include <stack>
using namespace std;
const int maxn=1010;
vector<int>G[maxn];
stack<int>st;
int dfn[maxn]={0},low[maxn]={0},visit[maxn]={0};
int time=1;//访问时间
int n,m;//节点数以及边数
void tarjan(int s)
{
st.push(s);//入栈
dfn[s]=low[s]=time++;//新进点的初始化
visit[s]=1;//s在栈里
for(int i=0;i<G[s].size();i++)
{
int v=G[s][i];//取出子节点
if(!dfn[v])//未被标记访问过
{
tarjan(v);
low[s]=min(low[s],low[v]);
}
else if(visit[v])//已经访问并且已经在栈里面//说明成环
{
low[s]=min(low[s],low[v]);//比较访问的先后得出子父关系就是连接对应关系
}
}
if(low[s]==dfn[s]) //是强连通分量的根节点
{
int u;
do
{
u=st.top();
printf("%d ",u);
visit[u]=0;//出栈
st.pop();//弹出
}
while(u!=s);
printf("\n");
}
}
int main()
{
scanf("%d %d",&n,&m);//输入
int u,v;
for(int i=0;i<m;i++)
{
scanf("%d%d",&u,&v);
G[u].push_back(v);//构图
}
for(int i=1;i<=n;i++)
{
if(!dfn[i]) tarjan(i);
}
return 0;
}
/*
6 8
1 2
1 4
2 3
2 5
3 6
4 5
5 1
5 6
*/