欧拉图 学习笔记
欧拉图 学习笔记
离散课没有认真听,但是做到了需要使用欧拉路径相关算法的题目,索性学了一下,弥补一下图论板块基础内容的空缺。
欧拉图
分为有向图和无向图两种情况。
必要条件
无论如何,都需要满足图联通的条件,有些时候题目并不保证这个条件成立,所以需要自己并查集判断一下。
欧拉路径
有向图中,满足:有且仅有两个点的入度不相等,且其差值只为 1。
无向图中:满足有且仅有两个奇数度数的点。
有向图
Fleury
“避桥法”,顾名思义,在 dfs 的过程中,我们优先走非桥边,这样可以保证把回路遍历完之后再前进,时间复杂度从线性到平方,取决于是否提前用 tarjan 预处理出割边。
Code(AI写的)
#include <iostream>
#include <vector>
#include <stack>
#include <list>
using namespace std;
bool isBridge(int u, int v, vector<list<int>>& adj) {
// 暂时删除边 (u, v)
adj[u].remove(v);
adj[v].remove(u);
// 使用DFS检查图是否仍然连通
vector<bool> visited(adj.size(), false);
stack<int> dfsStack;
dfsStack.push(u);
visited[u] = true;
while (!dfsStack.empty()) {
int node = dfsStack.top();
dfsStack.pop();
for (int neighbor : adj[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
dfsStack.push(neighbor);
}
}
}
// 恢复边 (u, v)
adj[u].push_back(v);
adj[v].push_back(u);
// 检查是否所有顶点都已访问
for (int i = 0; i < adj.size(); ++i) {
if (!visited[i] && !adj[i].empty()) {
return true; // 如果有未访问的顶点,则 (u, v) 是桥
}
}
return false; // 否则 (u, v) 不是桥
}
void fleury(int start, vector<list<int>>& adj, vector<int>& path) {
path.push_back(start);
while (!adj[start].empty()) {
int next = adj[start].front();
adj[start].remove(next);
adj[next].remove(start);
if (isBridge(start, next, adj)) {
// 如果 (start, next) 是桥,则恢复边并选择另一条边
adj[start].push_back(next);
adj[next].push_back(start);
continue;
}
path.push_back(next);
start = next;
}
}
int main() {
vector<list<int>> adj = {
{1, 2}, // 0
{0, 2, 3}, // 1
{0, 1, 3}, // 2
{1, 2} // 3
};
int start = 0; // 选择起点
vector<int> path;
fleury(start, adj, path);
for (int v : path) {
cout << v << " ";
}
return 0;
}
Hierholzer
CP 领域比较主流的求欧拉路径和回路的算法,核心思想在于利用 dfs 的回溯和递归。在对于一个节点访问完毕其所有出边之后,把节点加入栈中。这样整个算法结束之后,栈里面的序号序列就是欧拉路径序列的反序。
Code(自己写的)
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
vector<int> e[N];
int st[N],in[N];
int n,m;
int s[N],top;
inline void dfs(int x)
{
while(st[x]<e[x].size())dfs(e[x][st[x]++]);
s[++top]=x;
}
inline void solve()
{
cin>>n>>m;
for(int i=1,u,v;i<=m;++i)
{
cin>>u>>v;
e[u].push_back(v);
in[v]++;
}
int S=0;
for(int i=1;i<=n;++i)
{
if(abs((int)e[i].size()-in[i])>1)return cout<<"No",void();
if(e[i].size()>in[i])
{
if(S)return cout<<"No",void();
else S=i;
}
}
for(int i=1;i<=n;++i)sort(e[i].begin(),e[i].end());
dfs(S?S:1);
if(top!=m+1)return cout<<"No",void();
for(int i=m+1;i>=1;--i)cout<<s[i]<<' ';
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
return solve(),0;
}
无向图
对于 Hierholzer 算法来讲,有向图和无向图的代码实现差别其实不大,主要的不同就是在于“拆边”,由于这里建的是双向边,但是只能走一次,所以在从一个方向访问完了一条边之后,同样得将其反边删去。
- 如果用邻接矩阵实现,那么就是简单地打上一个标记即可;
- 如果是前向星实现,那么就要涉及到网络流里面通过异或获得反边的 trick;
- 还有一种方法,就是用 \(n\) 个 set存边,这样可以 \(O(\log n)\) 地进行删除。
邻接矩阵的办法过于 trival,所以下面仅仅展示用前向星和 set 的写法。
前向星
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int head[N<<2],nex[N<<2],to[N<<2];
int tmp[N<<2];
int tote=1;
//无向图中充要条件是:要么度数全都是偶数,这时形成欧拉回路 ;要么就有且只有两个奇度数点,这时是普通的欧拉路径
//这是用前向星实现的版本
//并且根据CF原题的要求输出了边的编号,这里用map存
inline void add(int u,int v)
{
tote++;
nex[tote]=head[u];
head[u]=tote;
to[tote]=v;
}
int vis[N<<1];
vector<int> stk;
map<pair<int,int> , int> ans;
inline void dfs(int x)
{
for(;head[x];head[x]=nex[head[x]])
{
if(vis[head[x]])continue;
vis[head[x]]=vis[head[x]^1]=1;
dfs(to[head[x]]);
}
stk.push_back(x);
}
int a[N],b[N];
int n;
inline void solve()
{
cin>>n;
int cnt=0;
for(int i=1;i<=n;++i)
{
cin>>a[i]>>b[i];
tmp[++cnt]=a[i],tmp[++cnt]=b[i];
}
sort(tmp+1,tmp+cnt+1);
cnt=unique(tmp+1,tmp+cnt+1)-tmp-1;
vector<int> deg(2*cnt+2,0);
for(int i=1;i<=n;++i)
{
a[i]=lower_bound(tmp+1,tmp+cnt+1,a[i])-tmp;
b[i]=lower_bound(tmp+1,tmp+cnt+1,b[i])-tmp;
add(a[i],b[i]+cnt),add(b[i]+cnt,a[i]);
deg[a[i]]++,deg[b[i]+cnt]++;
ans[{a[i],b[i]+cnt}]=ans[{b[i]+cnt,a[i]}]=i;
}
int count=0,S=0;
for(int i=1;i<=2*cnt;++i)
{
if(deg[i]&1)
{
count++;
if(!S)S=i;
}
}
if(count&&count!=2)return cout<<"NO\n",void();
if(!S)for(int i=1;i<=2*cnt;++i)if(deg[i]){S=i;break;}
dfs(S);
if(stk.size()!=n+1)return cout<<"NO\n",void();
reverse(stk.begin(),stk.end());
cout<<"YES\n";
for(int i=0;i<n;++i)
cout<<ans[{stk[i],stk[i+1]}]<<" \n"[i==n-1];
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
solve();
return 0;
}
set
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int tmp[N<<1];
vector<int> stk;
set<int> e[N<<1];
map<pair<int,int> , int> ans;
//无向图中充要条件是:要么度数全都是偶数,这时形成欧拉回路 ;要么就有且只有两个奇度数点,这时是普通的欧拉路径
//这是用set实现的版本 相比前向星,写起来十分方便,唯一不足的地方可能在于常数较大
//并且根据CF原题的要求输出了边的编号,这里用map存
inline void dfs(int x)
{
while(e[x].size())
{
int v=*e[x].begin();
e[x].erase(v),e[v].erase(x);
dfs(v);
}
stk.push_back(x);
}
int a[N],b[N];
int n;
inline void solve()
{
cin>>n;
int cnt=0;
for(int i=1;i<=n;++i)
{
cin>>a[i]>>b[i];
tmp[++cnt]=a[i],tmp[++cnt]=b[i];
}
sort(tmp+1,tmp+cnt+1);
cnt=unique(tmp+1,tmp+cnt+1)-tmp-1;
vector<int> deg(2*cnt+2,0);
for(int i=1;i<=n;++i)
{
a[i]=lower_bound(tmp+1,tmp+cnt+1,a[i])-tmp;
b[i]=lower_bound(tmp+1,tmp+cnt+1,b[i])-tmp;
e[a[i]].insert(b[i]+n),e[b[i]+n].insert(a[i]);
deg[a[i]]++,deg[b[i]+cnt]++;
ans[{a[i],b[i]+cnt}]=ans[{b[i]+cnt,a[i]}]=i;
}
int count=0,S=0;
for(int i=1;i<=2*cnt;++i)
{
if(deg[i]&1)
{
count++;
if(!S)S=i;
}
}
if(count&&count!=2)return cout<<"NO\n",void();
if(!S)for(int i=1;i<=2*cnt;++i)if(deg[i]){S=i;break;}
dfs(S);
if(stk.size()!=n+1)return cout<<"NO\n",void();
reverse(stk.begin(),stk.end());
cout<<"YES\n";
for(int i=0;i<n;++i)
cout<<ans[{stk[i],stk[i+1]}]<<" \n"[i==n-1];
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
solve();
return 0;
}
Bonus
实际上如果要求输出边的编号而非节点编号,可以把前驱节点的编号作为 dfs 的参数,然后在相应应该入栈的时候把边的编号入栈即可。
(感谢zeq大绳的教诲)
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int head[N<<2],nex[N<<2],to[N<<2];
int tmp[N<<2];
int tote;
inline void add(int u,int v)
{
tote++;
nex[tote]=head[u];
head[u]=tote;
to[tote]=v;
}
int vis[N<<1];
vector<int> stk;
inline void dfs(int pre_edge,int x)
{
for(;head[x];head[x]=nex[head[x]])
{
if(vis[head[x]])continue;
vis[head[x]]=vis[head[x]^1]=1;
dfs(head[x]>>1,to[head[x]]);
}
if(pre_edge)stk.push_back(pre_edge);
}
int a[N],b[N];
int n;
inline void reset(int cnt)
{
tote=1;
stk.clear();
for(int i=0;i<=2*cnt;++i)head[i]=0;
for(int i=0;i<=2*n+10;++i)vis[i]=0;
}
inline void solve()
{
cin>>n;
int cnt=0;
for(int i=1;i<=n;++i)
{
cin>>a[i]>>b[i];
tmp[++cnt]=a[i],tmp[++cnt]=b[i];
}
sort(tmp+1,tmp+cnt+1);
cnt=unique(tmp+1,tmp+cnt+1)-tmp-1;
reset(cnt);
vector<int> deg(2*cnt+2,0);
for(int i=1;i<=n;++i)
{
a[i]=lower_bound(tmp+1,tmp+cnt+1,a[i])-tmp;
b[i]=lower_bound(tmp+1,tmp+cnt+1,b[i])-tmp;
add(a[i],b[i]+cnt),add(b[i]+cnt,a[i]);
deg[a[i]]++,deg[b[i]+cnt]++;
}
int count=0,S=0;
for(int i=1;i<=2*cnt;++i)
{
if(deg[i]&1)
{
count++;
if(!S)S=i;
}
}
if(count&&count!=2)return cout<<"NO\n",void();
if(!S)for(int i=1;i<=2*cnt;++i)if(deg[i]){S=i;break;}
dfs(0,S);
if(stk.size()!=n)return cout<<"NO\n",void();
reverse(stk.begin(),stk.end());
cout<<"YES\n";
for(int i=0;i<n;++i)
cout<<stk[i]<<" \n"[i==n-1];
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
int T;cin>>T;
while(T--)solve();
return 0;
}
汉密尔顿路径和欧拉路径
实际上很多时候如果建模稍稍偏差,那么本可以用欧拉路径解决的问题,就会变成 NP-Hard 的 Hamilton 路径问题。
图的构成要素无非两点:点和边,如果有一个问题,要求在给定限制条件下用上所有给定的 \(n\) 个元素组成一个序列。
- 如果把这 \(n\) 个元素作为点,那这就是个 Hamilton 路径问题了。
- 如果把其视为边,那么就变成了 Euler 路径问题,可以正常求解较大范围的数据。
以上例子见 洛谷P1127 和 CF2210E。
本文来自博客园,作者:Hanggoash,转载请注明原文链接:https://www.cnblogs.com/Hanggoash/p/18907711

浙公网安备 33010602011771号