【欧拉路】学习笔记
定义
欧拉路问题,俗称“一笔画”问题,即给定一张图,若存在一条从点 \(S\) 到点 \(T\) 的路径恰好不重不漏地经过每条边一次,则称该路径为 \(S\) 到 \(T\) 的欧拉路;特别地,若 \(S\) 和 \(T\) 为同一点,则称该路径为欧拉回路
判定
一张无向图有欧拉路,当且仅当所有点度数均为偶数(此时图为欧拉图)或有且仅有两个点度数为奇数(此时图为半欧拉图);有欧拉回路,当且仅当所有点度数均为偶数
一张有向图有欧拉路,当且仅当所有点出度等于入度(此时图为欧拉图)或有且仅有一个点入度恰比出度大 \(1\),一个点入度恰比出度小 \(1\)(此时图为半欧拉图);有欧拉回路,当且仅当所有点入度等于出度
求法
使用 \(DFS\) 遍历即可求出一条欧拉路的可行路径
如果有且仅有两个点度数为奇数,从其中一个点开始遍历;否则因为有回路,任意一点开始遍历即可
考虑经过一个点时,将这个点所在的回路全部加入路径中,最后再加入这个点
直接朴素实现需要一遍 \(DFS\) 找回路并存储,一遍 \(DFS\) 将回路加入最终欧拉路径中,较为麻烦
考虑一个点的回溯代表的含义:要找的欧拉路在这个点之后的点已经走完了
那么这个时候存下这个点,最终即可得到一条 反着的 欧拉路
于是我们在 \(DFS\) 即将回溯的时候直接把点放进一个栈中,最后弹栈就是要找的欧拉路
可能会有一个疑问:为什么不能在 \(DFS\) 的过程中经过一个点就直接把一个点放入路径中,只能在回溯的时候记录?
考虑半欧拉图上搜到某点与那个入度比出度大1/小1的点相连,此时若直接存且不幸先搜到这个奇数点将会直接把这个点放进路径中,相当于提前走上了一条不归路,显然不对;
而在栈的存法下这个奇数点是最先入栈的,即最后输出的,没有问题
模板题代码:
#include<bits/stdc++.h>
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
inline int read()
{
int x=0,fu=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')fu=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*fu;
}
void write(int x)
{
if(x<0) x=-x,putchar('-');
if(x<10){putchar(x+'0');return;}
write(x/10);putchar(x%10+'0');
}
const int N=1e5+10;
vector<pii>ed[N];
int n,m;
int in[N],out[N];
stack<int>st;
int now[N];
void dfs(int u)
{
for(int v=now[u];v<ed[u].size();v=now[u])
{
if(ed[u][v].se) continue;
ed[u][v].se=1;
now[u]=v+1;
dfs(ed[u][v].fi);
}
st.push(u);
}
signed main()
{
n=read(),m=read();
for(int i=1;i<=m;i++)
{
int u=read(),v=read();
ed[u].push_back({v,0});
out[u]++,in[v]++;
}
int sg1=0,sg2=0;
for(int i=1;i<=n;i++)
{
if(in[i]==out[i]) continue;
if(in[i]-out[i]==1)
{
if(sg1)
{
puts("No");
return 0;
}
sg1=i;
}
else if(in[i]-out[i]==-1)
{
if(sg2)
{
puts("No");
return 0;
}
sg2=i;
}
else
{
puts("No");
return 0;
}
}
if((!sg1&&sg2)||(sg1&&!sg2))
{
puts("No");
return 0;
}
for(int i=1;i<=n;i++) sort(ed[i].begin(),ed[i].end());
if(sg1) dfs(min(sg1,sg2));
else dfs(1);
while(!st.empty()) write(st.top()),putchar(' '),st.pop();
return 0;
}
后记:网络流和欧拉路当前弧优化的区别
在复健图论时无意中发现欧拉路的当前弧优化与 \(dinic\) 不同,然后发现自己其实并没有完全学懂这两个算法
网络流:
for(int i=now[u];~i&&res;i=nxt[i])
{
...
now[u]=i;
...
}
这里的 \(now\) 不能直接变下一条边而只能停在“当前”,因为当前这条边的流量不一定流完,即网络流有可能再用这条边
如果扫到这条边后直接跳了当前弧就会导致增广路的利用其实是不到位的,甚至可能导致一直有增广路但是一直走不到的极端情况
欧拉路:
for(int v=now[u];v<ed[u].size();v=now[u])
{
...
now[u]=v+1;
...
}
这里的 \(now\) 就必须直接变下一条边,因为一条边只需要走一次,下一条边才是“当前”,即 欧拉路不可能再用这条边
如果不跳下一条边这条无用的边就会被再次找,当前弧优化的意义就有限了