次短路

单源次短路径指的是在某个无向图 \(G=(V,E)\) 中,给定某个源点 \(v\),由 \(v\) 到其他顶点的所有路劲中长度为次短的路径,单源次短路径问题是单源 \(k\) 短路径问题的特例。对于一般的单源 \(k\) 短路径问题,可以通过扩展 Dijkstra(要求没有负权边)算法来求解。

例题:P2865 [USACO06NOV] Roadblocks G

允许经过重复点和重复边的最短路。

类似分层图,用 \(dis_{u,0}\) 记从起点到 \(u\) 的最短路,\(dis_{u,1}\) 记从起点到 \(u\) 的次短路。

初始化 \(dis_{1,0} = 0\)\(dis_{1,1} = \infty\),往优先队列里放 (dis, 点),刚开始放 (0, 1)

vis 标记也要给每个点准备两份(有两次取出机会),一个点 \(u\) 第一次取出来的时候,一定是最短路,第二次取出来的时候,就是次短路了,所以每个点只会有两次有效取出,有效取出才会往后扫边更新。

对于边 \(u \rightarrow v\)

  • 如果当前这一次能更新最短路,就把原最短路放到次短路上,更新最短路,将点 \(v\) 和这次更新后的 \(dis\) 放入优先队列。
  • 如果当前这一次只能更新次短路,就把次短路更新,将点 \(v\) 和这次更新后的 \(dis\) 放入优先队列。

\(u\) 在第一次取出时标记 \(vis_{u,0}\),第二次取出时标记 \(vis_{u,1}\),之后如果再取出来的时候 \(vis_{u,0}\)\(vis_{u,1}\) 均已被标记,说明这个堆顶已经无效了,直接 continue

参考代码
#include <cstdio>
#include <vector>
#include <queue>
using pr = std::pair<int, int>;
const int N = 5005;
const int INF = 1e9;
std::vector<pr> g[N];
int dis[N][2];
bool vis[N][2];
int main()
{
    int n, r; scanf("%d%d", &n, &r);
    for (int i = 1; i <= r; i++) {
        int a, b, d; scanf("%d%d%d", &a, &b, &d);
        g[a].push_back({b, d});
        g[b].push_back({a, d});
    }
    std::priority_queue<pr, std::vector<pr>, std::greater<pr>> q;
    for (int i = 1; i <= n; i++) dis[i][0] = dis[i][1] = INF;
    dis[1][0] = 0; q.push({0, 1});
    while (!q.empty()) {
        pr p = q.top(); q.pop();
        int u = p.second, x = -1;
        if (!vis[u][0]) {
            vis[u][0] = true; x = 0;
        } else if (!vis[u][1]) {
            vis[u][1] = true; x = 1;
        } else continue;
        for (pr e : g[u]) {
            int v = e.first, w = e.second;
            int tmp = dis[u][x] + w;
            if (tmp < dis[v][0]) {
                dis[v][1] = dis[v][0];
                dis[v][0] = tmp;
                q.push({dis[v][0], v});
            } else if (tmp > dis[v][0] && tmp < dis[v][1]) {
                dis[v][1] = tmp;
                q.push({dis[v][1], v});
            }
        }
    }
    printf("%d\n", dis[n][1]);
    return 0;
}

例题:P1491 集合位置

路径上不允许经过重复的点(如果最短路有多条,次短路长度就是最短路长度)。

先求出一条最短路的路径,次短路一定不会把求出来的那条最短路全都经过一遍,至少有一条边不属于求出来的那条最短路径上的边。

枚举删除最短路径上的一条边,重新跑最短路。

时间复杂度为 \(O(n^3)\)\(O(nm \log m)\)

参考代码
#include <cstdio>
#include <algorithm>
#include <cmath>
const int N = 205;
const double INF = 1e9;
int n, x[N], y[N], pre[N];
double g[N][N], dis[N];
bool vis[N];
double distance(int i, int j) {
    double dx = x[i] - x[j];
    double dy = y[i] - y[j];
    return sqrt(dx * dx + dy * dy);
}
void dijkstra(bool rec) {
    for (int i = 1; i <= n; i++) {
        dis[i] = INF; vis[i] = false;
    }
    dis[1] = 0;
    while (true) {
        int u = -1;
        for (int i = 1; i <= n; i++)
            if (!vis[i] && (u == -1 || dis[i] < dis[u])) u = i;
        if (u == -1) break;
        vis[u] = true;
        for (int i = 1; i <= n; i++) {
            double w = g[u][i];
            if (dis[u] + w < dis[i]) {
                dis[i] = dis[u] + w;
                if (rec) pre[i] = u;
            }
        }
    }
}
int main()
{
    int m; scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d%d", &x[i], &y[i]);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) g[i][j] = INF;
        g[i][i] = 0;
    }
    for (int i = 1; i <= m; i++) {
        int p, q; scanf("%d%d", &p, &q);
        g[p][q] = g[q][p] = std::min(g[p][q], distance(p, q));
    } 
    dijkstra(true);
    if (dis[n] == INF) {
        printf("-1\n"); return 0;
    }
    int u = n;
    double ans = INF;
    while (u != 1) {
        double tmp = g[pre[u]][u];
        g[pre[u]][u] = g[u][pre[u]] = INF;
        dijkstra(false);
        ans = std::min(ans, dis[n]);
        g[pre[u]][u] = g[u][pre[u]] = tmp;
        u = pre[u];
    }
    if (ans == INF) printf("-1\n");
    else printf("%.2f\n", ans);
    return 0;
}

