无向图必经点、必经边的相关问题

无向图必经点、必经边的相关问题

一、 任意两点间路径的必经边

模板

首先考虑到必经边一定是原图的一条割边。

那么对于一个(e-DCC)中的点是不存在必经边的。不懂(e-DCC)相关内容?戳Here

那么很容易想到对于每一个(e-DCC)缩点,得到一棵树。两点间路径的必经边条数就是两点所在的

(e-DCC)对应的新节点的树上路径长度。直接树上倍增即可。

#include<bits/stdc++.h>
#define LL long long
#define DB double
#define RG register
#define IL inline
#define pb push_back
using namespace std;

const int N=2e5+3;

int n,m,Q,tot,cnt,num,Time,dfn[N],low[N];
int bri[N<<1],vis[N],bel[N],pos[N],dep[N],head[N],f[N][21];

struct edge{int x,y;}a[N];
struct EDGE{int next,to;}e[N<<1];

IL int gi() {
    RG int x=0,p=1; RG char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-') p=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') x=x*10+(ch^48),ch=getchar();
    return x*p;
}

IL void New() {
	tot=0;
	memset(&e,0,sizeof(e));
	memset(head,0,sizeof(head));
	memset(vis,0,sizeof(vis));
}

IL void make(int x,int y) {
	e[++tot]=(EDGE){head[x],y},head[x]=tot;
	e[++tot]=(EDGE){head[y],x},head[y]=tot;
}

void Tarjan(int x,int fx) {
	RG int i,y;
	dfn[x]=low[x]=++Time;
	for(i=head[x];i;i=e[i].next)
		if(!dfn[y=e[i].to]) {
			Tarjan(y,x),low[x]=min(low[x],low[y]);
			if(low[y]>dfn[x]) bri[i]=bri[i^1]=1;
		}
		else if(y!=fx) low[x]=min(low[x],dfn[y]);
}

void dfs(int x) {
	RG int i,y;
	vis[x]=1,bel[x]=cnt;
	for(i=head[x];i;i=e[i].next)
		if(!vis[y=e[i].to]&&!bri[i]) dfs(y);
}

void dfs2(int x) {
	RG int i,y;
	pos[x]=num,vis[x]=1;
	for(i=1;i<=20;++i) f[x][i]=f[f[x][i-1]][i-1];
	for(i=head[x];i;i=e[i].next)
		if(!vis[y=e[i].to])
			dep[y]=dep[x]+1,f[y][0]=x,dfs2(y);
}

IL int lca(int x,int y) {
	RG int i; 
	if(dep[x]<dep[y]) swap(x,y);
	for(i=20;i>=0;--i)
		if(dep[f[x][i]]>=dep[y]) x=f[x][i];
	if(x==y) return x;
	for(i=20;i>=0;--i)
		if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
	return f[x][0];
}

