图论算法----Tarjan求无向图双连通分量及拓展

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/C20180602_csq/article/details/102721463

(咕了N年的知识点终于写出了一个简单又可靠的板子)

割点:在一个无向图中,如果删掉该点,则图的连通性被破坏

桥::在一个无向图中,如果删掉该边,则图的连通性被破坏

点双连通分量:一个没有割点的连通分量

边双连通分量:一个没有桥的连通分量

具体讲一下dfs树的思想(懂了dfs树之后就不用背Tarjan模板了)

一个无向图,我们对它进行一次dfs,把走过的边标记为树边,那么图中剩下的边只会是返祖边。(想一想就明白了)

当一个点返祖时,我们就可以知道他的祖先(目前最近的祖先)是谁了(我们要求的是最前的祖先)

扫描二维码关注公众号,回复: 7640142 查看本文章

所以tarjan中有一句话:low[u]=min(low[u],dfn[v])

又因为一个点与它的儿子是有边相连的,所以儿子能找到的最前祖先也可能是父亲能找到的最前祖先

所以我们要用儿子的祖先来更新父亲的祖先:low[u]=min(low[u],low[v])

这个跟求割点有什么关系?

当一个点的所以子孙都无法找到它的辈分(dfn)之上祖先时(可以等于它)(可以画图感受一下)

也就是(low[v]>=dfn[u])时,它就可能是割点(这时需要一些特判)

其实有一种更简单的做法:

我们可以把割点当作它属于多个点双连通分量,这个时候我们就可以给每个点开一个vector,来存它所属的点双编号pbcno

当一个点的pbcno.size()>1时,它就是割点了

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
#define N 100005
#define M 300005
int fir[N],to[2*M],nxt[2*M],cnt;
void adde(int a,int b)
{
	to[++cnt]=b;nxt[cnt]=fir[a];fir[a]=cnt;
	to[++cnt]=a;nxt[cnt]=fir[b];fir[b]=cnt;
}
int dfn[N],low[N],stk[N],top,dc,pbccnt;
vector<int> pbcno[N],pbc[N];
void dfs(int u,int fa)
{
	dfn[u]=low[u]=++dc;
	stk[top++]=u;
	int v,p;
	for(p=fir[u];p;p=nxt[p]){
		v=to[p];
		if(!dfn[v]){
			dfs(v,u);low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]){
				pbccnt++;
				while(top>0){
					pbcno[stk[--top]].push_back(pbccnt);
					pbc[pbccnt].push_back(stk[top]);
					if(stk[top]==v)
						break;
				}
				pbcno[u].push_back(pbccnt);
				pbc[pbccnt].push_back(u);
			}
		}
		else if(v!=fa) low[u]=min(low[u],dfn[v]);
	}
}
int main()
{
	int n,m,i,u,v;
	scanf("%d%d",&n,&m);
	for(i=1;i<=m;i++){
		scanf("%d%d",&u,&v);
		adde(u,v);
	}
	for(i=1;i<=n;i++)
		if(!dfn[i]) dfs(i,0);
	printf("%d\n",pbccnt);
	for(i=1;i<=pbccnt;i++){
		printf("%d:",i);
		for(int j=0,k=int(pbc[i].size());j<k;j++)
			printf(" %d",pbc[i][j]);
		printf("\n");
	}
	printf("\n");
	for(i=1;i<=n;i++){
		printf("%d:",i);
		if(pbcno[i].size()==1)
			printf("Normal\n");
		else
			printf("Cut point\n");
	}
}

我们稍加思考,就可以想到求边双连通分量的方法了

当一个点的儿子的子孙所能找到的祖先的深度严格小于当前点时,我们断掉这条边就可以使上下两个连通块分开,这条边就是桥

其实还有更简单的做法

由于一个点只会在一个边双连通分量里,所以我们只需要在一个点判一下它能否跨过父亲向上走即可判断它是否为桥的端点

