题意:
给一张无向图,q次操作,每次在指定两点之间添加一条路径,
问添加上这条路径之后图中有多少条割边。
思路:
首先我们知道,v-dcc缩点后图(就是一棵树)中的各点之间通过割边连接,且包含原图
中所有割边,当我们在原图中的某两点之间添加一条路径之后,对应v-dcc缩点后图中 的两
个点之间的路径上的边将不在是割边,其路径长度可以用LCA的方法来求。确定这些边不是
割边后,需要作出标记,然后标记一下就可以了(如果朴素的由子节点向LCA走的话总的时
间复杂度约为M+Q*N),(tarjan算法的复杂度差不多N+M)。因为被标记过的边可以不再
被访问。所以我们可以用并查集优化,在从子节点向LCA走的时候,每标记一条边就将这条
边的子节点与与其父节点合并。这样我们在再次标记该点之后路径时可以先得到该点所属于
的v-dcc,然后并查集find(对应v-dcc的编号)就可以跳过那些已经被标记的边,直接到达还
没有被标记的边的起点(这里注意判断跳多了越过LCA或者偏离向LCA走的情况)。
WA了几次:主要原因有这些小算法不熟练,理解不透彻,组合起来的时候量很多,容易混,
其次有些地方的代码操作是重复的,如果处理不好(多写太多重复代码),会TLE。再就是从
子节点往LCA走的过程容易出错了,因为涉及到要用并查集跳过许多边,所以容易跳过了,
其次一个点连接着多条边,但怎么找出其连接父节点的那条边。
自己的代码就是把那些小算法拼接了一下,网上题解相比做了如下优化:①不再单独存储
缩点后的图(省出缩点的代码),直接在求v-dcc时用并查集将同一联通块内的点连接起来。
就相当于他们之间的边都已经被标记为不是割边了。②查询时,一边求LCA一边合并不再是
桥的那些点。(因为本身求LCA的过程本身就需要从两个子节点往上走一直到LCA,这里如
果用倍增,求LCA是快了,但求完之后还要再重复走一次来记录非桥边,得不偿失)。
AC代码(自己的没有把握住算法本质的代码):
#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include<algorithm>
#include <set>
#include <queue>
#include <stack>
#include<vector>
#include<map>
#include<ctime>
#define ll long long
using namespace std;
const int N=100010;
const int M=100100*2*2;
int ans=0;
int head[N];
int ver[M];
int Next[M];
int dfn[N];
int low[N];//该点两个low值来源中最小的时间戳值
int n,m,tot,num;//边数、时间戳
bool bridge[M];
void add(int x,int y)
{
ver[++tot]=y;
Next[tot]=head[x];
head[x]=tot;
}
void tarjan(int x,int in_edge)
{
dfn[x]=low[x]=++num;//从上往下搜索时预处理x点的时间戳和回溯值
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(!dfn[y])
{
tarjan(y,i);
low[x]=min(low[x],low[y]);//
if(low[y]>dfn[x])bridge[i]=bridge[i^1]=true;//满足定理条件:割边
}
else if(i!=(in_edge^1))low[x]=min(low[x],dfn[y]);//low【x】值的两种来源之后一种,y通过一条非树边到达x
}
}
int c[N];//点 所属于的联通块编号
int dcc;//联通块标号
void dfs(int x)
{
c[x]=dcc;
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(c[y]||bridge[i])continue;
dfs(y);
}
}
int hc[N];//点的首条边,head
int vc[M];//边的终点ver
int nc[M];//Next数组
int tc;//缩点后的边数
void add_c(int x,int y)
{
vc[++tc]=y;
nc[tc]=hc[x];
hc[x]=tc;
}
int f[N][20],d[N],t;
int dc[N];
int num2=0;
void bfs()//预处理,求深度、最短路,f数组;就是SPFA,时间复杂度O(NlogN)
{
queue <int> q;
while(q.size())q.pop();
q.push(1);
d[1]=1;
while(q.size())
{
int x=q.front();
dc[x]=++num2;//顺便求出缩点图各点的时间戳,用来明确前进方向
q.pop();
for(int i=hc[x];i;i=nc[i])
{
int y=vc[i];
if(d[y])continue;
d[y]=d[x]+1; //求节点的深度;
f[y][0]=x;
for(int j=1;j<=t;++j)
{
f[y][j]=f[f[y][j-1]][j-1];//动态规划,y点 跳(2^j)步 能到达哪个点。
//cout<<y<<" "<<j<<" "<<f[y][j]<<endl;
q.push(y);
}
}
}
}
int lca(int x,int y)//求LCA,时间复杂度:o(Log(N))
{
if(d[x]>d[y])swap(x,y);
for(int i=t;i>=0;--i)//倒着走******
if(d[f[y][i]]>=d[x])y=f[y][i];
if(x==y)return x;
for(int i=t;i>=0;--i)
if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];//注意一下:x是不断变化的,而i整个一套下来;
return f[x][0];//一直更新,直至x、y为目标点下面的那两个点;
}
int fat[N];
int find(int x)
{
if(x==fat[x])return fat[x];
return fat[x]=find(fat[x]);
}
void unionn(int x,int y)
{
int fa=find(x),fb=find(y);
if(fa!=fb)
{
fat[fa]=fb;
}
}
int lc;
void solve(int x,int e)
{
x=find(x);
if(dc[x]<=dc[lc])return ;//注意当前点应该往哪个方向走
//if(x==lc)return ;
for(int i=hc[x];i;i=nc[i])
{
if(i==(e^1))continue;
int y=vc[i];
y=find(y);//跳过重复的边
if(!(dc[y]<dc[x]))continue;//保证当前点顺着指向lca的方向走
ans--;
unionn(x,y);//此处注意并查集路径压缩的方向,一般使用并查集压缩路径都要注意方向
//cout<<".."<<endl;
solve(y,i);
}
}
int main()
{
int cas=0;
while(scanf("%d%d",&n,&m)&&n+m)
{
tot=1;
tc=1;
ans=0;
num=num2=0;
dcc=0;
memset(Next,0,sizeof(Next));
memset(head,0,sizeof(head));
memset(nc,0,sizeof(nc));
memset(hc,0,sizeof(hc));
memset(dc,0,sizeof(dc));
memset(dfn,0,sizeof(dfn));
memset(bridge,0,sizeof(bridge));
memset(low,0x3f,sizeof(low));
memset(ver,0,sizeof(ver));
memset(vc,0,sizeof(vc));
memset(d,0,sizeof(d));//多组输入,这一堆很容易错,尤其像这种if中出现的
memset(c,0,sizeof(c));
for(int i=1;i<=m;++i)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
tarjan(1,-1);//求V-DCC
for(int i=1;i<=n;++i)//并给每个v-dcc块标号
{
if(!c[i])//当前不属于任何联通块的点
{
++dcc;//联通块的标号
dfs(i);
}
}
//缩点
tc=1;
for(int i=2;i<tot;i+=2)//i+=2,有区别和书上
{
int x=ver[i],y=ver[i^1];
if(c[x]==c[y])continue;
add_c(c[x],c[y]);
add_c(c[y],c[x]);
}
t=(int)log(n)/log(2)+2;//倍增t
bfs();//预处理f【】【】,d【】
int q;
scanf("%d",&q);
ans=(tc-1)/2;//原图割边数
for(int i=0;i<=dcc;++i)fat[i]=i;
printf("Case %d:\n",++cas);
while(q--)
{
int x,y;
scanf("%d%d",&x,&y);
x=c[x];//别把原图与缩点图弄混
y=c[y];
lc=lca(x,y);
solve(x,-1);//更新ans(减小)
solve(y,-1);
printf("%d\n",ans);
}
printf("\n");
}
return 0;
}
大神的该题的真正代码:
(不过有个小问题,他的路径压缩只是优化了查询两点是否在同一双联
通块内,但向lca走的过程没有用路径压缩优化)。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define _rep(i,a,b) for(int i=(a);i<=(b);i++)
const int N=1e5+10;
const int M=2e5+10;
int ca,n,m,q,head[N],tot,fa[N],low[N],dfn[N],num,cnt,pre[N];
struct Edge{
int v,nx;
}edge[M<<1];
inline void addedge(int u,int v)
{
edge[tot].v=v;
edge[tot].nx=head[u];
head[u]=tot++;
}
int findfa(int x){return fa[x]==x?x:fa[x]=findfa(fa[x]);}
bool merge(int x,int y)
{
int fx=findfa(x);
int fy=findfa(y);
if(fx==fy)return false;
fa[fy]=fx;return true;
}
void tarjan(int u,int f)
{
dfn[u]=low[u]=++num;
int flag=0;
for(int i=head[u];~i;i=edge[i].nx)
{
int v=edge[i].v;
if(v==f&&!flag){flag=1;continue;}
if(dfn[v]==-1)
{
pre[v]=u;
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u])cnt++;
else merge(u,v);//求割边的同时处理处理原图中那些非桥边
}
else low[u]=min(low[u],dfn[v]);
}
}
inline int lca(int u,int v)//求LCA的较朴素算法
{
if(findfa(u)==findfa(v))return cnt;//并查集只在这里起作用,下面的移动每天有体现出路径压缩
if(dfn[u]>dfn[v])swap(u,v);
while(dfn[u]<dfn[v])
{
if(merge(pre[v],v))cnt--;
v=pre[v];
}
while(u!=v)
{
if(merge(u,pre[u]))cnt--;//一边求LCA一边更新答案,“标记”非树边
u=pre[u];
}
return cnt;
}
int main()
{
//freopen("in.txt","r",stdin);
int u,v;
while(scanf("%d%d",&n,&m)&&n&&m)
{
printf("Case %d:\n",++ca);
cnt=num=tot=0;
memset(head,-1,sizeof(head));
memset(dfn,-1,sizeof(dfn));
memset(low,-1,sizeof(low));
pre[1]=1;
_rep(i,1,n)fa[i]=i;
_rep(i,1,m)scanf("%d%d",&u,&v),addedge(u,v),addedge(v,u);
tarjan(1,1);
scanf("%d",&q);
_rep(i,1,q)
{
scanf("%d%d",&u,&v);
printf("%d\n",lca(u,v));
}
puts("");
}
return 0;
}
The end;