习题:P2901 [USACO08MAR] Cow Jogging G

解题思路

仿照次短路的计算,每个点从堆中第 \(k\) 次被取出的时候,对应的就是第 \(k\) 短路。

参考代码
#include <cstdio>
#include <vector>
#include <queue>
using namespace std;
using ll = long long;
const int N = 1005;
struct Edge {
    int to, w;
};
struct State {
    int u;
    ll d;
    bool operator>(const State& other) const {
        return d > other.d;
    }
};
vector<Edge> g[N];
int cnt[N];
int main()
{
    int n, m, k;
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 0; i < m; i++) {
        int x, y, d;
        // 题目保证 x > y 才是下坡,直接建边
        scanf("%d%d%d", &x, &y, &d);
        g[x].push_back({y, d});
    }
    // 使用小根堆进行扩展
    priority_queue<State, vector<State>, greater<State>> pq;
    pq.push({n, 0});
    int found = 0;
    while (!pq.empty() && found < k) {
        State cur = pq.top();
        pq.pop();
        cnt[cur.u]++;
        // 如果到达了池塘(终点 1)
        if (cur.u == 1) {
            printf("%lld\n", cur.d);
            found++;
        }
        // 如果某个点被弹出的次数已经超过 k,则不再扩展(优化)
        if (cnt[cur.u] > k) continue;
        for (Edge& e : g[cur.u]) {
            pq.push({e.to, cur.d + e.w});
        }
    }
    // 如果路径不足 k 条,按题目要求补 -1
    while (found < k) {
        printf("-1\n");
        found++;
    }
    return 0;
}

习题:P10947 Sightseeing

解题思路

维护最短路、严格次短路的长度和数量,最后看次短路的长度是不是刚好是最短路长度 +1,从而决定是否要将次短路数量加到最终答案中。

参考代码
#include <cstdio>
#include <vector>
#include <utility>
#include <queue>
using namespace std;
using pi = pair<int, int>;
const int N = 1005;
const int INF = 1e9;

vector<pi> g[N]; // 邻接表存储图
int dis[N][2];   // dis[i][0]存最短路, dis[i][1]存次短路
int cnt[N][2];   // cnt[i][0]存最短路数量, cnt[i][1]存次短路数量
bool vis[N][2];  // vis[i][0/1]标记i的最短/次短路是否已确定

void solve() {
    int n, m; scanf("%d%d", &n, &m);
    // --- 初始化每个测试用例的数据 ---
    for (int i = 1; i <= n; i++) {
        g[i].clear();
        dis[i][0] = dis[i][1] = INF;
        cnt[i][0] = cnt[i][1] = 0;
        vis[i][0] = vis[i][1] = false;
    }
    for (int i = 1; i <= m; i++) {
        int a, b, l; scanf("%d%d%d", &a, &b, &l);
        g[a].push_back({b, l});
    }
    int s, f; scanf("%d%d", &s, &f);

    // --- 扩展 Dijkstra 算法 ---
    priority_queue<pi, vector<pi>, greater<pi>> q; // 小顶堆优先队列
    q.push({0, s}); // {距离, 节点}
    dis[s][0] = 0;  // 起点的最短路是0
    cnt[s][0] = 1;  // 路径数量是1

    while (!q.empty()) {
        int d = q.top().first, u = q.top().second;
        q.pop();

        // 如果u的最短和次短路都已确定,则跳过
        if (vis[u][0] && vis[u][1]) continue;

        // 判断当前取出的(d, u)是u的最短路还是次短路
        int x = 0; // 0代表最短路, 1代表次短路
        if (vis[u][0]) x = 1; // 如果最短路已确定,则当前处理的是次短路
        vis[u][x] = true; // 标记对应类型的路径已确定

        // --- 松弛操作 ---
        for (pi e : g[u]) {
            int v = e.first, w = e.second;
            int new_dist = d + w;

            // Case 1: 发现更短的最短路
            if (new_dist < dis[v][0]) {
                // 原最短路降级为次短路
                dis[v][1] = dis[v][0];
                cnt[v][1] = cnt[v][0];
                // 更新最短路
                dis[v][0] = new_dist;
                cnt[v][0] = cnt[u][x];
                q.push({dis[v][0], v});
            } 
            // Case 2: 发现等长的最短路
            else if (new_dist == dis[v][0]) {
                cnt[v][0] += cnt[u][x];
            } 
            // Case 3: 发现更短的次短路
            else if (new_dist < dis[v][1]) {
                dis[v][1] = new_dist;
                cnt[v][1] = cnt[u][x];
                q.push({dis[v][1], v});
            } 
            // Case 4: 发现等长的次短路
            else if (new_dist == dis[v][1]) {
                cnt[v][1] += cnt[u][x];
            }
        }
    }

    // --- 计算最终答案 ---
    int ans = cnt[f][0]; // 先加上最短路的数量
    // 如果次短路长度恰好是“最短路+1”
    if (dis[f][1] == dis[f][0] + 1) {
        ans += cnt[f][1]; // 再加上次短路的数量
    }
    printf("%d\n", ans);
}

int main()
{
    int t; scanf("%d", &t);
    for (int i = 1; i <= t; i++) {
        solve();
    }
    return 0;
}
posted @ 2026-02-10 09:50  RonChen  阅读(1)  评论(0)    收藏  举报