欧拉路与欧拉图 学习笔记
djwj233
2021-08-01 20:14:45
### 欧拉图的相关概念
设 $G=(V,E)$ 是一张图:
- 如果 $G$ 中存在一条**包含所有边的简单回路**,那么我们称此回路为**欧拉回路**(*Euler circuit*),$G$ 为**欧拉图**(*Euler graph*)或 **E 图**。
- 如果 $G$ 中存在一条**包含所有边的简单路**,那么我们称此回路为**欧拉路**(*Euler path*),$G$ 为**半欧拉图**(*semi Euler graph*)。
- 特别地,我们定义一张**平凡图**(*trivial graph*)(只由一个孤立结点组成的图)为欧拉图。(不清楚对于一般的零图是否有此结论)
注意,**半欧拉图中不包含欧拉回路**。
结合一笔画问题,**从任意一个点开始**都可以一笔画完的图是欧拉图,必须**从某个点开始**才能一笔画完的图是半欧拉图。
要注意区分欧拉图与**哈密尔顿图**(*Hamilton graph*)的差异:
哈密尔顿回路指从一个点出发,不重不漏地经过**每个点**的回路,而欧拉回路的定义中是**边**。
### 欧拉图的基本性质
- **性质:一张无向连通图 $G$ 是非平凡的欧拉图,当且仅当它可被表示为一些环的并**。
证明:把欧拉回路分段,使每一段都是简单环。必要性同理。
- **性质:一张无向连通图 $G$ 是欧拉图,当且仅当图中每个结点的度数均为偶数**。(常用于欧拉图的判定,下同)
证明:**(必要性)**设 $G$ 有欧拉回路 $v_1\ e_1\ v_2\ e_2\cdots\ v_m\ e_{m}\ (v_1)$。
我们发现,$v_i$ 的出现一次便意味着存在与之相连的两条边 $e_{i-1}$ 和 $e_i$。(特别地,$v_1$ 的出现是 $e_1$ 和 $e_m$)
那么,对所有的点 $x$,若它在回路中出现了 $k$ 次,那么它的度 $d(x)$ 就是 $2k$,一定是一个偶数。
**(充分性)**我们任取一个度数不为 $0$ 的点 $v_1$,从 $v_1$ 出发经过与其相连的一条边 $e_1$ 到达 $v_2$。
由于 $v_2$ 的度数为偶数,且一定大于零(存在一条边 $e_1$ 与之相连),因此 $d(v_2)\geq 2$,至少还有一条边 $e_2$ 与之相连。
设 $e_2$ 的另一个结点是 $v_3$,同上过程可以找到 $v_4$ 与之相连,以此类推。
由于边数是有限的,因此这个过程一定会停止。停止的唯一可能是在 $v_1$ 处(由于有 $e_1$ 的存在),因此我们得到了一条回路 $C$。
删去这条回路和当前度数为 $0$ 的点,我们得到一张新图 $G^\prime$。
若 $G$ 不是空图,那么类似对必要性的证明,我们可以得到 $C$ 把每个点的度数都减去了一个非负偶数,因此 $G^\prime$ 中每个结点的度数仍均为偶数。
由于 $G$ 连通,因此 $G^\prime$ 和回路中至少有一个交点 $v_1^\prime$。我们从 $v_1^\prime$ 出发重复刚才的过程,得到一条新的回路 $C^\prime$,然后将 $C^\prime$ 并入 $C$。
重复以上 找环-并入-删边 的过程,每次图的规模都会缩小,最终一定会得到一个空图。此时的回路便是包含了所有边的简单回路。
这便说明了 $G$ 是欧拉图。证毕。
- **性质:一张有向图 $G$ 是欧拉图,当且仅当其基图连通,且图中每个结点的出度等于入度**。
证明与上一性质类似,此处略去。
- **性质:一张无向连通图 $G$ 是半欧拉图,当且仅当图中有且仅有两个点的度数为奇数**。(常用于半欧拉图的判定,下同)
证明:**(必要性)**设 $G$ 有欧拉路 $v_1\ e_1\ v_2\ e_2\cdots\ v_{m-1}\ e_{m-1}\ v_m$。
我们发现,$v_i(1<i<m)$ 的出现一次便意味着存在与之相连的两条边 $e_{i-1}$ 和 $e_i$。
(特别地,$v_1$ 的出现只有一条 $e_1$,$v_m$ 的出现只有一条 $e_m$)
那么,对所有的点 $x$,若它在通路中除了头尾以外的部分出现了 $k$ 次,而且它不是头也不是尾,那么它的度 $d(x)$ 就是 $2k$,一定是一个偶数。
如果 $x$ 是头或尾(依据欧拉路的定义 $x$ 不可能既是头又是尾),那么他的度 $d(x)$ 就是 $2k+1$,一定是一个奇数。这样的点有且仅有两个。
**(充分性)**由于 $G$ 是连通图,所以一定存在一条连接这两个点的通路。
删去这条路,图中每个点的度数都是偶数。由上文第二条性质可知,每个连通块内都存在一条欧拉回路。
依次将其并入原有的通路,便得到了一条 $G$ 中的欧拉路。这便说明了 $G$ 是半欧拉图。 证毕。
- **性质:一张有向图 $G$ 是半欧拉图,当且仅当其基图连通,且图中存在两点 $u,v$ 满足 $d^+(u)=d^-(u)+1,d^-(v)=d^+(v)+1$,其余每个结点的出度等于入度**。
证明与上一性质类似,此处略去。
- **性质:若无向连通图 $G$ 的奇点(度为奇数的点)数为 $k$,至少需要用 $\dfrac{k}{2}$ 笔画成。**
证明:这其实是上面一些性质的扩展,而且我们由握手原理易知 $k$ 一定是偶数。
证明的方法其实类似上面,此处不作展开。
- **性质:可以用加边的方式把一个非欧拉图变成欧拉图。**
具体地,对含有 $k$ 个奇点的无向连通图,至少要加 $\dfrac{k}{2}$ 条边;
对基图连通的有向图,至少要加的边数应为:
$$
\dfrac{\sum\limits_{x=1}^n|d^+(x)-d^-(x)|}{2}
$$
### Fleury 算法
*Fleuly* 算法(弗洛莱算法)又称**避桥法**,是用于求欧拉路或欧拉回路的算法。
算法的主要流程是每次选择下一条边的过程中**优先选择**一条不为桥的边。(因为桥意味着走过去就再也走不回来了)
由于每次删一条边都要再跑一遍 *Tarjan*,因此复杂度是 $\Theta(m(n+m))=\Theta(m^2)$ 的,效率较低。
可以依赖动态图进行一些优化,不过实践意义不大。
### Hierholzer 算法
*Hierholzer* 算法又称**逐步插入回路法**,也是用于求欧拉路或欧拉回路的算法。
算法的流程与性质中的证明类似,具体地:
- 我们先在图中任意找出一条回路。
- 任取一条目前回路中的点,将其替换为一条简单回路。这可以采用递归的形式进行
- 以此类推,可以找到一条欧拉回路。
如果从路开始的话,就可以找到一条欧拉路。
但是,这样暴力实现的复杂度还是用于 $\Theta(m^2)$ 的,怎么进行优化呢?
我们发现,引起复杂度退化的一个主要原因是我们在访问到一个点时,会多次访问那些**已经经过的边**。
那么,我们只要**在搜索时直接把经过的边删去**即可做到 $\Theta(n+m)$ 的复杂度。
(蓝书上的做法,一般不用)由于递归深度是 $\Theta(m)$ 的,因此容易爆栈。可以考虑用一个栈模拟 DFS 的过程。
### 例题
- [P2731 [USACO3.3]骑马修栅栏 Riding the Fences](https://www.luogu.com.cn/problem/P2731)
这是无向图的板子。由于数据范围极小,因此可以采用邻接矩阵配合各种暴力。
还有,这题有重边。
```c++
void dfs(int x)
{
for(int y=1;y<=n;y++)
if(g[x][y]>=1) {
g[x][y]--, g[y][x]--;
dfs(y);
}
ans[++cnt]=x;
}
```
- [P7771 【模板】欧拉路径](https://www.luogu.com.cn/problem/P7771)
这是有向图的板子。由于邻接表的特性,倒着加边可以帮助我们取到字典序最小。
```C++
void dfs(int x)
{
while(true)
{
int i=head[x];
while(i&&vis[i]) i=Next[i];
head[x]=i;
if(i==0) break;
vis[i]=true, dfs(ver[i]);
}
ans[++cnt]=x;
}
```
- [UVA10735 混合图的欧拉回路 Euler Circuit](https://www.luogu.com.cn/problem/UVA10735)
这题包着个欧拉回路的外皮,实际上没法用 *Hierholzer* 算法做,需要跑网络流。
这说明,***Hierholzer* 算法只能在单纯的有向图和无向图中使用,无法在混合图上跑**。
- [P3520 [POI2011]SMI-Garbage](https://www.luogu.com.cn/problem/P3520)
我们发现只有初始权值和最终权值不同的需要一次修改,别的都不需要。
那么我们只需要把这些边全都加入图中,对每个连通块跑一遍 *Hierholzer* 即可。
输出有点麻烦,需要用一个栈去维护。具体看代码。
(LOJ 上的数据比这里强一万倍,那上面就是蓝书中提到的 DFS 爆栈题![](https://啧.tk/fn)![](https://啧.tk/fn))
```c++
void solve(int rt)
{
R[cur=1]=rt;
while(cur) {
int x=R[cur], i=head[x];
while(i&&vis[i]) i=Next[i];
head[x]=i;
if(i==0) {
if(ins[x]) {
res.clear();
res.push_back(x);
while(st[top]!=x) {
res.push_back(st[top]);
ins[st[top]]=false, top--;
}
res.push_back(st[top]), ins[st[top]]=false, top--;
v.push_back(res);
}
ins[x]=true, st[++top]=x;
cur--; continue;
}
vis[i]=vis[i^1]=true;
R[++cur]=ver[i];
}
}
```
[Code](https://loj.ac/s/1209545)
- [#10110. 「一本通 3.7 练习 4」太鼓达人](https://loj.ac/p/10110)
这是一道经典题。
我们把每个 $2^{k-1}$ 位的二进制作为一个结点,表示当前的末尾。然后对所有可能的转移连边。
比如,$k=3$ 时,我们有结点 $\texttt{00},\texttt{01},\texttt{10},\texttt{11}$。
- 由于 $\texttt{00}$ 可以变为 $\texttt{000},\texttt{001}$,因此我们从 $\texttt{00}$ 出发向 $\texttt{00}$、$\texttt{01}$ 各连一条有向边;
- 由于 $\texttt{01}$ 可以变为 $\texttt{010},\texttt{011}$,因此我们从 $\texttt{01}$ 出发向 $\texttt{10}$、$\texttt{11}$ 各连一条有向边;
- 由于 $\texttt{10}$ 可以变为 $\texttt{100},\texttt{101}$,因此我们从 $\texttt{10}$ 出发向 $\texttt{00}$、$\texttt{01}$ 各连一条有向边;
- 由于 $\texttt{11}$ 可以变为 $\texttt{110},\texttt{111}$,因此我们从 $\texttt{11}$ 出发向 $\texttt{10}$、$\texttt{11}$ 各连一条有向边。
然后我们要求每种转移(就是代表着连续 $k$ 位的二进制)出现且仅出现一次,这便是一个欧拉回路的问题。
```c++
void dfs(int x)
{
if(g[0][x]!=-1) {
int tmp=g[0][x];
g[0][x]=-1;
dfs(tmp);
}
if(g[1][x]!=-1) {
int tmp=g[1][x];
g[1][x]=-1;
dfs(tmp);
}
ans[++cnt]=x;
}
int main()
{
cin>>k;
n=(1<<(k-1))-1;
fo(i,0,n) fo(j,0,1) g[j][i]=((i<<1|j)&n);
dfs(0);
printf("%d ",(1<<k));
int cur=k-2;
fo(i,1,cur) putchar('0');
while(cur<(1<<k)) {
printf("%d",ans[cnt]&1);
cnt--, cur++;
}
puts("");
return 0;
}
```