[算法鉴赏][图论]欧拉路问题---Hierholzer 算法

欧拉路径与欧拉回路
1. 定义
- 欧拉路径(Eulerian path):经过图中每条边恰好一次的路径。
- 欧拉回路(Eulerian circuit):经过图中每条边恰好一次的回路。
- 欧拉图(Eulerian graph):存在欧拉回路的图。
2. 性质
对于欧拉图,每个顶点的度数具有特殊性质(in_i 入度,out_i 出度,deg_i 度):
- 有向图:欧拉回路中,每个顶点的出度等于入度(
in_i == out_i)。 - 无向图:欧拉回路中,每个顶点的度数为偶数(
deg_i % 2 == 0)。
对于欧拉路径(不要求回到起点):
| 图类型 | 度数条件 |
|---|---|
| 有向图 | ① 至多一个顶点出度比入度大 1(起点 out_i - in_i = 1)② 至多一个顶点入度比出度大 1(终点 in_i - out_i = 1)③ 其余顶点入度等于出度 |
| 无向图 | 至多两个顶点度数为奇数(路径起点和终点),其余顶点度数为偶数 |
换言之,上述性质可用于判断欧拉路径是否存在。
3. Hierholzer 算法
在图中寻找欧拉路径(或回路)需要满足:
- 每个顶点的度数满足上述条件。
- 若是欧拉路径,需确定起点。
复杂度:( O(m) )
算法流程:
- 从某个起点开始 DFS。
- 对于从
x出发的每条边(x, y),若未访问,则标记该边,递归y,随后将x入栈。 - 倒序输出栈中顶点即为一条欧拉路径。
不同存图方式的实现
① 邻接矩阵
int mp[N][N];
void dfs(int u) {
for (int v = 1; v <= n; v++) {
if (mp[u][v] > 0) {
mp[u][v]--;
mp[v][u]--;
dfs(v);
}
}
path.push_back(u);
}
优点:简单易写
缺点:对于内存来说还是它极端了
2. 邻接表
int in[maxn], out[maxn], del[maxn];
vector<int> pic[maxn];
stack<int> ck;
void dfs(int p) {
for (int i = del[p]; i < pic[p].size(); i = del[p]) {
del[p] = i + 1;
dfs(pic[p][i]);
}
ck.push(p);
}
优点:简单易写
缺点:对于内存来说还是它极端了
2. 邻接表
int in[maxn], out[maxn], del[maxn];
vector<int> pic[maxn];
stack<int> ck;
void dfs(int p) {
for (int i = del[p]; i < pic[p].size(); i = del[p]) {
del[p] = i + 1;
dfs(pic[p][i]);
}
ck.push(p);
}
del 是弧优化,用于跳过上一次遍历过的边,不用损坏原树
优点:通用主流
缺点:容易退化成为 ( O(m^2) )
3. 链式前向星
// int head[N], idx;
// bool used[M];
// struct edge{
// int v, nxt;
// } adj[M];
auto dfs = [&](auto&& self, int u) -> void {
for (int& i = head[u]; ~i; ) {
int j = (t == 1 ? i / 2 + 1 : i + 1);
if (used[j]) {
i = adj[i].nxt;
continue;
}
int v = adj[i].v, id = (t == 1 && (i & 1)) ? -j : j;
used[j] = true;
i = adj[i].nxt;
self(self, v);
ans.push_back(id);
}
};
优点:防止卡常
缺点:逻辑没捋顺容易写挂
4. 练习及赏析
例题 1:模板题
P2731 [USACO3.3] 骑马修栅栏 Riding the Fences
题意也是十分简明,我们只要建图,然后走流程求欧拉路或是欧拉回路。需要注意的是要求输出字典序最小的,那么我们就需要对于每一个 pic[i] 排序。
#include<bits/stdc++.h>
#define ll long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define dep(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
const int maxn=3e3+7;
int n=1,m,del[maxn];
int vis[maxn][maxn];
vector<int>pic[maxn];
stack<int>path;
int deg[maxn],st=-1;
void dfs(int p)
{
for(int i=del[p];i<pic[p].size();i=del[p])
{
int u=pic[p][i];
del[p]=i+1;
if(vis[p][u]==0)continue;
vis[p][u]--;vis[u][p]--;
dfs(u);
}
path.push(p);
}
int main()
{
cin>>m;
int minp=INT_MAX;
rep(i,1,m)
{
int u,v;
cin>>u>>v;
pic[u].push_back(v);
pic[v].push_back(u);
deg[u]++,deg[v]++;
vis[u][v]++,vis[v][u]++;
minp=min(minp,min(u,v));
}
rep(i,1,500)sort(pic[i].begin(),pic[i].end());
rep(i,1,500)
{
if(deg[i]&1)
{
st=i;
break;
}
}
if(st==-1) st=minp;
dfs(st);
while(!path.empty())
{
cout<<path.top()<<'\n';
path.pop();
}
return 0;
}
例题 2:最短子段组合
一看找不到思路,实际上,我们把每两个字符直接连上有向边来进行建图,只需要找到图的欧拉路就可以找到最短的组合了。特别的,出题人并没有保证一定可以建成一个联通的图,对于此题还需要判断联通性。字典序最小的方案也需要排序求得。
#include<bits/stdc++.h>
#define ll long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define dep(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
const int maxn=150;
int n,vis[maxn][maxn],del[maxn];
vector<int>pic[maxn];
stack<int>path;
int st=200,deg[maxn];
int bcj[maxn];
void dfs(int p)
{
for(int i=del[p];i<pic[p].size();i=del[p])
{
int u=pic[p][i];
del[p]=i+1;
if(vis[p][u]==0)continue;
vis[p][u]--;
vis[u][p]--;
dfs(u);
}
path.push(p);
}
int find(int x)
{
if(bcj[x]==x)return x;
return bcj[x]=find(bcj[x]);
}
int main()
{
rep(i,0,149) bcj[i]=i;
cin>>n;
rep(i,1,n)
{
char a,b;
cin>>a>>b;
int u=(int)a,v=(int)b;
pic[u].push_back(v);
pic[v].push_back(u);
vis[u][v]++;vis[v][u]++;
deg[u]++;deg[v]++;
st=min(st,min(u,v));
int fu=find(u),fv=find(v);
if(fu!=fv) bcj[fu]=fv;
}
int root=-1;
rep(i,65,122)
{
if(deg[i]){
int r=find(i);
if(root==-1) root=r;
else if(root!=r){
cout<<"No Solution";
return 0;
}
}
}
int odd_cnt=0,start=st;
rep(i,65,122)
{
if(deg[i]&1)
{
odd_cnt++;
if(odd_cnt==1) start=i;
}
}
if(odd_cnt!=0&&odd_cnt!=2)
{
cout<<"No Solution";
return 0;
}
rep(i,65,122) sort(pic[i].begin(),pic[i].end());
dfs(start);
if(path.size()==n+1)
{
while(!path.empty())
{
cout<<char(path.top());
path.pop();
}
}
else
{
cout<<"No Solution";
}
return 0;
}
例题 3:使用加边技巧的题目
我们需要一条 ( s \to t ) 的欧拉通路。增设一条虚拟边 ( s-t ),翻转 s、t 度数的奇偶性,此时问题转化为构造欧拉回路(全部点度数为偶数),最小化新增边花费。
数轴权重要性质
- 边权为两点距离 ( |x-y| );
- 将所有奇点从小到大排序之后相邻两两配对的总代价最小。
- 配对 ([l,r]) 的同时合并区间内结点,可以减少连通块数目。
连通块搭桥
- 奇点配对结束之后图可能多个连通块;
- 相邻编号结点之间边权为 1,邮递员需要往返经过搭桥边,所以每条最小生成树边花费为 ( 2 \times 1 )。
解题步骤
- 累加所有丁香边权得到固定基础花费
base;统计每个点度数,并用并查集连通丁香边; - 枚举每一个终点 t:
- 复制原始度数数组、连通块并查集;虚拟增加 ( s-t ),翻转两点奇偶;
- 搜集全部奇点,相邻配对累加花费,并且合并配对区间里面所有结点;
- 提取现存所有连通块,相邻连通块建边,跑 Kruskal‑MST;
- 连通代价等于 MST 权值 × 2;
- ( ans = base + 配对代价 + 连通搭桥代价 )
- 输出所有终点对应的答案。
#include<bits/stdc++.h>
#define ll long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define dep(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
const int maxn=2505;
int n,m,s;
ll base;
int fa[maxn],deg[maxn];
int find(int x)
{
if(fa[x]!=x) fa[x]=find(fa[x]);
return fa[x];
}
void unite(int x,int y)
{
x=find(x),y=find(y);
if(x!=y) fa[y]=x;
}
ll calc(int t)
{
int d[maxn];
int f[maxn];
rep(i,1,n) d[i]=deg[i],f[i]=fa[i];
d[s]^=1; d[t]^=1;
vector<int> odd;
rep(i,1,n)
if(d[i]&1) odd.push_back(i);
ll pair_cost=0;
for(int i=0;i<odd.size();i+=2)
{
int l=odd[i],r=odd[i+1];
pair_cost+=r-l;
rep(j,l,r-1)
{
int x=find(f[j]);
int y=find(f[j+1]);
if(x!=y) f[y]=x;
}
}
unordered_set<int> blk_set;
rep(i,1,n) blk_set.insert(find(f[i]));
vector<int> blk(blk_set.begin(),blk_set.end());
int sz=blk.size();
if(sz<=1) return base+pair_cost;
int f_mst[maxn];
rep(i,0,sz-1) f_mst[i]=i;
auto find_mst=[&](auto&& self,int x)->int{
if(f_mst[x]!=x) f_mst[x]=self(self,f_mst[x]);
return f_mst[x];
};
ll mst_sum=0;
rep(i,0,sz-2)
{
int u=i,v=i+1;
int fu=find_mst(find_mst,u);
int fv=find_mst(find_mst,v);
if(fu!=fv)
{
f_mst[fv]=fu;
mst_sum+=1;
}
}
ll bridge_cost=mst_sum*2;
return base+pair_cost+bridge_cost;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>m>>s;
rep(i,1,n) fa[i]=i;
base=0;
memset(deg,0,sizeof deg);
rep(i,1,m)
{
int u,v;
cin>>u>>v;
base+=abs(u-v);
deg[u]++,deg[v]++;
unite(u,v);
}
rep(t,1,n)
{
cout<<calc(t)<<" ";
}
return 0;
}
关于欧拉路的加边处理
通常来说我们在图论建模的时候,总是出现不连通或是并不满足欧拉路存在的图。如果这时候发现需要使用欧拉路来求解,那我们就要进行如下操作:
-
对于每一个连通块内的欧拉路
连通块内未必满足欧拉路的存在,那我们只需要加重复边。比如通过加边使两个奇度点变为偶度点,这样就会使图满足欧拉路的存在性。相应的,加边时要注意新增的代价,尽可能的去构造欧拉回路,这样会更好处理。 -
对于连通块之间的加边处理
在每一个连通块内都满足欧拉路的前提下,我们要使整张图都有欧拉(回)路,就要在连通块之间加上两条重复边,可以将连通块当作点处理,从 A 走到 B 之后还需要一条边返回(相应的代价要 ×2)。要使代价最小,就需要使用最小生成树 MST 得到最小开销(同样是把连通块当作点处理)。
第一次写算法教程,格式由 deepseek 负责,后期会学 markdown。
感谢观看!
—— 2026/8/12 15:59

浙公网安备 33010602011771号