最短路


Dijkstra 最短路算法进阶与扩展笔记

1. 基础扩展:最短路计数(难度:⭐)

来源文件P_1144_最短路计数.cpp

  • 题面抽象:给定一个无向图,求从起点(节点 1)到所有其他节点的最短路径的条数,结果需要取模。
  • 核心思路:在标准 Dijkstra 的最短路更新逻辑中,挂载一个用于记录方案数的数组 cnt
  • 转移逻辑
  • 当发现严格更短的路径 dis[v] > dis[u] + w 时:不仅要更新 dis[v],还要直接继承前驱节点的方案数,即 cnt[v] = cnt[u]
  • 当发现等长的最短路径 dis[v] == dis[u] + w 时:说明找到了一条平行的最短路径,需要将方案数累加,即 cnt[v] = (cnt[v] + cnt[u]) % mod
点击查看代码
struct Node {
    int v, w;
    bool operator<(const Node &t) const {
        return w > t.w;
    }
};
// dij + dp
vector<Node> g[N];
void bfs() {
    vector<int> dis(n + 1, 1e18);
    vector<int> vis(n + 1, 0);
    priority_queue<Node> q;
    dis[1] = 0;
    vector<int> cnt(n + 1, 0);
    cnt[1] = 1;
    q.push({1, 0});
    while (q.size()) {
        auto now = q.top();
        q.pop();
        int u = now.v;
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto [v, w] : g[u]) {
            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
                cnt[v] = cnt[u];
                if (!vis[v]) q.push({v, dis[v]});
            } else if (dis[v] == dis[u] + w) {
                // 更新方案数
                cnt[v] = (cnt[v] + cnt[u]) % mod;
            }
        }
    }
    for (int i = 1; i <= n; i++)
        cout << cnt[i] << endl;
}

void solve() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        if (u == v) continue;
        g[u].push_back({v, 1});
        g[v].push_back({u, 1});
    }
    bfs();
    cout << endl;
}

2. 状态拆分:奇偶最短路(难度:⭐⭐)

来源文件P_5663_CSP_J_2019_加工零件.cpp

  • 题面抽象:给定一个无向图,如果节点 $u$ 要生产第 $L$ 阶段的零件,其相邻节点必须生产 $L-1$ 阶段的零件。起点(节点 1)负责提供 0 阶段的原材料。多次询问节点 $x$ 生产阶段 $L$ 的零件时,起点是否需要提供原材料。
  • 核心思路:因为零件的要求在相邻节点间来回传递,如果起点能在第 $T$ 阶段提供零件,那么它也能在 $T+2, T+4 \dots$ 阶段提供。因此,问题转化为求起点到节点 $x$ 的最短偶数步数最短奇数步数
  • 算法实现
  • 将原来的一维距离数组拆分为二维:d[N][2],分别表示到达某点的最短偶数路径和最短奇数路径。
  • 在 Dijkstra 转移时,当前状态的奇偶性取反即可更新相邻节点对应奇偶性的距离:f = (ww + w) % 2
  • 对于查询 (x, l),只需判断 d[x][l % 2] <= l 且该点可达即可。
点击查看代码
// 将原材料视为第 0 阶段的零件。
// 显然,假设轩轩要加工一个 L 阶段的零件,那么他隔壁的人(假设叫做牢大)就得加工一个 L−1
// 阶段的零件。轩轩作为牢大的隔壁, 轩轩就得加工一个 L−2 阶段的零件。以此类推,轩轩就得加工 0∼L 中所有和 L
// 奇偶性相同的零件。 假设有个人 u 要加工一个 T 阶段的零件,那么如果
// dis(u,1)>T,这个零件传不到轩轩这里,轩轩啥都不用做。否则 ,轩轩得加工第 T−dis(u,1) 阶段的零件,以及 0∼L 中所有和
// T−dis(u,1) 奇偶性相同的零件。 所以只要分奇偶预处理所有点到轩轩的最短距离,然后询问时直接判断即可。

struct Node {
    int x, w;
    bool operator<(const Node &v) const {
        return w > v.w;
    }
};
vector<Node> g[N];
int d[N][2];
int vis[N][2];
void dij() {
    memset(d, 0x3f, sizeof d);
    memset(vis, 0, sizeof vis);
    priority_queue<Node> q;
    q.push({1, 0});
    d[1][0] = 0;
    while (q.size()) {
        auto [u, ww] = q.top();
        q.pop();
        if (vis[u][ww % 2]) continue;
        vis[u][ww % 2] = 1;

        for (auto [v, w] : g[u]) {
            int dis = ww + w;
            int f = dis % 2;
            if (d[v][f] > d[u][ww % 2] + w && !vis[v][f]) {
                d[v][f] = d[u][ww % 2] + w;
                q.push({v, d[v][f]});
            }
        }
    }
}

