D164【模板】无向图 欧拉路径 欧拉回路 P2731 [USACO3.3] 骑马修栅栏

D164【模板】无向图 欧拉路径 欧拉回路 P2731 [USACO3.3] 骑马修栅栏_哔哩哔哩_bilibili

 

每条边恰好经过一次的路径称为欧拉路径,每条边恰好经过一次又回到起点的路径称为欧拉回路,通称一笔画问题

无向图的欧拉回路的判定:所有点的度都是偶数(即 0 个奇点),如图 1

无向图的欧拉路径的判定:有且仅有两个点的度是奇数(即 2 个奇点),这两个点分别作起点与终点,如图 2

图 3 存在 4 个奇点,不满足条件,不存在欧拉路径

 

P2731 [USACO3.3] 骑马修栅栏 Riding the Fences - 洛谷

无向连通图,n($\le500$) 个点,m($\le1024$) 条边,输出字典序最小的欧拉路径

思路

点数很少,考虑用邻接矩阵 $g[i][j]$ 存图,从起点 $s$ 开始遍历,从小到大枚举邻接点 $j$,就能满足字典序最小

题目保证有解,首先找出度数为奇数的起点 $s$,然后深搜遍历,每经过一条边就先删除它,避免走两次

路径不能先序输出,要后序时刻入栈,最后从栈中弹出,为什么?

 

如果先序输出,结果为 1,2,5,3,4,1 不对。后序入栈,序列为 5,2,1,4,3,1,弹出结果 1,3,4,1,2,5, 正确

因为先序输出通往奇数点的路径,后面路径无法到达

当一个点的全部出边都经过之后,表示这个点不能再到达其他点,为了到达其他点,只能让这个点先入栈,在其他点之后出栈

时间复杂度:需要遍历 n 个点,每个点需要枚举 n 次邻接点,所以 $O(n^2)$

// 欧拉路径 欧拉回路 O(n^2)
#include<bits/stdc++.h>
using namespace std;

const int n=500,N=505;
int m,du[N]; //
stack<int> path;
int g[N][N]; //邻接矩阵

void dfs(int x){
  for(int i=1; i<=n; i++){
    if(g[x][i]){ //如果边(x,i)存在
      g[x][i]--, g[i][x]--; //删除边
      dfs(i); //走到i
    }        
  }
  path.push(x); //后序记录路径
}
int main(){
  cin>>m;
  for(int a,b;m--;){
    cin>>a>>b;
    g[a][b]++, g[b][a]++; //累计两点之间的边
    du[a]++, du[b]++; //累计端点的度
  }
  
  int start=1;
  while(!du[start]) start++; //排除度数为0的点
  for(int i=1; i<=n; i++)if(du[i]&1){ //查找度数为奇数的点
    start=i; break;
  }
  
  dfs(start);
  
  while(!path.empty())printf("%d\n",path.top()),path.pop();
}

 

无向边即双向边,用 vector 存邻结点,对每个点的邻接点排序后,如图

关键是预处理每条边的反边的终点的下标位置,以便搜索时,对正反边同时打上删除标记

预处理:$e[1][4].idx=pos[e[1][4].to]++;\;→ e[1][4].idx=0$;即边 (1,4) 的反边 (4,1) 的终点 1 的下标为 0

    $e[3][4].idx=pos[e[3][4].to]++;\;→ e[3][4].idx=1$;即边 (3,4) 的反边 (4,3) 的终点 3 的下标为 1

打标记:$e[1][4].del=true;\;e[e[1][4].to][e[1][4].idx].del=e[4][\color{red}{0}].del=true$;

    $e[3][4].del=true;\;e[e[3][4].to][e[3][4].idx].del=e[4][\color{red}{1}].del=true$;

这样,排序决定时间复杂度 $O(mlogm)$

// 欧拉路径 欧拉回路 O(mlogm)
#include<bits/stdc++.h>
using namespace std;

const int n=500,N=505;
int m,du[N];
stack<int> path;
struct E{
  int to;  //终点
  int idx; //反边的终点的下标
  bool del; //删除标记
};
vector<E> e[N]; //邻接表
int pos[N]; //pos[x]记录点x的下标编号
int p[N];   //p[x]表示点x当前处理到第几条出边,初始值为0,相当于全局指针

void dfs(int x){
  for(int i=p[x]; i<e[x].size(); i=p[x]){
    p[x]=i+1; //全局指针,指向下一条边
    E &a=e[x][i];
    if(!a.del){
      a.del=e[a.to][a.idx].del=true; //打删除标记
      dfs(a.to);
    }
  }
  path.push(x); //后序记录路径
}
int main(){
  scanf("%d",&m);
  for(int i=1,a,b; i<=m; ++i){
    scanf("%d %d",&a,&b);
    e[a].push_back({b,0,0});
    e[b].push_back({a,0,0});
    ++du[a]; ++du[b];
  }
  
  for(int i=1; i<=n; ++i)if(!e[i].empty())
    sort(e[i].begin(),e[i].end(),[&](E a,E b){return a.to<b.to;}); //点i的邻接点升序
  
  for(int i=1; i<=n; ++i)for(int j=0; j<e[i].size(); ++j)
    e[i][j].idx=pos[e[i][j].to]++; //记录e[i][j]的反边的终点的下标
  
  int start=1;
  while(!du[start]) start++; //排除度数为0的点
  for(int i=1; i<=n; i++)if(du[i]&1){ //查找度数为奇数的点
    start=i; break;
  }
  
  dfs(start);

  while(!path.empty())printf("%d\n",path.top()),path.pop();
}
 

 

posted @ 2026-05-26 06:49  董晓  阅读(54)  评论(0)    收藏  举报