PAYATHON 2026

如何用 NetworkX 和 Matplotlib 绘制 DFA,减少多重边重叠?

数据阿明

可以为平行边设置不同的曲率,并让边标签使用相同的曲线设置;节点位置则通过布局函数或手动坐标单独调整。这样可以减少重叠,但无法保证所有 DFA 的边都不交叉、标签都不被遮挡。

为什么原来的边会重叠?

MultiDiGraph 可以保存同一对节点之间的多条边,但绘图时不会自动将它们分开。默认的 connectionstyle='arc3' 没有设置非零曲率,多条同向边容易沿同一路径绘制。可以用 connectionstyle='arc3,rad=0.2' 设置弯曲程度。NetworkX 边绘制文档

spring_layout 决定节点的位置,不负责为每条转移边安排独立路径。因此,调整节点布局和分开多重边需要分别处理。

如何修改 plot 方法?

下面的代码保留 MultiDiGraph,根据同一方向上平行边的最大数量生成曲线样式,并将样式传给边和边标签的绘制函数。NetworkX 官方多重图示例也采用了这种方式,用 (u, v, key) 标识每条边的标签。官方多重图绘制示例

import matplotlib.pyplot as plt
import networkx as nx


def plot(self):
    G = nx.MultiDiGraph()
    nodes = list(self.collect_nodes())

    for n in nodes:
        G.add_node(n.id)

    for n in nodes:
        for naar, label in n.get_edges():
            G.add_edge(n.id, naar, label=label)

    # 先使用圆形布局;也可以换成 spring_layout。
    pos = nx.circular_layout(G)

    max_parallel = max(
        (G.number_of_edges(u, v) for u, v in G.edges()),
        default=1,
    )
    connectionstyle = [
        f"arc3,rad={0.18 * (i + 1)}"
        for i in range(max_parallel)
    ]

    fig, ax = plt.subplots(figsize=(9, 6))
    node_size = 1200

    nx.draw_networkx_nodes(
        G, pos, ax=ax,
        node_size=node_size,
        node_color="lightblue",
    )
    nx.draw_networkx_labels(G, pos, ax=ax)

    nx.draw_networkx_edges(
        G, pos, ax=ax,
        arrows=True,
        arrowsize=20,
        node_size=node_size,
        connectionstyle=connectionstyle,
    )

    edge_labels = {
        (u, v, key): data["label"]
        for u, v, key, data in G.edges(keys=True, data=True)
    }
    nx.draw_networkx_edge_labels(
        G, pos, ax=ax,
        edge_labels=edge_labels,
        node_size=node_size,
        connectionstyle=connectionstyle,
        label_pos=0.5,
        rotate=False,
        font_size=10,
    )

    ax.set_aspect("equal")
    ax.margins(0.25)
    ax.set_axis_off()
    plt.tight_layout()
    plt.show()

代码中的 0.18 可以调整。值越大,曲线通常弯得越明显;平行边较多时,曲率过大也可能让边绕到其他节点附近。

这里沿用原代码的约定:naar 是目标节点的 ID。如果 get_edges() 实际返回节点对象,需要根据类的定义改用 naar.id,否则可能把对象与 ID 当作不同节点加入图中。

如何调整节点排列?

状态较少时,可以先试用示例中的 circular_layout,看看环形排列是否更清楚。如果希望继续使用力导向布局,可以将对应一行改为:

pos = nx.spring_layout(G, seed=42, k=1.2)

seed 用于固定随机初始化,方便比较调整参数后的效果;k 调整布局中的理想节点间距。这些参数需要结合实际图结构调整,无法保证节点按状态转移顺序排列。

如果 DFA 有明确的阅读顺序,手动指定 pos 往往更容易控制。例如,假设状态 ID 为 q0、q1、q2:

pos = {
    "q0": (0, 0),
    "q1": (2, 1),
    "q2": (2, -1),
}

使用时应替换为实际状态 ID,并为图中的每个节点提供坐标。

版本和绘图细节需要注意什么?

示例要求安装的 NetworkX 支持多重边标签,以及两个绘制函数的 connectionstyle 列表参数。当前官方文档提供这些接口,但原问题没有给出安装版本。如果出现参数不支持或多重边标签相关异常,需要先确认本地版本与对应文档。边标签绘制文档

绘制边和标签时,应保持 pos、node_size、connectionstyle 一致,避免标签偏离对应曲线。多个自环或密集转移仍可能重叠,需要继续调整布局、画布大小或标签位置。

对于 DFA,如果只需要展示转移关系,也可以将同一源状态到同一目标状态的多个输入符号合并为一个标签,例如 0, 1。合并时必须保留所有输入符号;直接改用 DiGraph 后反复添加同一条边,可能会覆盖边属性。

备注:内容仅供参考。