void solve() {
    cin >> n >> m >> q;
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        if (u == v) continue;
        g[u].push_back({v, 1});
        g[v].push_back({u, 1});
    }
    dij();
    //  cout<<1<<endl;
    for (int i = 1; i <= q; i++) {
        int x, l;
        cin >> x >> l;
        // d[x][l%2] <= l ok

        if (d[x][l % 2] <= l && vis[x][l % 2])
            cout << "Yes";
        else
            cout << "No";
        cout << endl;
    }
}

3. 高级技巧:点集内的两两最短路(难度:⭐⭐⭐⭐)

来源文件P_5304_GXOI_GZOI_2019_旅行者.cpp, P_5304_GXOI_GZOI_2019_旅行者2.cpp, 星海.cpp

  • 题面抽象:给定一个图(可能是有向图)和包含 $k$ 个特殊节点的点集,求这个点集中任意两个不同节点之间的最短路径的最小值。由于不能两两暴力跑最短路,必须在接近 $O(E \log V)$ 的时间内完成。

解法一:二进制分组 + 虚点(超级源汇)

适用代码P_5304_GXOI_GZOI_2019_旅行者.cpp星海.cpp

  • 思路:如果能将点集分为 $A$ 和 $B$ 两个集合,建一个超级源点连向 $A$ 中的点(权值为 0),建一个超级汇点被 $B$ 中的点连接(权值为 0),跑一次 Dijkstra 就能求出 $A$ 到 $B$ 的最短路。
  • 确保覆盖:为了保证任意两个不同的特殊节点至少有一次被分在不同的集合中,可以按照节点 ID 的二进制位进行分组。
  • 实现细节
  • 枚举二进制的每一位(0 到 17),如果特殊点该位为 0 分入集合 $A$,为 1 分入集合 $B$。
  • 对于有向图,需要跑两次 Dijkstra:一次 $A \to B$(源连 0,1 连汇),一次 $B \to A$(源连 1,0 连汇)。每次跑完后需要清理源汇点的连边。
点击查看代码
struct Node {
    int v, w;
    bool operator<(const Node &t) const {
        return w > t.w;
    }
};
// 多源最短路 (点集) n log n log m
// 求点集两点之间的最短路
// 法一
// 思路 将点集分成A B 两个 集合
// 对于A -> B 跑一遍dij
// 对于B -> A 也跑一遍 (因为是有向图)
// 求最小值
vector<Node> g[N];
int a[N], cnt[N];
int ans = 1e18;
void dij() { // A -> B
    priority_queue<Node> q;
    vector<int> vis(n + 5, 0);
    vector<int> dis(n + 5, 1e18);
    dis[0] = 0;
    q.push({0, 0});
    while (q.size()) {
        auto now = q.top();
        q.pop();
        int u = now.v;
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto [v, w] : g[u]) {
            if (dis[v] > dis[u] + w && !vis[v]) {
                dis[v] = dis[u] + w;
                q.push({v, dis[v]});
            }
        }
    }
    ans = min(ans, dis[n + 1]);
}
void dij2() { // B -> A
    priority_queue<Node> q;
    vector<int> vis(n + 5, 0);
    vector<int> dis(n + 5, 1e18);
    dis[n + 1] = 0;
    q.push({n + 1, dis[n + 1]});
    while (q.size()) {
        auto now = q.top();
        q.pop();
        int u = now.v;
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto [v, w] : g[u]) {
            if (dis[v] > dis[u] + w && !vis[v]) {
                dis[v] = dis[u] + w;
                q.push({v, dis[v]});
            }
        }
    }
    ans = min(ans, dis[0]);
}

void solve() {
    cin >> n >> m >> k;
    ans = 1e18;
    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back({v, w});
    }
    for (int i = 1; i <= k; i++)
        cin >> a[i];
    for (int i = 0; i <= 17; ++i) {
        for (int j = 1; j <= k; j++) {
            int tt = a[j] & (1 << i); // 取位
            if (tt == 0)
                g[0].push_back({a[j], 0});
            else {
                // cnt[a[j]]  = g[a[j]].size();
                g[a[j]].push_back({n + 1, 0}); // 虚点n+1
            }
        }

        dij();
        for (int j = 1; j <= k; j++) {
            if (a[j] & (1 << i)) {
                //   g[a[j]][cnt[a[j]]].w = 1e18;
                g[a[j]].pop_back();
            }
        }
        g[0].clear();
        g[n + 1].clear();
        for (int j = 1; j <= k; j++) {
            int tt = a[j] & (1 << i);
            if (tt == 0) {
                //   cnt[a[j]] = g[a[j]].size();
                g[a[j]].push_back({0, 0});
            } else
                g[n + 1].push_back({a[j], 0});
        }
        dij2();
        for (int j = 1; j <= k; j++) {
            if ((a[j] & (1 << i)) == 0) {
                //     g[a[j]][cnt[a[j]]].w = 1e18;
                g[a[j]].pop_back();
            }
        }
        g[n + 1].clear();
    }
    cout << ans << endl;
    for (int i = 0; i <= n + 1; i++) {
        g[i].clear();
    }
}

