桥与割点,无向图的双连通分量
Tarjan算法与无向图连通性
Tarjan算法求割点与割边
定义与性质:
定义
给定无向连通图
- 割点:节点
,若将节点 及其所相连的所有边删去之后,图 分成两个及以上子图,则称节点 为图 的割点 - 割边: 也称桥,边
,若将边 在图中删去,会使得图 分成两个不相连的子图,则称 为图 的桥 - 点双连通图:若图
不含有割点,则称图 为点双连通图 - 边双连通图:若图
不含有割边,则称图 为边双连通图 - 点双连通分量:图
的极大点双连通子图被称为图 的点双连通分量,简称" "(极大的意思即不存在更大的) - 边双连通分量:图
的极大边双连通子图被称为图 的边双连通分量,简称" " - 5、6统称为双连通分量,简称
,一张无向图的割点与割边就是各个连通块的割点割边的总和
性质: - 一个简单环上的节点一定不是割边
- 一个无向图是点双连通图,当且仅当满足以下条件之一:(1).图中节点数不超过2.(2)图中的任意两个点都至少被包含在一个简单环中
- 一张无向图是边双连通图,当且仅当每一条边都在一个简单环中
- 在一个点双连通图中(节点数大于2),任意两个节点都有着至少两条互不相交的路径
- 在一个边双连通图中,每一条边的两个端点都有另一条路线可以抵达对方
- 若某个点双连通分量中的两个节点被一个奇环所包含,则每一对节点都被至少一个奇环包含
Tarjan算法
tarjan算法可以在线性时间内求出一张图的所有割点与割边
具体是这样的
tarjan算法需要记录一个时间戳(
我们按照深度优先遍历的方法,每个节点只访问一次,形成一颗搜索树.
我们定义
- 属于
的节点 - 设一个节点
,通过不在搜素树上的一条边能够抵达 的节点
以下图为例,我们使用加粗代表搜索树,黑笔代表时间戳,红笔代表追溯值
对于 的计算,根据定义,我们应该先令 ,然后扫描 的每一条出边 是搜索树上的边,此时令 不是搜索树上的边,此时令
void tarjan(int u,int in){
dfn[u]=low[u]=++num;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v,i);
low[u]=min(low[u],low[v]);
}
else if((i^1)!=in)low[u]=min(low[u],dfn[v]);
}
}
割边判定法则
边
证明:简单证明,
对于程序的实现,我们因为无向图采用双向边,于是需要特判父亲节点,此时我们一般是记录节点的入边的编号,利用成对变换判断父节点
int dfn[N],low[N],n,m,head[N],nxt[M],ver[M],bridge[M],num,tot=1;
void add(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void tarjan(int u,int in){
dfn[u]=low[u]=++num;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v,i);
low[u]=min(low[u],low[v]);
if(dfn[u]<low[v]){
bridge[i]=bridge[i^1]=1;
}
}
else if((i^1)!=in)low[u]=min(low[u],dfn[v]);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v);add(v,u);
}
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,0);//若图不是无向连通图
for(int i=2;i<=tot;i++)printf("%d ",bridge[i]);
}
割点判定法则
节点
特别的,若
因为判定法则是
int dfn[N],low[N],n,m,head[N],nxt[M],ver[M],vis[N],num,tot=1,root;
void add(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void tarjan(int u){
dfn[u]=low[u]=++num;
int cnt=0;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v,i);
low[u]=min(low[u],low[v]);
if(dfn[u]<=low[v]){
cnt++;
if(u!=root||cnt>1)vis[u]=1;
}
}
else low[u]=min(low[u],dfn[v]);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v);add(v,u);
}
for(int i=1;i<=n;i++)if(!dfn[i])root=i,tarjan(i);
for(int i=1;i<=n;i++)printf("%d ",vis[i]);
}
双连通分量
边连通分量
求法
根据定义,我们只需要将原无向图中所有的桥删去,然后进行深度优先遍历整个图,剩下的所有连通块都是原图的一个边连通分量
//深度优先遍历的代码
void dfs(int u){
c[u]=edcc;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(c[v]||bridge[i])continue;
dfs(v);
}
}
//主函数中的代码
for(int i=1;i<=n;i++)if(!c[i])++edcc,dfs(i);
//最终我们就得到了edcc个边连通分量,其中每个节点属于的边连通分量的编号就是c
点连通分量
这个较为复杂,与边连通分量不同的是,点连通分量的割点可能会属于多个
其中1,6两个节点便是割点
我们发现,除了割点之外,所有的点都只属于一个
为了求出
- 递归到节点
,将其入栈 - 若割点判定法则
成立,无论 是否是根节点,都要不断弹出栈中节点直到 节点被弹出,然后再将 压入栈,所有弹出的节点与 构成一个 ,
void tarjan(int x) {
dfn[x] = low[x] = ++num;
stack[++top] = x;
if (x == root && head[x] == 0) { // 孤立点
dcc[++cnt].push_back(x);
return;
}
int flag = 0;
for (int i = head[x]; i; i = Next[i]) {
int y = ver[i];
if (!dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
if (low[y] >= dfn[x]) {
flag++;
if (x != root || flag > 1) cut[x] = true;
cnt++;
int z;
do {
z = stack[top--];
dcc[cnt].push_back(z);
} while (z != y);
dcc[cnt].push_back(x);
}
}
else low[x] = min(low[x], dfn[y]);
}
}
缩点
e-DCC的缩点
很简单,求出所有的
void add_v(int u,int v){
vnxt[++vtot]=vhead[u],vver[vhead[u]=vtot]=v;
}
//主函数中代码
vtot=1;
for(int i=2;i<=tot;i++)if(bridge[i])add_v(c[ver[i]],c[ver[i^1]]);
v-DCC的缩点
因为各个割点会在可能会出现在多个
连通分量的综合运用
冗余路径
为了从
奶牛们已经厌倦了被迫走某一条路,所以她们想建一些新路,使每一对草场之间都会至少有两条相互分离的路径,这样她们就有多一些选择。
每对草场之间已经有至少一条路径。
给出所有
两条路径相互分离,是指两条路径没有一条重合的道路。
但是,两条分离的路径上可以有一些相同的草场。
对于同一对草场之间,可能已经有两条不同的道路,你也可以在它们之间再建一条道路,作为另一条不同的道路。
分析
简单来说本题题意就是在一张无向连通图中加边,使得整个无向连通图变成一个边双连通图
我们先对整张图求
正确性很显然,当这棵树成了边双连通图后原图上所有的
而在一棵树上加边就会多出一个环,我们的目的就是让树上所有的点对都在至少一个环内。
引理
将一棵树变成一个边双连通图,至少需要
证明
先证必要性
从叶子节点入手,因为叶子节点的特殊性,对于每一次加边操作,我们最多使得两个叶子节点相连通,这一点很显然,因为只有加入的边的一端连向一个叶子节点,这个叶子节点才会在环中,所以每一次最多可以令两个叶子节点加入环,所以最少需要加入
再证充分性
因为必要性的证明,很容易发现,最优策略加边都是从两个叶子节点处加边,那么我们需要证明这样的操作一定可以覆盖整棵树
每一个叶子节点到根的路径都是一条链,整棵树就是由所有的链组成的(重复部分只算一次)。每一次进行加边操作,假设连接的是叶子节点
于是证明完成之后我们的问题就变成了统计有多少个连通块是缩点之后的叶子节点,无根树的叶子节点就是度数为1的节点,在这种情况下,我们并不需要将缩点后的图建立出来,直接统计就可以了
int head[N],nxt[M],ver[M],c[N],dfn[N],low[N],tot=1,dcc,num,bridge[M],n,m,cnt,in[N];
void add(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void tarjan(int u,int in){
dfn[u]=low[u]=++num;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v,i);
low[u]=min(low[u],low[v]);
if(dfn[u]<low[v]){
bridge[i]=bridge[i^1]=1;++cnt;
}
}
else if(in!=(i^1))low[u]=min(low[u],dfn[v]);
}
}
void dfs(int u){
c[u]=dcc;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(c[v]||bridge[i])continue;
dfs(v);
}
}//板子
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,0);
for(int i=1;i<=n;i++){
if(!c[i])dcc++,dfs(i);
}
for(int i=2;i<=tot;i++){
if(bridge[i])in[c[ver[i]]]++;//度数统计
}
int ans=0;
for(int i=1;i<=dcc;i++)if(in[i]==1)ans++;
printf("%d\n",(ans+1)/2);
}
无向图必经点与必经边
必经边
例题(裸题):逃不掉的路
现代社会,路是必不可少的。
共有 n 个城镇,m 条道路,任意两个城镇都有路相连,而且往往不止一条。
但有些路年久失修,走着很不爽。
按理说条条大路通罗马,大不了绕行其他路呗——可小撸却发现:从 a 城到 b 城不管怎么走,总有一些逃不掉的必经之路。
他想请你计算一下,a 到 b 的所有路径中,有几条路是逃不掉的?
分析
回想起边连通分量的定义,在一个边连通分量里任意两个点都具有两条及其以上的相离的路径,所以双连通分量里的路肯定逃得掉
于是我们只需要考虑双连通分量之外的路,很明显,割边是连接两个连通块之间的路径,肯定是逃不掉的,于是问题就变成了询问两个点,求路径上的割边数,简单的
int num,edcc,dep[N],c[N],head[N],vhead[N],nxt[M],vnxt[M],ver[M],vver[M],tot=1,vtot=1,dfn[N],low[N],bridge[M],n,m,f[N][25],t,ss;
void add(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void add_v(int u,int v){
vnxt[++vtot]=vhead[u],vver[vhead[u]=vtot]=v;
}
void tarjan(int u,int in){
dfn[u]=low[u]=++num;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v,i);
low[u]=min(low[u],low[v]);
if(dfn[u]<low[v]){
bridge[i]=bridge[i^1]=1;
}
}
else if(i!=(in^1))low[u]=min(low[u],dfn[v]);
}
}
void dfs(int u){
c[u]=edcc;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(c[v]||bridge[i])continue;
dfs(v);
}
}
void find_e_dcc(){
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,0);
for(int i=1;i<=n;i++)if(!c[i])++edcc,dfs(i);
for(int i=1;i<=tot;i++)if(bridge[i])add_v(c[ver[i]],c[ver[i^1]]);
}
void init(){
scanf("%d%d",&n,&m);
t=log(n)/log(2.0)+1;
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v),add(v,u);
}
}
void bfs(int s){
dep[s]=1;
queue<int>q;
q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=vhead[u];i;i=vnxt[i]){
int v=vver[i];
if(dep[v])continue;
dep[v]=dep[u]+1;
f[v][0]=u;
for(int i=1;i<=t;i++)f[v][i]=f[f[v][i-1]][i-1];
q.push(v);
}
}
}
int lca(int u,int v){
if(dep[u]>dep[v])swap(u,v);
for(int i=t;i>=0;--i)if(dep[u]<=dep[f[v][i]])v=f[v][i];
if(u==v)return u;
for(int i=t;i>=0;--i)if(f[v][i]!=f[u][i])u=f[u][i],v=f[v][i];
return f[u][0];
}
void prepare(){
init();
find_e_dcc();
bfs(1);
}
int solve(int u,int v){
u=c[u],v=c[v];
return dep[u]+dep[v]-2*dep[lca(u,v)];
}
int main(){
prepare();
int q;
scanf("%d",&q);
while(q--){
int u,v;
scanf("%d%d",&u,&v);
ss=0;
int ans=solve(u,v);
printf("%d\n",ans);
}
}
必经点
例题:交通实时查询系统
描述:某城市的交通堵塞问题非常严重,为解决这一问题,该城市建立了实时查询系统来检测所有交通情况。
该城市有 N 个交叉口,M 条道路,每条道路连接两个交叉口,且都是双向的。
实时查询系统的一个重要任务就是帮助司机找到从指定道路行驶到另一条指定道路必经的交叉口有多少个。
就是询问两条边之间的必经点有哪些,很明显也就是两点之间的割点数量
/*
因此求的是两条边之间经过的割点数量
我们只需要缩点之后用lca求两个点双连通分量之间的割点数量即可
由于求的是两条边之间经过的割点数量,因此还需要求出每条边属于的点双连通分量
*/
#define N 20500
#define M 405050
int n,m,s,head[N],vhead[N],ver[M],nxt[M],idx=1,dfn[N],low[N],num,dep[N],fa[N][16],dis[N],stack[N],top,vdcc,root,c[N],id[M],new_id[N],vis[N];
vector<int> dcc[N];
void add(int head[],int a,int b){
ver[++idx]=b,nxt[idx]=head[a],head[a]=idx;
}
void tarjan(int u){
dfn[u]=low[u]=++num;
stack[++top]=u;
if(u==root&&head[u]==0){
dcc[++vdcc].push_back(u);
return;
}
int cnt=0;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
if(dfn[u]<=low[v]){
cnt++;
if(root!=u||cnt>1)vis[u]=true;
vdcc++;
int z;
do{
z=stack[top--];
dcc[vdcc].push_back(z);
} while(z!=v);
dcc[vdcc].push_back(u);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
void bfs(int s){
queue<int>q;
q.push(s);
dep[s]=1;
dis[s]=(s>vdcc);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=vhead[u];i;i=nxt[i]){
int v=ver[i];
if(dep[v])continue;
dep[v]=dep[u]+1;
dis[v]=dis[u]+(v>vdcc);
fa[v][0]=u;
for(int i=1;i<=15;i++)
fa[v][i]=fa[fa[v][i-1]][i-1];
q.push(v);
}
}
}
int lca(int a,int b){
if(dep[a]<dep[b])swap(a,b);
for(int k=15;k>=0;k--)if(dep[fa[a][k]] >= dep[b])a=fa[a][k];
if(a==b)return a;
for(int k=15;k>=0;k--)if(fa[a][k]!=fa[b][k])a=fa[a][k],b=fa[b][k];
return fa[a][0];
}
int main(){
while(scanf("%d%d",&n,&m),n || m){
for(int i=1; i <= vdcc; i++) dcc[i].clear();
memset(head,0,sizeof head);
memset(vhead,0,sizeof vhead);
memset(dfn,0,sizeof dfn);
memset(vis,0,sizeof vis);
memset(id,0,sizeof id);
memset(dep,0,sizeof dep);
memset(fa,0,sizeof fa);
num=top=vdcc=0;idx=1;
while(m--){
int a,b;
scanf("%d%d",&a,&b);
add(head,a,b),add(head,b,a);
}
for(root=1;root<=n;root++)if(!dfn[root])tarjan(root);
int cnt=vdcc;
for(int i=1;i<=n;i++)
if(vis[i])new_id[i]=++cnt;
for(int i=1;i<=vdcc;i++){
for(int v=0;v<dcc[i].size();v++){
int x=dcc[i][v];
if(vis[x]){
add(vhead,i,new_id[x]);
add(vhead,new_id[x],i);
}
c[x]=i;
}
for(int v=0;v<dcc[i].size();v++){
int x=dcc[i][v];
for(int k=head[x];k;k=nxt[k]){
int y=ver[k];
if(c[y]==i)id[k/2]=i;//处理k/2代表是题目所给的k条边,因为建了反向边导致需要/2
}
}
}
for(int i=1;i<=cnt;i++)
if(!dep[i])
bfs(i);
scanf("%d",&s);
while(s--){
int a,b;
scanf("%d%d",&a,&b);
a=id[a],b=id[b];
int p=lca(a,b);
printf("%d\n",dis[a]+dis[b]-2*dis[p]+(p>vdcc));//若本身就是割点需要加上
}
}
return 0;
}
综合题目
1.network
给定一张 N 个点 M 条边的无向连通图,然后执行 Q 次操作,每次向图中添加一条边,并且询问当前无向图中“桥”的数量。
先找出
但这样做时间复杂度较低,我们可以采用并查集优化
不妨思考,若在节点
详细说,我们使用并查集,维护每一个节点的上一个没有被标记的边的位置,在每一次修改的时候就可以使用并查集一直向上跳,每一次修改后就把当前节点和当前被标记的那条边的节点的集合合并,这样总体复杂度就降为了log
int head[N],c[N],nxt[N],bridge[M],ver[M],tot=1,edcc,dfn[N],low[N],num,cnt,n,m,fa[N],lst[N];
int vhead[N],vnxt[N],vver[N],vtot=1,dep[N];
void add(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void dccadd(int u,int v){
vnxt[++vtot]=vhead[u],vver[vhead[u]=vtot]=v;
}
void tarjan(int u,int in){
dfn[u]=low[u]=++num;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v,i);
low[u]=min(low[u],low[v]);
if(dfn[u]<low[v]){
bridge[i]=bridge[i^1]=1;
}
}
else if((i^1)!=in)low[u]=min(low[u],dfn[v]);
}
}
void dfs(int u){
c[u]=edcc;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(c[v]||bridge[i])continue;
dfs(v);
}
}
void sd(){
tarjan(1,0);
for(int i=1;i<=n;i++)if(!c[i])++edcc,dfs(i);
for(int i=2;i<=tot;i++){
int u=ver[i],v=ver[i^1];
if(c[u]!=c[v])dccadd(c[u],c[v]);
}
}
void bfs(int s){
dep[s]=1;
queue<int>q;
q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=vhead[u];i;i=vnxt[i]){
int v=vver[i];
if(dep[v])continue;
lst[v]=u;
dep[v]=dep[u]+1;
q.push(v);
}
}
}
int get(int x) {
return x==fa[x]?x:fa[x] = get(fa[x]);
}
int change(int x, int y) {
int ans=0;
x=get(x),y=get(y);
while(x!=y) {
if(dep[x]<dep[y])swap(x,y);
if(x==1) break;
fa[x]=get(lst[x]);
ans++;
x=get(x);
}
return ans;
}
int main(){
int t=0;
while(~scanf("%d%d",&n,&m)&&(n||m)){
++t;
printf("Case %d:\n",t);
vtot=tot=1;
num=cnt=edcc=0;
memset(head,0,sizeof head);
memset(c,0,sizeof c);
memset(vhead,0,sizeof vhead);
memset(dfn,0,sizeof dfn);
memset(bridge,0,sizeof bridge);
memset(dep,0,sizeof dep);
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
sd();
bfs(1);
int ans=edcc-1;
for(int i=1;i<=edcc;i++)fa[i]=i;
int q;
scanf("%d",&q);
for(int i=1;i<=q;i++){
// puts("AS");
int u,v;
scanf("%d%d",&u,&v);
if(c[u]!=c[v])ans-=change(c[u],c[v]);
printf("%d\n",ans);
}
puts("");
}
}
圆桌骑士
题目描述
国王有时会在圆桌上召开骑士会议。
由于骑士的数量很多,所以每个骑士都前来参与会议的情况非常少见。
通常只会有一部分骑士前来参与会议,而其余的骑士则忙着在全国各地做英勇事迹。
骑士们都争强好胜,好勇斗狠,经常在会议中大打出手,影响会议的正常进行。
现在已知有若干对骑士之间互相憎恨。
为了会议能够顺利的召开,每次开会都必须满足如下要求:
1.相互憎恨的两个骑士不能坐在相邻的两个位置。
2.为了让投票表决议题时都能有结果(不平票),出席会议的骑士数必须是奇数。
3.参与会议的骑士数量不能只有 1 名。
如果前来参加会议的骑士,不能同时满足以上三个要求,会议会被取消。
如果有某个骑士无法出席任何会议,则国王会为了世界和平把他踢出骑士团。
现在给定骑士总数 n,以及 m 对相互憎恨的关系,求至少要踢掉多少个骑士。
分析
我们先建立出原图的补图,也即对每一对没有憎恨关系的骑士连边,根据题意,本题就是要求所有不在奇环上的点,这些点就应该被踢出
引理1:任意一个奇环一定只存在于一个
根据定义,采用反证法,若有两个节点
引理2:若一个
证明:设在
判定奇环的存在性,可以采用染色法,即我们对于整个
于是我们最终的做法变成了
- 建图,并求出所有的
- 判断每一个
的奇环存在性,若存在则将整个 的节点全部标记 - 最后没有被标记的节点就是被踢出的
int tot=1,head[N],nxt[N*N],ver[N*N],dcc,num,dfn[N],low[N],n,m,color[N],op,flag[N],ans,rt,vis[N];
vector<int>edcc[N];
int ljb[N][N];
stack<int>dc;
void add(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void tarjan(int u){
dc.push(u);
dfn[u]=low[u]=++num;
if(u==rt&&head[u]==0){
edcc[++dcc].push_back(u);
return ;
}
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
if(dfn[u]<=low[v]){
int z=0;
++dcc;
do{
z=dc.top();dc.pop();
edcc[dcc].push_back(z);
}while(z!=v);
edcc[dcc].push_back(u);
}
}
else low[u]=min(low[u],dfn[v]);
}
}
void clear(){
scanf("%d%d",&n,&m);
if(!(n||m))exit(0);
memset(head,0,sizeof head);
memset(vis,0,sizeof vis);
memset(ljb,0,sizeof ljb);
memset(dfn,0,sizeof dfn);
memset(color,0,sizeof color);
num=dcc=ans=0;tot=1;
for(int i=1;i<=n;i++)edcc[i].clear();
while(!dc.empty())dc.pop();
}
void init(){
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
ljb[u][v]=ljb[v][u]=1;
}
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
if(!ljb[i][j]){
add(i,j);
add(j,i);
}
}
}
}
void dfs(int u,int co){
color[u]=co;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(flag[v]){
if(!color[v])dfs(v,3-co);
else if(color[v]==co){
op=-1;
return ;
}
}
}
}
void find(){
for(int i=1;i<=dcc;i++){
int len=edcc[i].size();
for(int j=0;j<len;j++)flag[edcc[i][j]]=1,color[edcc[i][j]]=0;
op=0;
dfs(edcc[i][0],1);
for(int j=0;j<len;j++)flag[edcc[i][j]]=0,vis[edcc[i][j]]+=(op==-1);
}
for(int i=1;i<=n;i++)if(!vis[i])ans++;
}
void solve(){
clear();
init();
for(int i=1;i<=n;i++)if(!dfn[i])rt=i,tarjan(i);
find();
}
int main(){
// freopen("a1.in","r",stdin);
while(1){
solve();
printf("%d\n",ans);
}
}
欧拉路问题
定义:
- 给定一张无向图,若存在一条从
到 的路径,使得每条边都不重不漏经过恰好一次,则称这条路径为 到 的欧拉路 - 给定一张无向图,若存在一条路径使得从一个节点
出发不重不漏经过所有的边后又回到了 ,则称这条路为欧拉回路,存在欧拉回路的无向图被称为欧拉图
注:节点可以重复经过
定理: - 一张无向图存在一条欧拉路,当且仅当所有的节点的度数都为偶数且图连通,只有两个节点的度数为奇数,这两个节点就是欧拉路的两端
- 一张无向图为欧拉图,当且仅当图连通且所有节点度数都为偶数
在判定欧拉回路存在后,我们可以借助深度优先遍历和栈求出一种具体的欧拉回路的方案
伪代码如下
因为欧拉图中每个节点的度数为偶数,则经过该节点就一定存在一条未被访问的边可以离开该节点,故在上面的伪代码中调用
类似于图中,加粗边就是一条回路
需要注意的是,我们并不能够保证这条回路一定经过了所有的节点,于是我们采用了栈,在访问到一些节点的时候,会递归寻找其他回路,我们将所有的回路拼起来就得到了整个欧拉回路,这就是栈在起作用
拼接的方式就是直接嵌入,栈完成了这个嵌入的过程
这个程序的时间复杂度是
一个很简单的优化是,我们采用邻接表存图,将边被标记为已访问的过程可以改为
另外,由于递归层数是
int head[100010], ver[1000010], Next[1000010], tot,stack[1000010], ans[1000010]; // 模拟系统栈,答案栈
bool vis[1000010];
int n, m, top, t;
void add(int x, int y) {
ver[++tot] = y, Next[tot] = head[x], head[x] = tot;
}
void euler() {
stack[++top] = 1;
while (top > 0) {
int x = stack[top], i = head[x];
// 找到一条尚未访问的边
while (i && vis[i]) i = Next[i];
// 沿着这条边模拟递归过程,标记该边,并更新表头
if (i) {
stack[++top] = ver[i];
head[x] = Next[i];
vis[i] = vis[i ^ 1] = true;
}
// 与x相连的所有边均已访问,模拟回溯过程,并记录于答案栈中
else {
top--;
ans[++t] = x;
}
}
}
int main() {
cin >> n >> m;
tot = 1;
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d%d", &x, &y);
add(x, y), add(y, x);
}
euler();
for (int i = t; i; i--) printf("%d\n", ans[i]);
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析