第一次接触图论时,我被那些抽象的定义和公式弄得晕头转向。直到有一天,我尝试用Python把那些概念画出来——突然一切都变得清晰了。平面图的欧拉公式n-m+r=2,这个看似简单的等式背后藏着怎样的几何奥秘?让我们用代码和可视化来揭开它的面纱。
想象你正在用笔在纸上画图:点代表城市,线代表道路。如果能够画出所有道路而不让任何两条路交叉(除了在交叉路口),这就是一个平面图。欧拉公式则揭示了这种图中点(n)、边(m)和面(r)之间的神奇关系。
为什么n-m+r总是等于2? 让我们从一个最简单的例子开始——三角形:
python复制import networkx as nx
import matplotlib.pyplot as plt
G = nx.Graph()
G.add_edges_from([(1,2), (2,3), (3,1)]) # 三角形
pos = nx.planar_layout(G)
nx.draw(G, pos, with_labels=True)
plt.show()
运行这段代码,你会看到:
面的定义在可视化中变得特别直观:
注意:平面图的绘制方式可能影响面的形状,但不会改变面的数量和公式结果
让我们构建一个可以交互式验证欧拉公式的工具。关键步骤包括:
networkx库创建各种图结构n = len(G.nodes)m = len(G.edges)r的计算需要分析绘制后的面数python复制def verify_euler(G):
n = len(G.nodes)
m = len(G.edges)
pos = nx.planar_layout(G)
nx.draw(G, pos, with_labels=True, node_color='lightblue')
# 计算面数(外部面+内部面)
if m == 0: r = 1 # 只有孤立点
else: r = 2 + m - n # 由欧拉公式推导
plt.title(f"n={n}, m={m}, r={r}\nn - m + r = {n - m + r}")
plt.show()
# 测试四边形
G = nx.Graph([(1,2), (2,3), (3,4), (4,1)])
verify_euler(G)
随着图的复杂度增加,观察公式如何保持成立:
| 图类型 | 顶点数(n) | 边数(m) | 面数(r) | n-m+r |
|---|---|---|---|---|
| 三角形 | 3 | 3 | 2 | 2 |
| 四边形 | 4 | 4 | 2 | 2 |
| 五边形 | 5 | 5 | 2 | 2 |
| 三叉图 | 4 | 3 | 1 | 2 |
树是最简单的连通图——没有环路。让我们看看欧拉公式如何适用:
python复制# 创建一棵树
G = nx.Graph([(1,2), (2,3), (2,4), (1,5)])
verify_euler(G)
结果会显示:
为什么树只有一个面? 因为没有闭合环路,所以不形成任何内部面。
不是所有图都是平面图。著名的反例是完全图K₅和完全二分图K₃,₃。我们可以用推论来判定:
python复制def is_planar(n, m):
if n >= 3:
return m <= 3*n - 6
return True
# 检查K5 (n=5, m=10)
print(is_planar(5, 10)) # 输出False
可视化尝试:
python复制K5 = nx.complete_graph(5)
try:
pos = nx.planar_layout(K5) # 会抛出异常
nx.draw(K5, pos, with_labels=True)
except nx.NetworkXException:
print("K5是非平面图,无法平面化绘制")
每个面的度数等于其边界长度。我们可以用networkx计算:
python复制def face_degrees(G):
cycles = list(nx.cycle_basis(G))
degrees = [len(cycle) for cycle in cycles]
# 外部面的度数等于边界总长
external_degree = 2*len(G.edges) - sum(degrees)
degrees.append(external_degree)
return degrees
G = nx.Graph([(1,2),(2,3),(3,4),(4,1),(1,3)])
print(face_degrees(G)) # 示例输出:[3, 3, 4]
使用ipywidgets创建交互界面:
python复制from ipywidgets import interact
@interact(n=(3,10), m=(2,30))
def explore_graph(n, m):
G = nx.random_graphs.gnm_random_graph(n, m)
try:
verify_euler(G)
except:
print("该图可能不是平面图")
这个工具让你可以:
Q:为什么我的图形绘制看起来有交叉边?
A:可能是非平面图,或者布局算法不够理想。尝试:
python复制pos = nx.planar_layout(G, scale=2) # 调整scale参数
nx.draw(G, pos, with_labels=True)
Q:如何计算复杂图形的面数?
A:实用公式:
优化绘制效果的技巧:
spring_layout获得更均匀的分布python复制edge_labels = {(u,v): f"{u}-{v}" for u,v in G.edges}
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)
在多次实验中,我发现最有效的学习方式是边修改代码边观察图形变化。比如尝试在四边形中添加对角线,观察面数如何从2增加到3,而公式依然成立。这种动态验证比静态记忆要深刻得多。