无向图必经点、必经边的相关问题
一、 任意两点间路径的必经边
首先考虑到必经边一定是原图的一条割边。
那么对于一个(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