zoukankan      html  css  js  c++  java
  • 【图论】割点、桥、双连通

    连通分量

    个数可以通过一次BFS或者DFS得到


    割点和桥

    可以枚举删除每一个点或者每一条边,判断连通分量个数是否增加


    更好的方法

    该算法是R.Tarjan发明的。对图深度优先搜索,定义DFS(u)为u在搜索树(以下简称为树)中被遍历到的次序号。定义Low(u)为u或u的子树中能通过非父子边追溯到的最早的节点,即DFS序号最小的节点。

    一个顶点u是割点,当且仅当满足(1)或(2) 

    (1) u为树根,且u有多于一个子树。 

    (2) u不为树根,且满足存在(u,v)为树枝边(或称父子边,即u为v在搜索树中的父亲),使得DFS(u)<=Low(v)


    一条无向边(u,v)是桥,当且仅当(u,v)为树枝边,且满足DFS(u)<Low(v)

    void dfs(int u,int fa)
    {
    	int child=0;
    	pre[u]=low[u]=++tim;
    	for(int e=fst[u];e!=-1;e=nxt[e])
    	{
    		int v=s[e].y;
    		if(!pre[v])
    		{
    			child++;
    			dfs(v,u);
    			low[u]=min(low[u],low[v]);
    			if(low[v]>=pre[u])cut[u]++;
    			if(low[v]>pre[u])bridge[e]=1;
    		}
    		else
    			if(pre[v]<pre[u] && v!=fa)	//v!=fa
    			low[u]=min(low[u],pre[v]);
    	}
    	if(fa<0&&child==1)cut[u]=0;
    }


    需要注意的是:

    1.如果i是root,那么去掉i后,连通分量个数增加cut[i]-1个。如果i不是root,那么连通分量增加cut[i]个

    2.无向图  bridge[e]=1  bridge[e+1]同样应标注为1(bridge[e-1])


    双连通

    双连通分点双联通和边双联通

    对于一个连通图,若任意两点至少存在两条“点不重复”的路径,则此连通图是点-双连通的,意味着任意的两条边都是在同一个简单环上,即内部无割顶。点-双连通的极大子图叫做双连通分量或块。定理:不同双联通分量最多只有一个公共点,且它一定是割顶,任意割顶都是至少两个不同双连通分量的公共点。

    对于一个连通图,若任意两点至少存在两条“边不重复”的路径,则此连通图是边-双连通的,意味着只需要每条边都至少在一个简单环中,即所有边都不是桥。边-双连通的极大子图叫做边-双连通分量,除了桥不属于任何边-双连通分量之外,其他每条边恰好属于一个边-双连通分量,而且把所有桥删除之后,每个连通分量对应原图中的一个边-双连通分量。


    对于点双连通分支,实际上在求割点的过程中就能顺便把每个点双连通分支求出。建立一个栈,存储当前双连通分支,在搜索图时,每找到一条树枝边或后向边(非横叉边),就把这条边加入栈中。如果遇到某时满足DFS(u)<=Low(v),说明u是一个割点,同时把边从栈顶一个个取出,直到遇到了边(u,v),取出的这些边与其关联的点,组成一个点双连通分支。割点可以属于多个点双连通分支,其余点和每条边只属于且属于一个点双连通分支。



    对于边双连通分支,求法更为简单。只需在求出所有的桥以后,把桥边删除,原图变成了多个连通块,则每个连通块就是一个边双连通分支。桥不属于任何一个边双连通分支,其余的边和每个顶点都属于且只属于一个边双连通分支。

    有重边的 边-双连通分量 应尤其注意,因为重边也算不同的边。解决方案是用边判断,而不是利用v!=fa判断能否由pre[v]更新low[u]

    void dfs(int u,int fa)
    {
    	tim++;
    	low[u]=pre[u]=tim;
    	for(int e=fst[u];e!=-1;e=nxt[e])
    	{
    		int w=e;if (e%2)w++;else w--;
    		int v=s[e].y;
    		if(!pre[v])
    		{
    			dfs(v,e);
    			low[u]=min(low[u],low[v]);
    			if (low[v]>pre[u])bridge[e]=bridge[w]=1;
    		}
    		else
    			//if(pre[v]<pre[u] && v != fa)
    			if (pre[v]<pre[u] && (e!=fa && w!=fa))
    			low[u]=min(low[u],pre[v]);
    	}
    }

     

    void dfs(int u)
    {
    	tim++;
    	pre[u]=low[u]=tim;
    	for(int e=fst[u];e!=-1;e=nxt[e])
    	{
    		int w=e;if(w%2)w++;else w--;
    		int v=s[e].y;
    		if(!pre[v])
    		{
    			flag[e]=flag[w]=1;
    			dfs(v);
    			low[u]=min(low[u],low[v]);
    			if(low[v]>pre[u])cnt[e]=cnt[w]=1;
    		}
    		else 
    		if(pre[v]<pre[u]&&!flag[e])  //判断边而不是判断点 
    		{
    			flag[e]=flag[w]=1;
    			low[u]=min(low[u],pre[v]);
    		}
    	}
    }



    构造双连通图

     

    一个有桥的连通图,如何把它通过加边变成边双连通图?方法为首先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1。

    统计出树中度为1的节点的个数,即为叶节点的个数,记为leaf。则至少在树上添加(leaf+1)/2条边,就能使树达到边二连通,所以至少添加的边数就是(leaf+1)/2。具体方法为,首先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的。然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(leaf+1)/2次,把所有点收缩到了一起。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    using namespace std;
    int m,tn,n;
    struct path{int x,y;}s[20001];
    int fst[20001],nxt[20001];
    bool bridge[20001];
    int pre[5001],low[5001];
    int tim,part,x,y;
    int hash[5001],out[5001];
    
    void dfs(int u,int fa)
    {
    	tim++;
    	low[u]=pre[u]=tim;
    	for(int e=fst[u];e!=-1;e=nxt[e])
    	{
    		int w=e;if (e%2)w++;else w--;
    		int v=s[e].y;
    		if(!pre[v])
    		{
    			dfs(v,e);
    			low[u]=min(low[u],low[v]);
    			if (low[v]>pre[u])bridge[e]=bridge[w]=1;
    		}
    		else
    			//if(pre[v]<pre[u] && v != fa)
    			if (pre[v]<pre[u] && (e!=fa && w!=fa))
    			low[u]=min(low[u],pre[v]);
    	}
    }
    
    void makeside(int x,int y)
    {
    	n++;s[n].x=x;s[n].y=y;
    	nxt[n]=fst[x];fst[x]=n;
    }
    
    void color(int i)
    {
    	hash[i]=part;
    	for(int e=fst[i];e!=-1;e=nxt[e])
    	if (!bridge[e]&&!hash[s[e].y])
    	color(s[e].y);
    }
    
    void print()
    {
    	int z=0;
    	for(int e=1;e<=n;e++)
    	if(bridge[e])
    	{
    		out[hash[s[e].x]]++;
    		out[hash[s[e].y]]++;
    	}
    	for(int i=1;i<=part;i++)
    	if(out[i]==2)z++;
    	cout<<(z+1)/2<<endl;
    }
    
    int main()
    {
    	while(scanf("%d%d",&m,&tn)==2)
    	{
    		memset(fst,-1,sizeof(fst));
    		memset(nxt,-1,sizeof(nxt));
    		memset(bridge,0,sizeof(bridge));
    		memset(hash,0,sizeof(hash));
    		memset(out,0,sizeof(out));
    		memset(pre,0,sizeof(pre));
    		memset(low,0,sizeof(low));
    		n=part=tim=0;
    		for(int i=1;i<=tn;i++)
    		{
    			scanf("%d%d",&x,&y);
    			makeside(x,y);
    			makeside(y,x);
    		}
    		for(int i=1;i<=m;i++)
    			if(!pre[i])
    			dfs(i,-1);
    		
    		for(int i=1;i<=m;i++)
    		if(!hash[i])
    		{
    			part++;
    			color(i);
    		}
    		print();
    	}		
    	return 0;
    }
    


    见题目:

    HOJ

    1007 SPF

    1098 NetWork

    1789 Electricity

    2360 Redundant Paths

     

    POJ

    3117 Redundant Paths

    3352 Road Construction



    参考:

    https://www.byvoid.com/blog/biconnect

    http://blog.csdn.net/z635457712a/article/details/8229113

    http://blog.csdn.net/lyy289065406/article/details/6762370


  • 相关阅读:
    奇偶游戏(带权并查集)
    银河英雄传说(边带权并查集)
    程序自动分析(并查集+离散化)
    关于树状数组的小总结(树状数组)
    你能回答这些问题吗 (线段树)
    Phython 3 笔记3 —— 类,库与文件的读写
    Phython 3 笔记2 —— 基础语法
    Phython 3 笔记1 —— 基础容器
    CRJ巨佬gjd算法伪代码
    CRJ巨佬的gjd算法模板
  • 原文地址:https://www.cnblogs.com/abgnwl/p/6550352.html
Copyright © 2011-2022 走看看