图论起源与欧拉回路
- 图论起源
- 一笔画问题通解(回溯)
- 欧拉回路
- 欧拉回路的存在性
- Fleury寻找欧拉回路(贪心)
- Hierholzer寻找欧拉回路(模拟)
图论起源
图论是生活中的一个抽象的概念或者说是工具,围绕多对多的连接关系,计算机科学家们设计了很多算法,而后把很多实际问题抽象出来,用图论的算法解决多对多的信息或者数学问题。
关于图的算法有很多,但最重要的是图的遍历算法,也就是如何从一个点出发,通过连接的线访问图的各个点。
图要是学好了,您可以跨学科研究:社会网络、数据分析、离散数学、网络爬虫、道路规划、机器人导航、系统动力学…而系统动力学是真的好玩。
图,这种结构起源于大数学家欧拉,我今天要拿一些专业名称来唬人,请不要揭穿我。
比如说,奇点
、偶点
、起点
、终点
、顶点
、度数
。
“请大家翻到 95
页,思考 3m
… (其实读一下题就好) ”,小学数学老师如是说。
这是小学数学书上的一道题,好像是二年级… (比划过,有些印象)
这道题是图论的起源,由大数学家 莱昂哈德·欧拉 采用一笔画的解法证明在当前给定的条件下,不能走遍哥尼斯堡七桥。
我们来走一遍,试试。
起点:陆地A的店主桥
,不重复走过的桥并且全部走过,终点:内福夫岛
。
- 从
[陆地A的店主桥]
出发,经过[店主桥]
, 抵达[内福夫岛]
;(但没有走过所有桥,继续) - 从
[内福夫岛]
出发,经过[铁匠桥]
,抵达[陆地A]
; - 从
[陆地A]
出发,经过[木桥]
,抵达[陆地C]
; - 从
[陆地C]
出发,经过[密桥]
,抵达[内福夫岛]
; - 从
[内福夫岛]
出发,经过[绿桥]
,抵达[陆地D]
; - 从
[陆地D]
出发,经过[吉布莱茨桥]
,抵达[内福夫岛]
;
漏了[高桥]
!!!
生活在那里的人们,晚上散步也会去试试说不定就全部走了一遍,可惜就是没走出来。
后来,欧拉再次把上面的图简化为几何图形!!
连接方式不变,简化后的连接方式,就是【图】(Graph)。
A、B、C、D
不在是陆地了,是叫【顶点】,而 7
座桥现在叫【边】。
解决这个问题其实非常简单,对于这个几何图形,我们只需要考虑入口和出口。
您看,顶点A
是不是有3
条线连在A
的圈圈上,所以,顶点A
有3
个出入口,度数为 3
。
由此类推,其他顶点:
B:5
C:3
D:3
度数严谨的定义:顶点所关联的边数。
因为每个顶点都关联多条边(多个度数),但每通过顶点一次,这个顶点就减去 2
个度数。
如果想一次性走成,必须满足所有顶点都是偶数或者只有俩个奇点。
我们顺着图走,边走边减。
每经过一个顶点,就划去 2
条边,边走边减。 因为一次减2
条边,每个顶点的奇偶性都不会变。
从[陆地A]
出发,A
的度数减1
只有出口 -> 经过[店主桥]
到达[陆地B]
,B
的度数减2
因为经过入口和出口;
其余步骤同理。验证结果如下,
- 若
起点和终点相同
,则A、B、C、D
顶点度数都要为偶数
; - 若
起点和终点不同
,则A、B、C、D
起点、终点度数为奇数
,其余顶点度数要为偶数
;
而我们不需要回到顶点,所以是第2
种情况,即起点和终点不同。
起点和终点不同,走遍七桥并不重复,结果是所有顶点度数都为0
。
如果还存在大于0
的顶点,那就还有没走过的边。
那倒推过来,起点、终点顶点度数为奇数
,其余顶点度数要为偶数
,才能全部走完。
A:3
B:5
C:3
D:3
对比一下,发现原图的七桥并不匹配,所以不可能在不走重复桥的前提下,走遍7
桥。
一笔画问题通解(回溯)
这种每经过一个顶点,就划去 2 条边,边走边减
的方法也叫【一笔画】。
- 先计算出图的度数、每个顶点的度数;
- 模拟走的过程,每经过一个顶点,就划去 2 条边,边走边减;
- 当 走过顶点数 等同 总顶点数 时,结束;
- 如果没有路了,但还有顶点没走,要退回继续尝试。
回溯算法:
#include <stdio.h>
const int Q = 4;
/* 第一步,建模 [ 邻接矩阵 ] */
int graph[Q][Q]= {
0,1,1,0,
1,0,1,1,
1,1,0,1,
0,1,1,0,
}; // 顶点C在七桥中是只能走一边,所以C点要全部赋值为0即无解情况
/* 第二步,统计每个顶点度数和奇点个数 */
int a[Q]; // 记录Q个顶点的度数
int total; // 顶点数总计
int edge; // 记录走过的顶点
int draw(int v) {
int k = 0;
if( total == edge ) return 1;
// 递归结束条件:走过顶点数 等同 总顶点数
for(int i = 0; i < Q; i++) { // 1 - Q-1
if( graph[v][i] == 1 ) {
k = 1;
graph[v][i] = 0;
graph[i][v] = 0;
edge += 2;
if( draw(i) ) { // 如果递归能走下去
printf(" -> %c", i+65); // 输出顶点
return 1;
} else { // 否则,悔一步要复原
graph[v][i] = 1;
graph[i][v] = 1;
edge -= 2;
k = 0;
}
}
}
if(k == 0) return 0;
}
int main(int argc, char *argv[]) {
int v = 1; // 若没有奇点则从顶点1开始
int k = 0; // 奇点个数总计
for( int i = 0; i < Q; i ++ ) {
for( int j = 0; j < Q; j ++)
if( graph[i][j] == 1 ) a[i]++; // 统计每个顶点的度
total += a[i];
if( a[i]%2 == 1 ) { // 判断当前顶点度数是否为奇点
k ++;
v = i;
}
}
if( k > 2 )
printf("No solution, k > 2\n"); // 奇点大于2,无解情况
else{
draw(v); // 从俩个奇点任意一点出发
printf(" -> %c ",v+65);
}
return 0;
}
回溯法是一种【选优搜索法】,按照选优条件深度优先搜索
,以达到目标。当搜索到某一步时,发现原先选择并不是最优或达不到目标,就【退回】一步重新选择,走不通就退回再走
。
回溯复杂度:
- 时间复杂度:
- 空间复杂度:
欧拉回路
欧拉回路就是欧拉证明【哥尼斯堡七桥】问题无解后而得名的,欧拉回路即从一个点出发,沿着边行走,经过每个边恰好一次
,最后再回到出发点。
欧拉回路,其实就是对【哥尼斯堡七桥】问题的描述。
目前求解欧拉回路,一共有三种算法:
- 回溯:
- Fleury:
- Hierholzer:
回溯法,我们已经实现了,就是上面的那个。
解决欧拉路径问题:
- 先判断回路是否是欧拉回路
- 再用最优的算法寻找欧拉回路
欧拉回路的存在性
欧拉回路即从一个点出发,沿着边行走,经过每个边恰好一次
,最后再回到出发点。
欧拉证明欧拉回路的方法很清晰,主要看度数。
每经过一个顶点,就划去 2 条边,边走边减
欧拉回路想让每条边都走一遍,回到原点,那每个点都必须有进有出。
那每个点的相连边数(度数),必须是偶数。
如果图存在欧拉回路,每个点的度数是偶数。
那如果一个图,它的每个点的度数是偶数,图一定存在欧拉回路吗?
如果这个图是联通的,而且是无向图,就成立 — 其余的就不成立。
如果我们要判断一个图是否存在欧拉回路,就可以采用这个简明的证明:
- 判断图是否是联通的,
- 如果联通分量
,条件就不成立,不存在欧拉回路
- 遍历图的所有顶点,如果某个点是奇数,也不存在欧拉回路
bool has_euler_loop( adjMatrix * G ){
if( DFS( G ) > 1 ) // DFS 返回 联通分量的个数
return false;
for (int i=0; i<G->n; i++ )
if( degree(G->n) % 2 == 1 ) // degree 返回 顶点的度数
return false;
return true;
}
欧拉路径的存在条件:
- 无向图:仅有两个点度数为奇数,且是连通图(用并查集判断);
- 有向图:有两个点可以入度出度不相等(差不大于一),即起点终点;起点入度小于出度,终点入度大于出度,且是连通图(用并查集判断)。
欧拉回路的存在条件:
- 无向图:所有点度数都为偶数,且是连通图(用并查集判断);
- 有向图:所有点的出度入度都相等;从任意一点都可实现,且是连通图(用并查集判断)。
Fleury寻找欧拉回路(贪心)
思路:从某个点开始,遍历边,但有一个挑选条件,要先选【多边】的点走,不走单边的顶点。
贪心就体现在,要先选【多边】的顶点走,因为对比回溯,贪心就在于看出来走【多边】比走单边要好得多,避免做无用功。
如果一个顶点的邻边只有一条,也被称为【桥】(走这条路会把图分成俩部分)。
因为就像现实生活里的桥一样,如果拆掉河水上的桥,那把这张陆地(图)分割为俩个部分,不联通所以走不通。
Fleury算法步骤:
- 对当前点的邻边,判断一下是否是【桥】
- 如果存在多边,就不走【桥】,选一条多边的走就可
- 否则,只能走【桥】
- 其余的,和回溯法相同
输入:
5 6
1 2
1 3
2 3
3 4
3 5
4 5
输出:
1 3 5 4 3 2 1
完整代码:
#include <iostream>
using namespace std;
#define M 202
typedef long long ll;
struct stack {
int top, node[M];
}s;
int e[M][M],n;
void dfs(int x) {
int i;
s.node[++s.top]=x;
for(i=0;i<n;i++) {
if(e[i][x]>0) {
e[i][x]=e[x][i]=0; //删除这条边
dfs(i);
break;
}
}
}
void fleury(int x) {
int i,flag;
s.top=0; s.node[s.top]=x;
while(s.top>=0) {
flag=0;
for(i=0; i<n; i++) {
if(e[s.node[s.top]][i]>0) {
flag=1;
break;
}
}
if(!flag) printf("%d ",s.node[s.top--]+1);
else dfs(s.node[s.top--]);
}
puts("");
}
int main( )
{
int i,j,u,v,m,degree,num=0,start=0;
scanf("%d %d",&n ,&m);
memset(e, 0, sizeof(e));
for(i=0;i<m;i++) {
scanf("%d %d",&u, &v);
e[u-1][v-1] = e[v-1][u-1] = 1;
}
for(i=0; i<n; i++) {
degree = 0;
for(j=0;j<n;j++)
degree+=e[i][j];
if(degree & 1) {
start=i;
num++;
}
}
if(num==0||num==2) fleury(start);
else printf("No Euler path\n");
return 0;
}
Fleury
复杂度:
- 时间复杂度:
- 空间复杂度:
Fleury
的时间复杂度还可以优化为 ,具体的优化方法比较复杂,需要查找相应的论文。
Hierholzer寻找欧拉回路(模拟)
思路:从一个点出发,随意走。
其实就是把上面的数学证明 【一个无向联通图,它的每个点的度数是偶数,图一定存在欧拉回路】的过程,给模拟下来了。
- 选择任一顶点为起点,遍历所有相邻边;
- 深度搜索,访问相邻顶点。将经过的边都删除;
- 如果当前顶点没有相邻边,则将顶点入栈;
- 栈中的顶点倒序输出,就是从起点出发的欧拉回路。
void remove_edge( adjMatrix *nG, int v, int w )
{
if( nG->kind == DN || nG->kind == UDN ) // 无向图
nG->matrix[v][w] = nG->matrix[w][v] = 0;
else
nG->matrix[v][w] = 0;
}
void __hierholzer(adjMatrix *nG, std::stack<int>&s, int v){
for(int i=0; i<nG->n; i++)
if(nG->matrix[v][i]){
remove_edge( nG, v, i );
__hierholzer(nG, s, i);
// 不用恢复边!相当于删除边
}
s.push(v); // 出栈时记录
}
// 深拷贝:保护数据
void hierholzer( adjMatrix *G ){
deep_copy(G, nG);
std::stack<int> s;
__hierholzer( nG, s, 0 );
while(!s.empty()){
printf("%d ",s.top());
s.pop();
}
printf("\n");
}
输入:
5 6
1 2
1 3
2 3
3 4
3 5
4 5
输出:
1 2 3 4 5 3 1
Hierholzer
复杂度:
- 时间复杂度:
- 空间复杂度:
这个算法会删除边,可以用深拷贝优化,保护原始数据不受影响。