← 中文资源

简体中文版本

Bellman-Ford 最短路径与负环检测

通过反复松弛所有边计算单源最短路径,重建实际路线,并报告可达的负权环。

阅读英文原文

功能说明

Bellman-Ford 与 Dijkstra 的关键差别在于允许负权边:它对全部边做节点数减一轮松弛,因此能发现总权重为负的环。这一组函数接收节点列表、边列表和起点,返回距离表、前驱表以及可选的负环路径。前驱表可以直接还原从起点到任意节点的具体路线,适合带惩罚或折扣权重的路由、结算差额传播等场景。

当一轮松弛没有任何更新时会提前退出,在普通图上通常远快于最坏情况的 O(VE)。检测到负环后,距离值不再代表真实最短路径,调用方应拒绝该结果。

关键代码

核心循环外层最多执行 len(nodes) - 1 次,内层遍历所有边并比较 distance[source] + weight 与 distance[target]。发现负环时,从被再次松弛的目标出发,沿前驱指针走节点数步,必然停在环内;随后继续前进直到回到起点,反转即可得到环上的节点序列。shortest_path 从终点反向追溯前驱,若遇到 None 说明目标不可达。

使用注意

节点列表必须包含所有边的端点,否则会触发 KeyError。浮点权重会累积误差,金额场景应改用整数或 Decimal。负环检测只能发现从起点可达的环;不可达区域的权重不会参与比较。图中存在零权环不会误报,因为判定条件是严格小于。前驱表在负环场景下已被污染,不能再用来还原路径。节点数量为 1 时不会执行松弛,但仍会扫描负环。

验证方式

示例图从 A 出发,B 的 4.0 加上 B 到 C 的 -2.5 使 D 的最短距离为 4.5,路线为 A -> B -> C -> D。第二组数据包含 B 与 C 之间的双向负权,输出负环 B, C, B。可以再添加一个不可达节点,确认其距离保持 inf 且路径为空。

代码示例

from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True)
class Edge:
    source: str
    target: str
    weight: float


def bellman_ford(
    nodes: list[str],
    edges: list[Edge],
    start: str,
) -> tuple[dict[str, float], dict[str, str | None], list[str] | None]:
    distance = {node: float("inf") for node in nodes}
    previous: dict[str, str | None] = {node: None for node in nodes}
    distance[start] = 0.0

    for _ in range(len(nodes) - 1):
        relaxed = False
        for edge in edges:
            candidate = distance[edge.source] + edge.weight
            if candidate < distance[edge.target]:
                distance[edge.target] = candidate
                previous[edge.target] = edge.source
                relaxed = True
        if not relaxed:
            break

    return distance, previous, find_negative_cycle(nodes, edges, distance, previous)


def find_negative_cycle(
    nodes: list[str],
    edges: list[Edge],
    distance: dict[str, float],
    previous: dict[str, str | None],
) -> list[str] | None:
    for edge in edges:
        if distance[edge.source] + edge.weight < distance[edge.target]:
            previous[edge.target] = edge.source
            node = edge.target
            for _ in range(len(nodes)):
                node = previous[node]  # type: ignore[assignment]
            cycle = [node]
            cursor = previous[node]
            while cursor != node and cursor is not None:
                cycle.append(cursor)
                cursor = previous[cursor]
            cycle.append(node)
            cycle.reverse()
            return cycle
    return None


def shortest_path(previous: dict[str, str | None], start: str, target: str) -> list[str]:
    path = [target]
    cursor = target
    while cursor != start:
        parent = previous[cursor]
        if parent is None:
            return []
        path.append(parent)
        cursor = parent
    path.reverse()
    return path


def main() -> None:
    nodes = ["A", "B", "C", "D"]
    edges = [
        Edge("A", "B", 4.0),
        Edge("A", "C", 5.0),
        Edge("B", "C", -2.5),
        Edge("C", "D", 3.0),
        Edge("B", "D", 6.0),
    ]
    distance, previous, cycle = bellman_ford(nodes, edges, "A")
    print("cycle:", cycle)
    print("D cost:", distance["D"])
    print("path:", " -> ".join(shortest_path(previous, "A", "D")))

    risky = [Edge("A", "B", 1.0), Edge("B", "C", -1.0), Edge("C", "B", -1.0)]
    _, _, cycle = bellman_ford(["A", "B", "C"], risky, "A")
    print("negative cycle:", cycle)


if __name__ == "__main__":
    main()