【图论】割边与桥,双连通分量与强连通分量

割点与桥
割点:
如果去掉一个点以及与它相关的边,使得整个图的连通分支数增加,那么这个点就是一个割点。
桥:如果去掉一条边,使得整个图的连通分支数增加,那么这条边就是一座桥。


tarjan算法求无向图的割点:
定义low[u]为u或u的子树能够追溯到的最早的栈中节点的次序号,dfn[u]为节点u搜索的次序编号(时间戳)那么u是割点当且仅当u满足
①u是dfs搜索树的树根,并且u含有大于等于2棵的子树。
②u不是dfs搜索树的根,并且有不等式low[v]>=dfn[u],其中(u,v)是树枝边。
在这种情况下, low[v]>=dfn[u]成立,也就是说v要到达其祖先必须通过u,否则如果有其他边可以让v到达祖先的话,low[v]显然会比dfn[u]小,那么这时候去掉点u,图的连通分支数一定会增加。 
 
void tarjan(int u,int root,int fa)    
{    
	    DFN[u]=low[u]=++index;    
	    instack[u]=1;    
	    int cnt=0;    
	    for(int i=head[u];i!=-1;i=edge[i].next)    
	    {    
	        int v=edge[i].to;    
	        if(!instack[v])    
	        {    
	            tarjan(v,root,u);    
	            cnt++;    
	            if(low[u]>low[v])//如果当前结点的low值大于子节点的low值,说明子节点连到到比当前结点更早的节点,则更新当前结点的low值跟子节点一样。 
                low[u]=low[v];    
	            if(u==root && cnt>1)//如果u等于根结点并且有2个子树,则为割点。这句也也可以放在循环外,由于可能会进入多次,所以最后如果是割点的话增加的连通分量可能不止1个。即根节点有多个子树。 
	                {
	                	cut_point[u]=1; 
						add_block[u]++;	
					}    
	            else if(u!=root && low[v]>=DFN[u])//如果u不等于根节点并且子节点可以回到的最早的点在该点之后 
	                {
						cut_point[u]=1;    
						add_block[u]++;
					}
	        }    
	        else if(v!=fa && low[u] > DFN[v])//如果有边回到了之前的点,则更新该起始点的low值 
	            low[u]=DFN[v];    
	    }
} 



tarjan算法求无向图的桥:

边(u,v)是无向图的桥当且仅当(u,v)满足 low[v]>DFN[u],也就是边(u,v)是v到达其祖先的必经之路,所以去掉边(u,v),连通分支数就会增加,但是如果u,v之间存在重边的话,就不是桥了,所以我们要对重边判定

void Tarjan(int u,int pre)
{
    int v;
    Low[u] = DFN[u] = ++Index;
    Stack[top++] = u;
    Instack[u] = true;
    int son = 0;
    for(int i = head[u];i != -1;i = edge[i].next)
    {
        v = edge[i].to;
        if(v == pre)continue;
        if( !DFN[v] )
        {
            son++;
            Tarjan(v,u);
            if(Low[u] > Low[v])Low[u] = Low[v];
            //桥
            //一条无向边(u,v)是桥,当且仅当(u,v)为树枝边,且满足DFS(u) DFN[u])
            {
                bridge++;
                edge[i].cut = true;
                edge[i^1].cut = true;
            }
            //割点
            //一个顶点u是割点,当且仅当满足(1)或(2) (1) u为树根,且u有多于一个子树。
            //(2) u不为树根,且满足存在(u,v)为树枝边(或称父子边,
            //即u为v在搜索树中的父亲),使得DFS(u)<=Low(v)
            if(u != pre && Low[v] >= DFN[u])//不是树根
            {
                cut[u] = true;
                add_block[u]++;
            }
        }
        else if( Low[u] > DFN[v])
             Low[u] = DFN[v];
    }
    //树根,分支数大于1
    if(u == pre && son > 1)cut[u] = true;
    if(u == pre)add_block[u] = son - 1;
    Instack[u] = false;
    top--;
}



tarjan算法求无向图的双连通分量:
在无向连通图中,如果删除该图的任何一个结点都不能改变该图的连通性,则该图为双连通的无向图。
双连通分量里任何2个顶点之间都至少有2条不相交的路径,所有low相等的点位于同一个双连通分量中。 


例题: POJ 3177 给定一个连通的无向图G,至少要添加几条边,才能使其变为双连通图。
#include 
#include 
#include 
#include 
#include 
using namespace std;
const int MAXN = 5010;//点数
const int MAXM = 20010;//边数,因为是无向图,所以这个值要*2
struct Edge
{
    int to,next;
    bool cut;//是否是桥标记
}edge[MAXM];
int head[MAXN],tot;
int Low[MAXN],DFN[MAXN],Stack[MAXN],Belong[MAXN];//Belong数组的值是1~block
int Index,top;
int block;//边双连通块数
bool Instack[MAXN];
int bridge;//桥的数目