解法二:正反图 + 多源 Dijkstra 染色

适用代码P_5304_GXOI_GZOI_2019_旅行者2.cpp

  • 思路:将点集作为一个整体丢入初始队列,求出它们到全图其他点的最短距离,并“染色”记录具体来源于哪个特殊点。
  • 实现细节
  • 正向跑:把所有特殊点压入优先队列,求出原图的最短路 d1[i],并用 col1[i] 记录到达点 $i$ 的最短路径是出自哪个特殊点。
  • 反向跑:在反图上再做一次同样的操作,求出 d2[i],用 col2[i] 记录点 $i$ 到达的最短特殊点是哪个。
  • 合并答案:遍历原图的所有边 $u \to v$,如果 col1[u] != col2[v](说明是不同的特殊点),那么这可能是一条合法的连接路径,用 d1[u] + d2[v] + w 更新全局最小答案。
点击查看代码
// 法二
// 双dij 加染色
// 1.将 点集中的点 push到堆中 进行 dij
// 表示 把集合看成一个点 进行dij
// dis1[i]  代表集合到i的最短距离
// 用 col1 来记录 具体是集合中哪个点到i的最短距离
// 2.建反图
// 对于有向图,从所有点到兴趣城市的最短路,等价于在反图上从兴趣城市到所有点的最短路
//  设点到兴趣城市(集合中的点) 的 最短距离为 dis2[N];
// 用 col2 来记录 点 到 哪个 兴趣城市
//  col1 是 兴趣城市 -> 点  col2 是点  -> 兴趣城市
// 遍历边来更新结果
//  u -> v
// col1[u] = a col2[v] = b;
//  dis1[u] 代表 从 兴趣城市a 到 u 的最短距离  dis2[v] 代表 从 点 v 到 兴趣城市 b 的最短距离
//  如果 a 和 b 不等 则符合要求 答案为 dis1[u] + dis2[v] + w(u,v);
//  a 和 b 相等 则为环
struct bian {
    int u, v, w;
};
bian a[N];
int f[N];
struct Node {
    int v, w;
    bool operator<(const Node &t) const {
        return w > t.w;
    }
};
vector<Node> g[N];
int col1[N], col2[N];
vector<Node> g2[N];
int d1[N], d2[N];
void dij() {
    priority_queue<Node> q;
    vector<int> vis(n + 1, 0);
    for (int i = 1; i <= n; i++)
        d1[i] = 1e18;
    for (int i = 1; i <= k; i++) {
        q.push({f[i], 0});
        d1[f[i]] = 0;
        col1[f[i]] = f[i];
    }
    while (q.size()) {
        int u = q.top().v;
        q.pop();
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto &[v, w] : g[u]) {
            if (d1[v] > d1[u] + w) {
                d1[v] = d1[u] + w;
                col1[v] = col1[u];
                q.push({v, d1[v]});
            }
        }
    }
}

void dij2() {
    priority_queue<Node> q;
    vector<int> vis(n + 1, 0);
    for (int i = 1; i <= n; i++)
        d2[i] = 1e18;
    for (int i = 1; i <= k; i++) {
        q.push({f[i], 0});
        d2[f[i]] = 0;
        col2[f[i]] = f[i];
    }
    while (q.size()) {
        auto u = q.top().v;
        q.pop();
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto [v, w] : g2[u]) {
            if (d2[v] > d2[u] + w) {
                d2[v] = d2[u] + w;
                col2[v] = col2[u];
                q.push({v, d2[v]});
            }
        }
    }
}

void solve() {
    cin >> n >> m >> k;
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        a[i].u = u;
        a[i].v = v;
        a[i].w = w;
        g[u].push_back({v, w});
        g2[v].push_back({u, w});
    }
    for (int i = 1; i <= k; i++)
        cin >> f[i];
    dij();
    dij2();
    int ans = 2e18;
    for (int i = 1; i <= m; i++) {
        auto [u, v, w] = a[i];
        if (col1[u] != col2[v]) ans = min(ans, (d1[u] + d2[v] + w));
    }
    cout << ans << endl;
    for (int i = 1; i <= n; i++) {
        g[i].clear();
        g2[i].clear();
    }
}
posted @ 2026-05-16 19:03  r_123  阅读(19)  评论(0)    收藏  举报