用一个vector存一下就好了

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
#define N 100005
#define M 300005
int fir[N],to[2*M],nxt[2*M],cnt;
void adde(int a,int b)
{
	to[++cnt]=b;nxt[cnt]=fir[a];fir[a]=cnt;
	to[++cnt]=a;nxt[cnt]=fir[b];fir[b]=cnt;
}
int dfn[N],low[N],stk[N],top,dc,bbccnt,bbcno[N];
vector<int> bbc[N];
void dfs(int u,int fa)
{
	dfn[u]=low[u]=++dc;
	stk[top++]=u;
	int v,p;
	for(p=fir[u];p;p=nxt[p]){
		v=to[p];
		if(!dfn[v]){
			dfs(v,u);
			low[u]=min(low[u],low[v]);
		}
		else if(v!=fa&&!bbcno[v]) low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]){
		bbccnt++;
		while(top>0){
			bbcno[stk[--top]]=bbccnt;
			bbc[bbccnt].push_back(stk[top]);
			if(stk[top]==u)
				break;
		}
	}
}
int main()
{
	int n,m,i,u,v;
	scanf("%d%d",&n,&m);
	for(i=1;i<=m;i++){
		scanf("%d%d",&u,&v);
		adde(u,v);
	}
	for(i=1;i<=n;i++)
		if(!dfn[i]) dfs(i,0);
	printf("%d\n",bbccnt);
	for(i=1;i<=bbccnt;i++){
		printf("%d:",i);
		for(int j=0,k=int(bbc[i].size());j<k;j++)
			printf(" %d",bbc[i][j]);
		printf("\n");
	}
}

类比SCC(有向图强连通分量),就多了一个v!=fa,简单易懂

这时候,有人就会问:为什么点双不能也怎么做呢?

由于一个点双必定是边双,而一个边双不一定是点双

所以对于一个low[u]=dfn[u]点,它一定会成为它的儿子连通块之间的割点,但是它可以把儿子们的边双连接成一个边双

如图:

所以求点双必须在一个点遍历儿子的时候去做,而求边双可以在遍历完儿子之后再做

大家可以类比以上两种思想,推一推SCC的写法。。。

顺便存一下SCC的板子:

//2481
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 205
int dfn[N],low[N],sccno[N],stk[N],scccnt,top,dfs_clock;
int fir[N],to[N*N],nxt[N*N],cnt;
void adde(int a,int b)
{
    to[++cnt]=b;
    nxt[cnt]=fir[a];
    fir[a]=cnt;
}
void dfs(int u)
{
    dfn[u]=low[u]=++dfs_clock;
    stk[top++]=u;
    int i,v;
    for(i=fir[u];i;i=nxt[i]){
        v=to[i];
        if(!dfn[v]){
            dfs(v);
            low[u]=min(low[u],low[v]);
        }
        else if(!sccno[v]) low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]){
        scccnt++;
        while(top>0){
            sccno[stk[--top]]=scccnt;
            if(stk[top]==u)
                break;
        }
    }
}
int main()
{
    int n,i,x;
    scanf("%d",&n);
    for(i=1;i<=n;i++){
        while(1){
            scanf("%d",&x);
            if(x==0) break;
            else adde(i,x);
        }
    }
    for(i=1;i<=n;i++)
        if(!dfn[i]) dfs(i);
    printf("%d",scccnt);
}

下面是拓展内容:

题目:https://www.luogu.org/problem/P4082

题解

此题难度应该是提高+

其实可以不用圆方树的(虽然圆方树也基于点双)

我们可以很轻松地想到暴力dfs怎么写

然后考虑一下我们的时间都浪费在了哪里

发现我们每一次想要换个方位推箱子的时候,就要重新dfs一遍求换的方位算法可行

于是我们可以把每一个空白格子看成一个点,每两个相邻的空白格子之间连一条边

然后预处理这个图的点双(因为箱子所在点的位置相当于把这个点割掉)

再来dfs,只要箱子要换方向,就只需要判断一下当前的方向和要换的方向是否在一个点双内

由于一个点的出度最多为4,它所属的点双数目最多为2,所以我们就可以暴力比较两个点的所属点双集是否有交

于是就过了

O2AC代码(有点慢,不想重构了,代码丑勿喷):