void addedge(int u,int v)
{
    edge[tot].to = v;edge[tot].next = head[u];edge[tot].cut=false;
    head[u] = tot++;
}
void Tarjan(int u,int pre)
{
    int v;
    Low[u] = DFN[u] = ++Index;
    Stack[top++] = u;
    Instack[u] = true;
    for(int i = head[u];i != -1;i = edge[i].next)
    {
        v = edge[i].to;
        if(v == pre)continue;//判断重边 
        if(!DFN[v])//如果没被访问 
        {
            Tarjan(v,u);
            if( Low[u] > Low[v] )Low[u] = Low[v];//访问完一个子树后更新当前low,看子树能不能到达更前的节点。 
            if(Low[v] > DFN[u])//孩子节点能到达的最早的点在当前点之后,说明孩子节点无法绕过u,v边到达之前的节点,即u,v为桥 
            {
                bridge++;
                edge[i].cut = true;
                edge[i^1].cut = true;//i^1,一种技巧,当i为偶数时,i^1相当于i+1,当i为奇数时,相当于i-1。因为我们每次增加2边,下标0,1为同一条边,2,3为同一条边,如果i=0,i^1就等于1,如果i=1,i^1就等于0,保证了将同一条边标记为桥。 
            }
        }
        else if( Instack[v] && Low[u] > DFN[v] )//如果被访问了,说明找到了一条回去的边,如果当前点的low大于终点的dfn,更新low值 
            Low[u] = DFN[v];
    }
    if(Low[u] == DFN[u])//如果low[u]==dfn[u],说明找到双连通分量 
    {
        block++;
        do
        {
            v = Stack[--top];
            Instack[v] = false;
            Belong[v] = block;
        }
        while( v!=u );
    }
}
void init()
{
    tot = 0;
    memset(head,-1,sizeof(head));
}


int du[MAXN];//缩点后形成树,每个点的度数
void solve(int n)
{
    memset(DFN,0,sizeof(DFN));
    memset(Instack,false,sizeof(Instack));
    Index = top = block = 0;
    Tarjan(1,0);
    int ans = 0;
    memset(du,0,sizeof(du));
    for(int i = 1;i <= n;i++)
       for(int j = head[i];j != -1;j = edge[j].next)
          if(edge[j].cut)//因为存的是边而不是点. 
             du[Belong[i]]++;
    for(int i = 1;i <= block;i++)
       if(du[i]==1)
          ans++;
    //找叶子结点的个数ans,构造边双连通图需要加边(ans+1)/2
    printf("%d\n",(ans+1)/2);
}
int main()
{
    int n,m;
    int u,v;
    while(scanf("%d%d",&n,&m)==2)
    {
        init();
        while(m--)
        {
            scanf("%d%d",&u,&v);
            addedge(u,v);
            addedge(v,u);
        }
        solve(n);
    }
    return 0;
}


tarjan算法求有向图的强连通分量:
 在一个图的子图中,任意两个点相互可达,也就是存在互通的路径,那么这个子图就是强连通分量(或者称为强连通分支)。
 如果一个有向图的任意两个点相互可达,那么这个图就称为 强连通图。求强连通分量的过程跟之前类似。当dfn[u]==low[u]时,从栈顶到u的节点都属于同一个强连通分量,这时候不断出栈直到弹出u即可。 

 

const int maxn=10005;  
int DFN[maxn];//记录每个点被访问到的时间  
int low[maxn];//记录点可以直接或间接到达的最早被访问到的点(也就是那个强连通分量的根)  
int stack[maxn];  
int sccnum[maxn];//标记每个点属于第几个强连通分量  
bool instack[maxn];  
int sccNum;//强连通分量的数目  
int top;  
int index;  
int n;  
struct node  
{  
    int to;  
    int next;  
}edge[10*maxn];  
int head[maxn];  
int tot;  
void addedge(int from,int to)  
{  
    edge[tot].to=to;  
    edge[tot].next=head[from];  
    head[from]=tot++;  
}  
void tarjan(int u)  
{  
    DFN[u]=low[u]=++index;//刚刚搜到这个点,DFN和low都赋值为被访问到的时间  
    stack[top++]=u;//入栈  
    instack[u]=1;  
    for(int j=head[u];j!=-1;j=edge[j].next)  
    {  
    	int v=edge[j].to;
        if(!DFN[v])//如果没有被访问过  
        {  
            tarjan(v);  
            if(low[u]>low[v]) low[u]=low[v]; //这个时候low可能要修改,值为i或者i的子树可以到达的最早被访问到的点的时间  
        }  
        else if(instack[v])//已经在栈  
        {  
            if(low[u]>DFN[v])  low[u]=DFN[v];  
        }  
    }  
    if(DFN[u]==low[u])//找到根  
    {  
        sccNum++;  
        do  
        {  
            int v=stack[--top];  
            sccnum[v]=sccNum;  
            instack[v]=0;//标记出栈  
        }while(v!=u);  
    }  
}  
void solve()  
{  
    memset(DFN,0,sizeof(DFN));  
    memset(instack,0,sizeof(instack));  
    index=0;  
    sccNum=0;  
    top=0;  
    for(int i=1;i<=n;i++)  
        if(!DFN[i])  
            tarjan(i);  
}  
  
int main()  
{  
    int m,a,b;  
    while(~scanf("%d%d",&n,&m))  
    {  
        if(n==0 && m==0)  
            break;  
        memset(head,-1,sizeof(head));  
        tot=0;  
        for(int i=0;i


你可能感兴趣的:(算法)