int main()
{
	RG int i,x,y;
	n=gi(),m=gi(),tot=1;
	for(i=1;i<=m;++i)
		a[i].x=gi(),a[i].y=gi(),make(a[i].x,a[i].y);
	for(i=1;i<=n;++i)
		if(!dfn[i]) Tarjan(i,0);
	for(i=1;i<=n;++i)
		if(!vis[i]) ++cnt,dfs(i);
	for(i=1,New();i<=m;++i) {
		x=a[i].x,y=a[i].y;
		if(bel[x]!=bel[y]) make(bel[x],bel[y]);
	}
	for(i=1;i<=cnt;++i)
		if(!vis[i]) f[i][0]=0,dep[i]=1,dfs2(i);
	for(Q=gi();Q;--Q) {
		x=bel[gi()],y=bel[gi()];
		if(pos[x]!=pos[y]) puts("0"); // 是否在同一连通块
		else {
			if(x==y) puts("0"); // 是否在同一e-DCC
			else printf("%d
",dep[x]+dep[y]-2*dep[lca(x,y)]);
		}
	}
	return 0;
}

二、 任意两点间路径的必经点

同上面一样,必经点一定是割点。位于同一个(v-DCC)中的两点间路径不存在必经点。

同样的进行(v-DCC)缩点。这时(v-DCC)缩点方式的优越体现了出来:

假设每一个(v-DCC)缩完的新点叫圆点,每一个割点对应的新点叫方点,

那么两点间路径的必经点个数就是 两点所对应的树上新点 间路径上的方点个数!

事实上,这也正体现了(v-DCC)缩点后原来的点的性质在新图中的分布特点:

割点的性质体现在割点对应的新点(方点)上;

非割点的性质体现在其所在(v-DCC)对应的新点(圆点)上。

void tarjan(int x) {
	RG int i,k,y,num=0;
	dfn[x]=low[x]=++Time,sta[++top]=x;
	if(x==RT&&head[x]==0) {
		vec[++cnt].push_back(x);
		return;
	}   // 孤立点的情况
	for(i=head[x];i;i=e[i].next)
		if(!dfn[y=e[i].to]) {
			tarjan(y),low[x]=min(low[x],low[y]);
			if(dfn[x]<=low[y]) {
				if(x!=RT||++num>1) cut[x]=1;
				vec[++cnt].push_back(x);
				do{
					k=sta[top--]; 
					vec[cnt].push_back(k);
				}while(k!=y);
			}
		}
		else low[x]=min(low[x],dfn[y]);
}

void dfs(int x,int fx) {
	RG int i,y;
	dep[x]=dep[fx]+1,f[x][0]=fx,g[x][0]=mtx[fx],vis[x]=1;
  	// g[][]记录的是往上跳的方点个数
	for(i=1;i<=20;++i)
		f[x][i]=f[f[x][i-1]][i-1],g[x][i]=g[x][i-1]+g[f[x][i-1]][i-1];		
	for(i=head[x];i;i=e[i].next)
		if((y=e[i].to)!=fx) dfs(y,x);
}

IL int lca(int x,int y) {
	RG int i,dis=0;
	if(dep[x]<dep[y]) swap(x,y);
	for(i=20;i>=0;--i)
		if(dep[f[x][i]]>=dep[y]) dis+=g[x][i],x=f[x][i];
	if(x==y) return dis-mtx[y];
	for(i=20;i>=0;--i)
		if(f[x][i]!=f[y][i]) dis+=g[x][i]+g[y][i],x=f[x][i],y=f[y][i];
	return dis+g[x][0]+g[y][0]-mtx[f[x][0]];
}

int main()
{
	while(scanf("%d%d",&n,&m)!=EOF&&n&&m) {
		New_case();
		RG int i,j,x,y;
		for(i=1;i<=m;++i)
			scanf("%d%d",&s[i].x,&s[i].y),make(s[i].x,s[i].y);
		for(i=1;i<=n;++i)
			if(!dfn[i]) RT=i,tarjan(i);
		for(i=1;i<=n;++i)
			if(cut[i]) bel[i]=++cnt,mtx[cnt]=1;
      	//mtx[]记录的是缩点后树上的点是不是方点。
		for(i=1,New();i<=cnt;++i)
			for(j=0;j<vec[i].size();++j)
				if(cut[x=vec[i][j]]) make(i,bel[x]);
				else bel[x]=i;
		for(i=1;i<=cnt;++i)
			if(!vis[i]) dfs(i,0);
		for(scanf("%d",&Q);Q;--Q) {
			scanf("%d%d",&x,&y);
			if(bel[x]==bel[y]) puts("0");
			else printf("%d
",lca(bel[x],bel[y]));
		}
	}
	return 0;
}
// 无向图两点间路径的必经点

上面两个问题事实上都涉及到点的性质在双连通分量中的体现,而下面问题则涉及到边的性质在双连通分量中的体现。

三、 任意两边间路径的必经点

模板

简单来讲,问题需要解决的只有边的归属问题(边丢在哪一个新点中),然后用同上的方法即可解决。

这个时候分类讨论一下:

1、边两端点都是非割点。那么这两点必然在一个(v-DCC)中,该边就对应该(v-DCC)对应的节点(圆点)。

2、边两端点有且仅有一个割点。这样的边对应 其非割点端点所在(v-DCC)对应的节点(圆点)。

3、边两端点都是割点。这样的边就对应 仅包含这两个割点的(v-DCC)对应的节点(圆点)。

​ 这个具体讲一下,因为两点都为割点,那么先遍历到二者之一后,必然会把另外一个点和该点加入一个新的(v-DCC)

​ 而且该(v-DCC)也仅有这两个点。因此我们可以在划分(v-DCC)的过程中,记录一下点被划分进入(v-DCC)编号

​ (不包括最后单独放入的那个节点),然后对于这两割点,比较一下谁先被划分,就丢入它对应的(v-DCC)即可。

会抓紧补上一张图的。。。

Tarjan 中的记录 :

if(dfn[x]<=low[y]) {
	if(x!=RT||++num>1) cut[x]=1;
		vec[++cnt].push_back(x); // x不考虑
		do{
			k=sta[top--]; 
			vec[cnt].push_back(k);
            in[k]=cnt; // 记录一下编号
        }while(k!=y);
}

边的归属划分:

for(i=1;i<=m;++i) {
	x=s[i].x,y=s[i].y;
	if(!cut[x]) pos[i]=bel[x];
	else if(!cut[y]) pos[i]=bel[y];
	else {
		if(!in[x]) pos[i]=in[y];
		else if(!in[y]) pos[i]=in[x];
		else pos[i]=min(in[x],in[y]);
	}
}

然后同样考虑树上路径的方点个数即可。

四、 任意两边间路径的必经边

这个的话,边的划分没那么多复杂。

对于非割边,直接丢在其任一端点对应的(e-DCC)中(两端点在同一(e-DCC))。

对于割边,新建一个节点,其类似于上面的割点对应的新节点,也就是方点。

然后考虑树上路径的方点个数就好了!

五、声明:

这一类问题大概讲到这里为止了,如果有什么问题欢迎指出。

另外,圆点方点跟那个SMG圆方树没啥关系。。。hhh

原文地址:https://www.cnblogs.com/Bhllx/p/11273294.html