//Push a Box
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
#define N 1505
char a[N][N];
int num[N][N];
int fir[N*N],to[4*N*N],nxt[4*N*N],cnt;
void adde(int a,int b)
{
    to[++cnt]=b;nxt[cnt]=fir[a];fir[a]=cnt;
}
int dx[4]={0,0,1,-1};
int dy[4]={1,-1,0,0};
int dfn[N*N],low[N*N],stk[N*N];
int dfs_clock,top,pbccnt;
vector<int> pbcno[N*N];
void dfs(int u,int fa)
{
	dfn[u]=low[u]=++dfs_clock;
	stk[top++]=u;
	int v,p;
	for(p=fir[u];p;p=nxt[p]){
		v=to[p];
		if(v==fa)continue;
		if(!dfn[v]){
			dfs(v,u);low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]){
				pbccnt++;
				while(top>0){
					pbcno[stk[--top]].push_back(pbccnt);
					if(stk[top]==v)
						break;
				}
				pbcno[u].push_back(pbccnt);
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
bool vis[N][N];
void dfs1(int i,int j)
{
	vis[i][j]=1;
	int o,_i,_j;
	for(o=0;o<=3;o++){
		_i=i+dx[o];_j=j+dy[o];
		if(a[_i][_j]!='#'&&a[_i][_j]!='B'&&!vis[_i][_j])
			dfs1(_i,_j);
	}
}
bool can[N][N][4];
bool pd(int u,int v)
{
    for(int i=0;i<int(pbcno[u].size());i++)
        for(int j=0;j<int(pbcno[v].size());j++)
            if(pbcno[u][i]==pbcno[v][j])
                return 1;
    return 0;
}
void dfs2(int i,int j,int o)
{
    if(can[i][j][o]) return;
    can[i][j][o]=1;
    int k,_i,_j;
    for(k=0;k<=3;k++){
        _i=i+dx[k];_j=j+dy[k];
        if(a[_i][_j]!='#'){
            if((k^1)!=o&&pd(num[i+dx[o]][j+dy[o]],num[i+dx[k^1]][j+dy[k^1]]))
                dfs2(_i,_j,k^1);
            //if(k^1==o)XXXXXXXXXXXXXXXXXXXX
            if((k^1)==o)
                dfs2(_i,_j,k^1);
        }
    }
}
int main()
{
    int n,m,Q,i,j,o,con=0;
    int Ax,Ay,Bx,By;
    scanf("%d%d%d",&n,&m,&Q);
    for(i=1;i<=n;i++){
        scanf("%s",a[i]+1);
        for(j=1;j<=m;j++)
            if(a[i][j]!='#'){
                num[i][j]=(++con);
                if(a[i][j]=='A')
                    Ax=i,Ay=j;
                if(a[i][j]=='B')
                    Bx=i,By=j;
            }
    }
    for(i=1;i<=n;i++)
        a[i][0]=a[i][m+1]='#';
    for(i=1;i<=m;i++)
        a[0][i]=a[n+1][i]='#';
    int _i,_j;
    for(i=1;i<=n;i++){
        for(j=1;j<=m;j++){
            if(a[i][j]!='#'){
                for(o=0;o<=3;o++){
                    _i=i+dx[o];_j=j+dy[o];
                    if(a[_i][_j]!='#')
                        adde(num[i][j],num[_i][_j]);
                }
            }
        }
    }
    
    for(i=1;i<=con;i++)
        if(!dfn[i])
            dfs(i,-1);
    dfs1(Ax,Ay);
	/*for(i=1;i<=n;i++){
        for(j=1;j<=m;j++)
			if(pbcno[num[i][j]].size()>0)
				printf("%d",pbcno[num[i][j]].size());
			else
				printf("0");
		printf("\n");
    }
	printf("\n");
	for(i=1;i<=n;i++){
        for(j=1;j<=m;j++)
			if(pbcno[num[i][j]].size()>0)
				printf("%d",pbcno[num[i][j]][0]);
			else
				printf("0");
		printf("\n");
    }
	printf("\n");
	for(i=1;i<=n;i++){
        for(j=1;j<=m;j++)
			if(pbcno[num[i][j]].size()>1)
				printf("%d",pbcno[num[i][j]][1]);
			else
				printf("0");
		printf("\n");
    }
	printf("\n");*/
    for(o=0;o<=3;o++){
        _i=Bx+dx[o];_j=By+dy[o];
        if(vis[_i][_j])
            dfs2(Bx,By,o);
    }
    int x,y;
    for(i=1;i<=Q;i++){
        scanf("%d%d",&x,&y);
        if(can[x][y][0]||can[x][y][1]||can[x][y][2]||can[x][y][3]||(Bx==x&&By==y))printf("YES\n");
        else printf("NO\n");
    }
}
/*
6 5 1
.#...
.#.#.
.....
.#.##
..B##
##A##
1 1
*/

圆方树占坑代填。。。

圆方树已填坑

猜你喜欢

转载自blog.csdn.net/C20180602_csq/article/details/102721463