作者:周鼎淯,清华大学,清华大学深圳国际研究生院,清华-伯克利深圳学院,硕士在读
作者:刘兴禄
背景
欧拉回路与欧拉路径
Hierholzer算法
数学规划的方法
分析流程
欧拉回路模型
欧拉路径模型
Python调用Gurobi求解
总结
背景
17世纪的东普鲁士有一座哥尼斯堡城(现为俄国的加里宁格勒城),城中有一座奈佛夫岛。普雷格尔河的两条支流环绕着这座岛屿,并将整个城市分成北区、东区、南区和岛区4个区域。为了连接这些区域,全城共有7座桥将4个城区相连起来。人们常通过这些桥梁到各城区游玩,这些桥梁不仅方便了居民的出行,还引发了一个数学难题:是否存在一条路径,走遍这7座桥,使得每座桥恰好被经过一次并最终回到起点?这个难题就是著名的“哥尼斯堡七桥问题”。
1736年,大数学家列昂纳德·欧拉(L.Euler)发表了关于“哥尼斯堡七桥问题”的论文——《与位置几何有关的一个问题的解》,他在文中指出,从一点出发不重复地走遍七桥,最后又回到原出发点是不可能的。以下是欧拉解决七桥问题的简要思路:
抽象问题
为了解决哥德斯堡七桥问题,欧拉对该问题进行了抽象,用4个字母A、B、C、D代表4个城区,用7条线表示7座桥,如下图所示。这样抽象出了问题中最本质的东西,忽视问题非本质的东西(如桥的长度、宽度等),从而将哥尼斯堡七桥问题抽象为一个数学问题。了解图论的读者一定会觉得非常熟悉,因为这一抽象正是图的表示。事实上,欧拉对哥尼斯堡七桥问题的研究正是图论的开端。
分析“度”的性质
在简化后的图中,欧拉引入了一个重要的概念——“度”(Degree)。一个顶点的度是指与该顶点相连的边的数量。例如,如果一个顶点连接了三条边,那么这个顶点的度就是3。
欧拉注意到,如果一个图形能够被一笔画成(即存在一条路径,使得每条边恰好被经过一次),那么对于路径中的每个顶点,进入该顶点的次数必须等于离开该顶点的次数,除非这个顶点是起点或终点。因此,除了可能的起点和终点外,其他所有顶点的度必须是偶数。
结论
欧拉仔细检查了哥尼斯堡七桥问题中四个顶点的度:D点(北岸)的度为3;B点(南岸)的度为3;A点(一个岛屿)的度为5;C点(另一个岛屿)的度为3。他发现这四个顶点的度都是奇数。根据前面的分析,如果一个图形能够被一笔画成,那么最多只能有两个奇数度的顶点(一个作为起点,另一个作为终点)。然而,哥尼斯堡七桥问题中的图形有四个奇数度的顶点,这显然超过了允许的最大值。因此,欧拉得出结论:不存在一条路径,使得每座桥恰好被经过一次并最终回到起点。
欧拉回路与欧拉路径
为了纪念欧拉对七桥问题的解决以及在图论和拓扑学领域开创性贡献,人们把七桥问题这一类经过经过所有边恰好一次的问题称为”欧拉回路“和”欧拉路径“问题:
欧拉回路:指在图(无向图或有向图)中,经过图中所有边且只经过边一次所形成的回路,称为欧拉回路。具有欧拉回路的图称为欧拉图。如下图结构为欧拉图,从1号顶点出发,经过所有边后可以重回到1号顶点。
欧拉路径: 指通过图中每条边且仅通过一次形成的路径(没有环)。具有欧拉路径但不具有欧拉回路的图称为半欧拉图。如下图,从6号顶点出发,可以经过每一条边后到达 2号顶点,存在欧拉路径,只能说是半欧拉图。
基于哥尼斯堡七桥问题的解决,欧拉在《与位置几何有关的一个问题的解》一书中进一步提出了一个更一般的结论:
图形必须是连通的(即从任何一个顶点出发,可以到达其他所有顶点)。 图形中最多只能有两个奇数度的顶点。如果图形中没有奇数度的顶点,那么存在一个欧拉回路(起点和终点相同);如果有两个奇数度的顶点,那么存在一个欧拉路径(起点和终点不同)。
Hierholzer算法
然而,欧拉提出的算法只能判断欧拉回路或者欧拉路径的存在性,我们还需要一个用来找出欧拉回路或者欧拉路径的算法。1871年,德国数学家Carl Hierholzer提出了Hierholzer算法,用于在已经判定无向图的欧拉回路存在的前提下,找出一条欧拉回路。
Hierholzer算法的基本思路是:先找到一个子回路,以此子回路为基础,逐步将其他回路以插入的方式合并到改子回路中,最终形成完整的欧拉回路。具体步骤结合图示如下:
寻找子回路:如下从顶点1开始,沿着边遍历图,一边遍历一边删除经过的边。如果遇到一个所有边都被删除的顶点,那么该顶点必然是 1(回到初始点)(证明可参考资料[3]中所示)。将该回路上的顶点和边添加到结果序列中。
插入子回路:回溯时检查刚添加到结果序列中的顶点,看是否还有与顶点相连且未遍历的边。可发现顶点 2有未遍历的边,则从2出发开始遍历,找到一个包含2的新回路,将结果序列中的一个 2用这个新回路替换,此时结果序列仍然是一个回路。
直到所有的边被遍历。
在寻找欧拉路径时,首先检查图中所有顶点的度。如果所有顶点的度都是偶数,则可以直接应用Hierholzer算法来寻找欧拉回路。如果存在两个顶点的度是奇数,那么这两个顶点将分别是欧拉路径的起点和终点。选择其中一个奇数度顶点作为起点,开始执行Hierholzer算法的步骤1,寻找子回路。在遍历过程中,所有的边都已经被遍历,并且当前顶点是另一个奇数度顶点,那么找到了一条完整的欧拉路径。如果还有未遍历的边,回到步骤2,继续寻找并合并新的子回路,直到所有边都被遍历。
数学规划的方法
在前文的叙述中,我们采用统计顶点度判断欧拉回路(路径)存在性以及用Hierholzer算法寻找欧拉回路(路径)。然而,从欧拉分析问题过程中,提到了“图连通”和“进入一个顶点的次数必须等于离开该顶点的次数相同”。这也恰好对应数学规划问题中常用的约束类型。我们是否可以借鉴欧拉的思想,使用数学规划模型直接判断欧拉回路(路径)是否存在,而不用结合图中度的性质?以下是分析流程:
分析流程
图连通性:
对于一个有个顶点的无向图,如果其边数小于,那么这个图一定是不连通的。例如,一个有5个顶点的图,如果只有3条边,那么肯定存在部分顶点之间无法连通。所以,连通图至少需要条边来连接所有顶点。根据这条性质,我们可以将图转化为一个流量网络:从任意一个顶点开始,向其他顶点发送单位的流量。其余顶点接收来自相邻顶点的流量后,保留1单位流量后,再向相邻的顶点发送剩余流量。若所有顶点均能接收到流量,则说明图连通,否则,图不连通。
结合上图作为示例进行说明:对于蓝色框线部分图,由顶点1发送3单位流量,流量沿蓝色虚线方向流动,最终顶点2、3、4均保留1单位流量,故该图是连通图;而顶点5、6无法接收来自顶点1发送的流量,故在黑色框线部分视野下,图为非连通图。
欧拉回路性质分析:
如果一个连通图中存在欧拉回路,那么满足:
对于任意一个顶点,进入一个顶点的次数必须等于离开该顶点的次数相同,否则将“困在”该顶点内,无法形成欧拉回路; 每条边仅能经过一次,且必须经过所有边:对于顶点对,如果它们之间边总数为,那么如果从方向选择条边,则从方向须选择条边。
欧拉回路模型
用表示无向多重图(图中不存在自环),其中,是顶点集合,是边集合,表示邻接表,其中表示顶点相邻顶点的集合。在现实中,用多重邻接矩阵表示,表示顶点对之间边的总数,其中。这些构成了多重邻接矩阵。
决策变量:
:整数变量,对于每一对顶点,表示从顶点到顶点方向,选择边的个数。 :整数变量,对于每一对顶点,表示从顶点到顶点方向的流量。
数学规划模型:
下面上述模型进行解释:
目标函数为一个常数,目的是为了解决问题的可行解,而不是去优化某个具体的目标值。 约束(2)-(5)判断图是否连通,这是欧拉回路是否存在的必要条件:
约束(2)表示容量约束:对于每一对顶点,表示这些被选择的边所能承载的最大总流量。由于每条边的最大流量为,所以多条边的总容量为。该约束确保了流量不能超过被选择的边的总容量。 约束(3)、(4)约束表示流量平衡约束:对于起始顶点,需要向其他所有顶点发送单位的流量;对于其他顶点,每个非起始顶点需要接收1单位的净流量。 约束(5)表示变量约束:为大于0的连续变量,表示从顶点到顶点的流量为正。
如果已知图为连通图,约束(2)-(5)可以省去
约束(6)-(8)判断图是否存在欧拉回路:
约束(6)表示流量平衡约束:确保每个顶点的入度等于出度,这是欧拉回路的必要条件。 约束(7)表示边使用次数约束:确保所有的边均被选择,且每条边均仅选择一次。 约束(8)表示变量约束: 为整数变量,其中,并且符合边的使用次数。
欧拉路径模型
作为拓展,本文也对将判断一个无向多重图中是否存在欧拉路径进行了数学规划建模。
决策变量:
:整数变量,对于每一对顶点,表示从顶点到顶点方向,选择边的个数。 :整数变量,对于每一对顶点,表示从顶点到顶点方向的流量。 :0-1变量,顶点是否被选择为起点。
:0-1变量,顶点是否被选择为终点。
数学规划模型:
模型解释:
在欧拉路径的数学规划模型中,与欧拉回路相比,关键在于调整用于判断是否存在欧拉回路的约束条件,以适应欧拉路径的特点。具体来说,在欧拉回路的模型中,我们要求每个顶点的入度和出度相等,如约束(6)所示;然而,当我们将模型应用于寻找欧拉路径时,该约束需要做出相应的调整,以允许路径有一个起点和一个终点:
对于起始顶点,出度比入度多1(或相同,对应特殊情况欧拉回路);对于结束顶点,入度比出度多1(或相同,对应特殊情况欧拉回路);对于其他顶点,入度和出度仍然必须相等。这种修改使得模型既能反应欧拉路径的特点,同时也兼顾欧拉回路这种特殊情况。
我们引入0-1变量和,修改约束(6)为:
其中,和满足:
对于顶点:当时,对应起始顶点;当时,对应结束顶点;当时,对应其他顶点;当,此时起始顶点和结束顶点重合,对应欧拉回路这种特殊情况。由于,这保证了起始顶点和结束顶点各只能存在一个。
Python调用Gurobi求解
以下算法基于Gurobi实现欧拉回路(路径)存在性判断,并且当欧拉回路(路径)存在时,根据决策变量的值,通过DFS算法输出对应的回路(路径)。
import matplotlib.pyplot as plt
import numpy as np
import networkx as nx
from gurobipy import Model, GRB
import gurobipy as gpclass EulerProgramming:
def __init__(self):
self.G = nx.MultiGraph() # 创建一个多重图
self.ind2vertex = {} # 用于将索引映射到顶点
self.vertex2ind = {} # 用于将顶点映射到索引
self.adjacency_matrix = None
# 添加边
def addEdge(self, u, v):
self.G.add_edge(u, v)
self.update_matrices()
# 添加多个边
def addEdges(self, edges):
self.G.add_edges_from(edges)
self.update_matrices()
# 更新邻接矩阵和顶点索引列表/字典
def update_matrices(self):
for ind, vertex in enumerate(self.G.nodes()):
self.ind2vertex[ind] = vertex
self.vertex2ind[vertex] = ind
self.ind = list(self.ind2vertex.keys()) # 获取顶点的索引列表
self.adjacency_matrix = nx.adjacency_matrix(self.G).todense()
def has_EulerCircuit(self):
if nx.is_eulerian(self.G):
print("标准函数判断:存在欧拉回路")
else:
print("标准函数判断:不存在欧拉回路")
def has_EulerPath(self):
# 判断是否有欧拉路径
if nx.has_eulerian_path(self.G):
print("标准函数判断:存在欧拉路径")
else:
print("标准函数判断:不存在欧拉路径")
def EulerCircuit_solver(self):
m = Model("EulerCircuit")
m.setParam('OutputFlag', 0) # 关闭求解器输出
num_nodes = len(self.ind)
s = self.ind[0] # 选择第一个节点作为源节点
# 创建边的列表
arcs = []
for i in self.ind:
for j in self.ind:
if i != j and self.adjacency_matrix[i, j] > 0:
arcs.append((i, j))
# 定义决策变量 x 和 f
x = m.addVars(arcs, vtype=GRB.INTEGER, name="x")
f = m.addVars(arcs, lb=0, vtype=GRB.CONTINUOUS, name="f")
# 以下判断回路是否存在
# 顶点流入和流出约束
for i in self.ind:
m.addConstr(
gp.quicksum(x[i, j] for j in self.ind if (i, j) in arcs) ==
gp.quicksum(x[j, i] for j in self.ind if (j, i) in arcs),
name=f"flow_balance_{i}"
)
# 边容量约束
for i, j in arcs:
m.addConstr(x[i, j] <= self.adjacency_matrix[i, j],
name=f"capacity_{i}_{j}")
m.addConstr(x[i, j] + x[j, i] == self.adjacency_matrix[i, j],
name=f"edge_flow_{i}_{j}")
# 以下判断图是否连通
# 容量约束
for i, j in arcs:
m.addConstr(f[i, j] + f[j, i] <= (num_nodes - 1) * self.adjacency_matrix[i, j],
name=f"flow_constraint_{i}_{j}")
# 流量平衡约束
# 对于源节点 s
m.addConstr(
gp.quicksum(f[s, j] for j in self.ind if (s, j) in arcs) -
gp.quicksum(f[j, s] for j in self.ind if (
j, s) in arcs) == (num_nodes - 1),
name="flow_conservation_s"
)
# 对于其他节点
for i in self.ind:
if i != s:
m.addConstr(
gp.quicksum(f[i, j] for j in self.ind if (i, j) in arcs) -
gp.quicksum(f[j, i]
for j in self.ind if (j, i) in arcs) == -1,
name=f"flow_conservation_{i}"
)
m.setObjective(1, GRB.MAXIMIZE)
m.optimize()
# m.write("model.lp")
# 分析结果
if m.status == GRB.OPTIMAL:
print("数学规划判断:存在欧拉回路")
self.get_eulerian_circuit(x)
print(" -> ".join([str(i) for i in self.eulerian_circuit]))
else:
print("数学规划判断:不存在欧拉回路")
def EulerPath_solver(self):
num_nodes = len(self.ind)
s = self.ind[0] # 选择第一个节点作为源节点
# 创建边的列表
arcs = []
for i in self.ind:
for j in self.ind:
if i != j and self.adjacency_matrix[i, j] > 0:
arcs.append((i, j))
# 首先判断图是否连通
m = Model("ConnectedGraph")
m.setParam('OutputFlag', 0) # 关闭求解器输出
f = m.addVars(arcs, lb=0, vtype=GRB.CONTINUOUS, name="f")
# 容量约束
for i, j in arcs:
m.addConstr(f[i, j] + f[j, i] <= (num_nodes - 1) * self.adjacency_matrix[i, j],
name=f"flow_constraint_{i}_{j}")
# 流量平衡约束
# 对于源节点 s
m.addConstr(
gp.quicksum(f[s, j] for j in self.ind if (s, j) in arcs) -
gp.quicksum(f[j, s] for j in self.ind if (
j, s) in arcs) == (num_nodes - 1),
name="flow_conservation_s"
)
# 对于其他节点
for i in self.ind:
if i != s:
m.addConstr(
gp.quicksum(f[i, j] for j in self.ind if (i, j) in arcs) -
gp.quicksum(f[j, i]
for j in self.ind if (j, i) in arcs) == -1,
name=f"flow_conservation_{i}"
)
m.setObjective(1, GRB.MAXIMIZE)
m.optimize()
# 如果环路不连通,则直接判断不存在欧拉路径
if m.status != GRB.OPTIMAL:
print("数学规划判断:不存在欧拉路径")
return
# 如果环路连通,则继续判断是否存在欧拉路径
m = Model("EulerPath")
m.setParam('OutputFlag', 0) # 关闭求解器输出
x = m.addVars(arcs, vtype=GRB.INTEGER, name="x")
# 创建二进制变量 y 和 z
y = m.addVars(self.ind, vtype=GRB.BINARY, name="y")
z = m.addVars(self.ind, vtype=GRB.BINARY, name="z")
# 添加 y 和 z 变量的约束
m.addConstr(gp.quicksum(y[i] for i in self.ind) == 1, "sum_y_equals_1")
m.addConstr(gp.quicksum(z[i] for i in self.ind) == 1, "sum_z_equals_1")
# 添加流量平衡约束
for i in self.ind:
out_flow = gp.quicksum(x[i, j]
for j in self.ind if (i, j) in arcs)
in_flow = gp.quicksum(x[j, i]
for j in self.ind if (j, i) in arcs)
m.addConstr(out_flow - in_flow ==
y[i] - z[i], name=f"flow_balance_{i}")
# 边容量约束
for i, j in arcs:
m.addConstr(x[i, j] <= self.adjacency_matrix[i, j],
name=f"capacity_{i}_{j}")
m.addConstr(x[i, j] + x[j, i] == self.adjacency_matrix[i, j],
name=f"edge_flow_{i}_{j}")
m.setObjective(1, GRB.MAXIMIZE)
m.optimize()
# 分析结果
if m.status == GRB.OPTIMAL:
print("数学规划判断:存在欧拉路径")
for i in self.ind:
if y[i].X > 0:
self.begin = i
if z[i].X > 0:
self.end = i
self.get_eulerian_path(x)
print(" -> ".join([str(i) for i in self.eulerian_path]))
else:
print("数学规划判断:不存在欧拉路径")
def get_eulerian_circuit(self, x):
self.eulerian_circuit = []
visited_edges = {} # 使用字典记录每条边的访问次数
visited_nodes = {} # 使用字典记录每个节点的访问次数
# 初始化访问计数
for (u, v) in x.keys():
visited_edges[(u, v)] = 0
visited_edges[(v, u)] = 0 # 如果是无向图,也需要记录反向边
for u in self.ind:
visited_nodes[u] = 0
class CircuitFound(Exception):
# 利用异常处理以退出整个dfs搜索
pass
def dfs(current_vertex):
visited_nodes[current_vertex] += 1
# 在邻居结点中查找
for neighbor in [self.vertex2ind[i] for i in list(self.G.neighbors(self.ind2vertex[current_vertex]))]:
if x[current_vertex, neighbor].X > visited_edges[(current_vertex, neighbor)]:
self.eulerian_circuit.append((current_vertex, neighbor))
visited_edges[(current_vertex, neighbor)] += 1
if all(value > 0 for value in visited_nodes.values()) and \
all(visited_edges[(u, v)] == x[u, v].X for (u, v) in x.keys()) and \
all(visited_edges[(v, u)] == x[v, u].X for (u, v) in x.keys()):
# 所有结点和所有边都得到访问时,则退出dfs
raise CircuitFound()
dfs(neighbor)
visited_edges[(current_vertex, neighbor)] -= 1
self.eulerian_circuit.pop()
visited_nodes[current_vertex] -= 1
try:
current_vertex = self.ind[0]
dfs(current_vertex)
except CircuitFound:
for i in range(len(self.eulerian_circuit)):
self.eulerian_circuit[i] = (
self.ind2vertex[self.eulerian_circuit[i][0]], self.ind2vertex[self.eulerian_circuit[i][1]]) # 将索引映射回顶点
pass # 不处理异常,直接退出
def get_eulerian_path(self, x):
self.eulerian_path = []
visited_nodes = {} # 使用字典记录每个节点的访问次数
visited_edges = {} # 使用字典记录每条边的访问次数
# 初始化访问计数
for (u, v) in x.keys():
visited_edges[(u, v)] = 0
visited_edges[(v, u)] = 0 # 记录反向边
for u in self.ind:
visited_nodes[u] = 0
class PathFound(Exception):
# 利用异常处理以退出整个dfs搜索
pass
def dfs(current_vertex):
# 所有边都已访问,且到达self.end
if all(visited_edges[(u, v)] == x[u, v].X for (u, v) in x.keys()) and current_vertex == self.end:
# 所有边都已访问,且到达终点
raise PathFound()
visited_nodes[current_vertex] += 1
# 查找邻居
for neighbor in [self.vertex2ind[i] for i in self.G.neighbors(self.ind2vertex[current_vertex])]:
if x[current_vertex, neighbor].X > visited_edges[(current_vertex, neighbor)]:
self.eulerian_path.append((current_vertex, neighbor))
visited_edges[(current_vertex, neighbor)] += 1
dfs(neighbor)
# 回溯
visited_edges[(current_vertex, neighbor)] -= 1
self.eulerian_path.pop()
visited_nodes[current_vertex] -= 1
try:
dfs(self.begin)
except PathFound:
# 将索引映射回顶点
for i in range(len(self.eulerian_path)):
u, v = self.eulerian_path[i]
self.eulerian_path[i] = (
self.ind2vertex[u], self.ind2vertex[v])
pass # 不处理异常,直接退出
# 大模型编写测试用例
circuit_tests = [
[(1, 2), (1, 2), (2, 3), (2, 3), (1, 4), (2, 4), (3, 4)], # 原始七桥问题
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 1), (1, 3), (3, 5)], # 环形图加两条对角线
[(1, 2), (2, 3), (3, 1), (1, 2), (2, 3), (3, 1)], # 三角形重复边
[(1, 2), (2, 3), (3, 4), (4, 1), (1, 3), (3, 5), (5, 1)], # 五点构成的复杂环
[(1, 2), (2, 3), (3, 4), (4, 1), (1, 2), (2, 3), (3, 4), (4, 1)], # 四点完全图
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1),
(1, 3), (3, 5), (5, 2), (2, 4)], # 六点复杂环
[(1, 2), (2, 3), (3, 4), (4, 1), (1, 2), (2, 3),
(3, 4), (4, 1), (1, 3)], # 四点复杂环加一条额外边
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 1),
(1, 3), (3, 5), (5, 7), (7, 2), (2, 4)], # 七点复杂环
[(1, 2), (2, 3), (3, 1), (1, 4), (4, 5), (5, 6),
(6, 4), (4, 1), (1, 3), (3, 5), (5, 1)], # 分支环,且存在多重边
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8),
(8, 1), (1, 3), (3, 5), (5, 7), (7, 9), (9, 1)] # 九点复杂环
]
print("************************************")
print("欧拉回路测试:")
print("************************************")
print()
for edge in circuit_tests:
ecp = EulerProgramming()
ecp.addEdges(edge)
ecp.has_EulerCircuit()
ecp.EulerCircuit_solver()
print("------------------------------")
# 8个新的测试用例,其中5个图存在欧拉路径,另外3个图不存在欧拉路径。包含多重边情况
path_tests = [
[(1, 2), (2, 3), (3, 4), (4, 1), (1, 3), (1, 3)], # 多重边
[(1, 2), (2, 3), (3, 4), (4, 2), (2, 1)], # 形成回路
[(1, 2), (1, 3), (2, 3)], # 完整图
[(1, 2), (1, 2), (2, 3)], # 只有欧拉路径
[(1, 2), (2, 3), (3, 1), (3, 4)], # 只有欧拉路径
[(1, 2), (1, 2), (2, 3), (2, 3), (1, 4), (2, 4), (3, 4)], # 原始七桥问题
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 1), (1, 3), (4, 5)], # 超过2个奇数度数结点
[(1, 2), (1, 3), (2, 3), (4, 5), (5, 4)], # 图不连通
]
print("************************************")
print("欧拉回路测试:")
print("************************************")
print()
for edge in path_tests:
ecp = EulerProgramming()
ecp.addEdges(edge)
ecp.has_EulerPath()
ecp.EulerPath_solver()
print("------------------------------")
测试环境包含简单图、无向多重图、非连通但各子图存在欧拉回路(路径)等多种情况。以下展示几个典型测试用例:
测试结果:
************************************
欧拉回路测试:
************************************
标准函数判断:不存在欧拉回路
数学规划判断:不存在欧拉回路
------------------------------
标准函数判断:不存在欧拉回路
数学规划判断:不存在欧拉回路
------------------------------
标准函数判断:存在欧拉回路
数学规划判断:存在欧拉回路
(1, 3) -> (3, 2) -> (2, 1) -> (1, 3) -> (3, 2) -> (2, 1)
------------------------------
标准函数判断:存在欧拉回路
数学规划判断:存在欧拉回路
(1, 4) -> (4, 3) -> (3, 2) -> (2, 1) -> (1, 5) -> (5, 3) -> (3, 1)
------------------------------
标准函数判断:存在欧拉回路
数学规划判断:存在欧拉回路
(1, 4) -> (4, 3) -> (3, 2) -> (2, 1) -> (1, 4) -> (4, 3) -> (3, 2) -> (2, 1)
------------------------------
标准函数判断:不存在欧拉回路
数学规划判断:不存在欧拉回路
------------------------------
标准函数判断:不存在欧拉回路
数学规划判断:不存在欧拉回路
------------------------------
标准函数判断:不存在欧拉回路
数学规划判断:不存在欧拉回路
------------------------------
标准函数判断:存在欧拉回路
数学规划判断:存在欧拉回路
(1, 2) -> (2, 3) -> (3, 1) -> (1, 4) -> (4, 5) -> (5, 3) -> (3, 1) -> (1, 4) -> (4, 6) -> (6, 5) -> (5, 1)
------------------------------
标准函数判断:存在欧拉回路
数学规划判断:存在欧拉回路
(1, 8) -> (8, 7) -> (7, 6) -> (6, 5) -> (5, 4) -> (4, 3) -> (3, 2) -> (2, 1) -> (1, 9) -> (9, 7) -> (7, 5) -> (5, 3) -> (3, 1)
------------------------------
************************************
欧拉回路测试:
************************************
标准函数判断:存在欧拉路径
数学规划判断:存在欧拉路径
(3, 2) -> (2, 1) -> (1, 4) -> (4, 3) -> (3, 1) -> (1, 3)
------------------------------
标准函数判断:存在欧拉路径
数学规划判断:存在欧拉路径
(4, 3) -> (3, 2) -> (2, 1) -> (1, 2) -> (2, 4)
------------------------------
标准函数判断:存在欧拉路径
数学规划判断:存在欧拉路径
(3, 2) -> (2, 1) -> (1, 3)
------------------------------
标准函数判断:存在欧拉路径
数学规划判断:存在欧拉路径
(3, 2) -> (2, 1) -> (1, 2)
------------------------------
标准函数判断:存在欧拉路径
数学规划判断:存在欧拉路径
(3, 1) -> (1, 2) -> (2, 3) -> (3, 4)
------------------------------
标准函数判断:不存在欧拉路径
数学规划判断:不存在欧拉路径
------------------------------
标准函数判断:不存在欧拉路径
数学规划判断:不存在欧拉路径
------------------------------
标准函数判断:不存在欧拉路径
数学规划判断:不存在欧拉路径
------------------------------
总结
本文提出了一种解决哥尼斯堡七桥问题的新视角,即数学规划的视角。本文首先回顾了哥尼斯堡七桥问题的背景及其图论解法。然后,我们从数学规划的视角,将其建模为一个数学规划模型,并通过Python调用Gurobi求解了该问题。
本研究的目的是向读者展示数学规划在解决经典问题中的广泛应用潜力,将“图连通性”以及“欧拉回路”这样常用算法思想求解的问题,从数学规划的角度进行求解。我们希望,通过这样的视角,能够激发读者以数学规划的方法去思考和解决更多的问题,拓宽他们在数学建模和优化领域的视野。
参考资料:
[1] https://en.wikipedia.org/wiki/File:Konigsberg_bridges.png
[2] “欧拉回路”与“哈密尔顿回路”:https://blog.51cto.com/u_15952369/6035591
[3] 从散步中诞生的算法问题——欧拉回路与欧拉路径(上) - 知乎:https://zhuanlan.zhihu.com/p/639902025
[4] 从散步中诞生的算法问题——欧拉回路与欧拉路径(下) - 知乎:https://zhuanlan.zhihu.com/p/639915736
[5] C++ 图论算法之欧拉路径、欧拉回路算法的一笔画_一枚大果壳的技术博客_51CTO博客:https://blog.51cto.com/gkcode/8842735?articleABtest=0
微信公众号后台回复
加群:加入全球华人OR|AI|DS社区硕博微信学术群
资料:免费获得大量运筹学相关学习资料
人才库:加入运筹精英人才库,获得独家职位推荐
电子书:免费获取平台小编独家创作的优化理论、运筹实践和数据科学电子书,持续更新中ing...
加入我们:加入「运筹OR帷幄」,参与内容创作平台运营
知识星球:加入「运筹OR帷幄」数据算法社区,免费参与每周「领读计划」、「行业inTalk」、「OR会客厅」等直播活动,与数百位签约大V进行在线交流
文章须知
文章作者:运小筹
责任编辑:Road Rash
微信编辑:疑疑
文章转载自『运小筹』公众号,原文链接:优化问题 | 哥尼斯堡七桥问题:一种数学规划的方法及Python+Gurobi实现
关注我们
FOLLOW US