NeFut Logo NeFut
EN 管理员登录

全面解析多源最短路算法与Bitset状态压缩优化

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#algorithm #Dynamic Programming #Bitset

核心逻辑与数学原理

多源最短路(All-Pairs Shortest Paths, APSP)的目标是求解图中任意两点 $u, v \in V$ 之间的最短路径。针对这一问题,Floyd-Warshall 算法在动态规划的框架下展现了极其精炼的矩阵代数本质。同时,在无权图或传递闭包问题中,结合 Bitset 状态压缩可以将算法常数压制到物理极限。

1. 矩阵增量范式:Floyd-Warshall 算法

Floyd 算法的本质是拓扑中间节点的动态引入

2. 传递闭包与 Bitset 状态压缩

在有向图的连通性判定(传递闭包)问题中,我们只关心 $i$ 是否能到达 $j$,状态矩阵退化为布尔矩阵 $f[i][j] \in \{0, 1\}$。


算法推导与状态设计

1. Floyd-Warshall 状态转移方程

定义 $d[i][j]$ 为存储距离的二维矩阵。在第 $k$ 轮迭代中,对于任意节点对 $(i, j)$,状态转移方程为:

$$d[i][j] = \min(d[i][j], d[i][k] + d[k][j])$$

物理边界:初始化时,若 $i == j$ 则 $d[i][j] = 0$;若存在直接有向边则 $d[i][j] = w(i, j)$;其余情况全部标定为 $\infty$。 循环顺序的硬性数学约束阶段变量 $k$ 必须作为最外层循环。因为 $f[k][i][j]$ 的状态强依赖于 $f[k-1]$ 阶段的完全熟化状态。若将 $k$ 置于内层,则状态转移在中间点未完全就绪时发生,导致拓扑递推链断裂。

2. Bitset 传递闭包转移方程

定义 std::bitset<MAXN> g[MAXN],其中 g[i][j] 表示 $i$ 能否到达 $j$。当外层循环控制变量 $k$ 确立时:

$$\text{if } (g[i][k]) \quad g[i] \mid= g[k]$$

物理含义:若 $i$ 可达 $k$,则所有 $k$ 能到达的点, $i$ 均可达。整行直接进行位或运算。


C++ 标准源码(Bitset 优化传递闭包工业模板)

#include <iostream>
#include <vector>
#include <bitset>
#include <algorithm>

const int MAXN = 2005; // 针对 N <= 2000 强度的图论连通性设计

// 使用 bitset 压缩图的邻接矩阵和可达性矩阵
std::bitset<MAXN> g[MAXN]; 

void transitive_closure(int n) {
    // 致命低级错误防范:k 必须作为最外层循环,这是阶段驱动的核心
    for (int k = 1; k <= n; ++k) {
        for (int i = 1; i <= n; ++i) {
            // 关键常数优化:只有当 i 确实能到达 k 时,整行位运算才有意义
            if (g[i][k]) {
                g[i] |= g[k]; // 单条机器指令并行处理 64 个点的状态转移
            }
        }
    }
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n, m;
    if (!(std::cin >> n >> m)) return 0;

    // 自环初始化
    for (int i = 1; i <= n; ++i) {
        g[i].set(i); // g[i][i] = 1
    }

    for (int i = 0; i < m; ++i) {
        int u, v;
        std::cin >> u >> v;
        g[u].set(v); // 有向边建立,g[u][v] = 1
    }

    transitive_closure(n);

    // 输出最终的可达性矩阵(0/1 连通状态)
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            std::cout << (g[i][j] ? 1 : 0) << (j == n ? "" : " ");
        }
        std::cout << "\n";
    }

    return 0;
}

NOIP 实战避坑指南

  1. Floyd 算法三层循环顺序颠倒: 极其常见且低级的错误是将循环写成 for(i...) for(j...) for(k...)。这种写法在内层更新时,依赖的 d[i][k]d[k][j] 本身还没有经过完全松弛,它们当前可能依然是初始的 $\infty$。这直接导致更新漏算,求出的最短路由于拓扑断层而比真实值偏大。必须铁律执行 k 在最外层
  2. Floyd 初始化未进行重边拦截: 在图的输入阶段,若题目给出的数据包含重边(即相同两点间有多条不同的边),选手若直接写 d[u][v] = w,会导致后续读入的、边权较大的重边覆盖掉之前读入的、更优的短边。标准读入必须写成 d[u][v] = \min(d[u][v], w)

经典 NOIP/洛谷 真题

1. 洛谷 P1119 灾后重建

2. 洛谷 P2419 [USACO08JAN] Cow Contest S

原文链接: local://10.4

[h] 返回首页