1 of 2

代码详解:完善程序-2

该代码实现用“双层 Dijkstra”维护最短路层 0..n-1 与次短路层 n..2n-1:当某点的最短路被更新时,旧的最短路会被推广为该点在次短路层的候选;同时,从最短层出发的边若无法改进最短路,也会尝试用于改进次短路层。最终从结点 n+t 回溯输出次短路路径。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/**************************************************************** 
 * 代码作者: Alex Li
 * 创建时间: 2025-09-03 10:57:31
 * 最后修改: 2025-09-03 20:22:36
 * 文件描述: 2024CSP-S  完善程序-2
****************************************************************/
#include <cstdio>
#include <queue>
#include <utility>
#include <cstring>
using namespace std;

const int maxn = 2e5 + 10;
const int maxm = 1e6 + 10;
const int inf  = 522133279; // 0x1f1f1f1f,用于与 memset(..., 0x1f, ...) 保持一致

int n, m, s, t;

/* 邻接链表存图
head[maxn]:存放“每个点的第一条边的编号
nxt[maxm]: 每条边的下一条的编号
to[maxm]: 每条边的终点编号
w[maxm]: 存放“边的权重
tot = 1: 记录当前总共有多少条边,初始化为1
*/
int head[maxn], nxt[maxm], to[maxm], w[maxm], tot = 1;

/*dis[]:距离数组(长度开到 2n)dis[0..n-1]:从起点 s 到每个点的最短路长度。
dis[n..2n-1](也就是 dis2[v] = dis[n+v]):从起点 s 到每个点的次短路长度)。*/
int dis[maxn << 1], *dis2;  // dis2 = dis + n


/*pre[]:前驱数组(同样长度 2n,用来回溯路径)
pre[0..n-1]:对应最短路层,pre[v] 存的是“在最短路里到达 v 的前一个结点”的索引(在 0..n-1 这一层里)。
pre[n..2n-1](也就是 pre2[v] = pre[n+v]):对应次短路层,pre[n+v] 存“在次短路里到达 v 的前一个结点”的索引。
注意这个索引可能在最短层(< n)或在次短层(≥ n):*/
int pre[maxn << 1], *pre2;   // pre2 = pre + n

/*  Dijkstra 访问标记,在这份代码里有两层状态(共 2n 个):0 .. n-1:最短路层,n .. 2n-1:次短路层
因此 vis 的大小也是 2n,按“状态编号 aa”来标记:
vis[aa] == true 表示这个状态(含层信息)已经被堆顶弹出并最终确定了它的距离 dis[aa],
后续如果队列里还有这个状态的旧条目就直接跳过。*/
bool vis[maxn << 1];       



//建立一条新边 a -> b,权重为 c,编号是 tot。
inline void add(int a, int b, int c) {
    ++tot;             //分配新边编号,从2开始
    nxt[tot] = head[a];//把旧的第一条边挂到 nxt 链表上。
    to[tot]  = b;      // 记录终点
    w[tot]   = c;      // 记录权值
    head[a]  = tot;//更新 a 的第一条边为新建的这条。
}


/**
 * 尝试用 (a -> b, 距离 d) 更新 dis[b]。
 * 若 b < n,则 b 属于“最短层”;若 b >= n,则 b 属于“次短层”。
 * 返回值:是否成功改进 dis[b]。
 *
 * 关键点(① 的位置):
 *   - 当且仅当准备改进“最短层 b”时(b < n),需要把“旧最短 dis[b] + pre[b]”
 *     推广为“次短层的候选”,即调用 upd(pre[b], n+b, dis[b], q)。
 *   - 这样“旧最短”有机会成为“次短”(更优的次短会覆盖它)。
 */
bool upd(int a, int b, int d, priority_queue<pair<int, int>> &q) {
    if (d >= dis[b]) return false;  // 没有更优,直接拒绝

    if (b < n) {
        // ① 推广“旧最短”为“次短候选”:把旧的 (pre[b] -> b, 距离 = dis[b])
        //    尝试写到“次短层的 b”(即 n + b)
        //    这里利用 upd 自身的判重逻辑,若无改进会直接返回 false
        upd(pre[b], n + b, dis[b], q);
    }

    // ② priority_queue 是大根堆,这里用 (-d, b) 实现“最小距离优先”
    q.push(make_pair(-d, b));

    // 真正写入更优解
    dis[b] = d;
    pre[b] = a;
    return true;
}

/**
 * 双层 Dijkstra:
 *   层 0:最短层(0..n-1)
 *   层 1:次短层(n..2n-1)
 *
 * 队列中的节点索引 aa ∈ [0, 2n)
 *   - a = aa % n 为其对应的原始结点编号
 *   - aa < n 表示当前在最短层;aa >= n 表示当前在次短层
 */
void solve() {

/*pari.first 用于排序的键——放的是“距离的相反数 -d,因为 C++ 默认是大根堆,
把距离取负后,距离越小 -d 越大,就会越早被弹出,相当于实现了最小堆效果。
初始化时也有 q.push(make_pair(0, s));(表示起点距离 0 的相反数仍是 0)。
pair.second:状态编号(节点 + 层)。代码有两层状态:0..n-1 是最短层,n..2n-1 是次短层。这里存的就是这个状态索引*/
    priority_queue<pair<int, int>> q;

    // ③ 用 0x1f 初始化,得到 0x1f1f1f1f,数值正好等于 inf(522133279)
    memset(dis, 0x1f, sizeof(dis));
    //把每个字节置为 0xFF,对 int 就是 -1(补码 0xFFFFFFFF),
    memset(pre, -1,  sizeof(pre));
    memset(vis, 0,   sizeof(vis));

    dis2 = dis + n;   // 指向次短层距离数组
    pre2 = pre + n;   // 指向次短层前驱数组

    // 起点:最短层 s 的距离为 0
    dis[s] = 0;
    q.push(make_pair(0, s)); // (负距离, 结点索引)

    // Dijkstra 主循环
    while (!q.empty()) {
        int aa = q.top().second; q.pop();
        if (vis[aa]) continue;
        vis[aa] = true;

        int a = aa % n; // 对应的原图结点编号

        // 遍历 a 的所有出边
        for (int e = head[a]; e; e = nxt[e]) {
            int b = to[e], c = w[e];

            if (aa < n) {
                // 在“最短层”中松弛边 a -> b
                // 先尝试改进最短层:若失败,再把该条路径作为“次短候选”
                if (!upd(a, b, dis[a] + c, q)) {
                    // ④ 若无法改进最短层,用它去尝试改进“次短层的 b”(n + b)
                    upd(a, n + b, dis[a] + c, q);
                }
            } else {
                // 在“次短层”中只能更新“次短层到次短层”
                upd(n + a, n + b, dis2[a] + c, q);
            }
        }
    }
}

/**
 * 递归输出:从结点 a 回溯直至 s
 *
 * 最终调用形如 out(n + t),表示要输出“到达 t 的次短路”。
 * 打印时用 a % n + 1 还原为 1..n 的结点编号,直到 a == n + t 换行。
 */
void out(int a) {
    if (a != s) {
        if (a < n) {
            // 在最短层,回溯到 pre[a]
            out(pre[a]);
        } else {
            // 在次短层,回溯到次短层前驱
            // ⑤ 注意:pre2 指向 pre + n,因此 a 对应的原始编号是 a - n
            out(pre2[a - n]);
        }
    }
    printf("%d%c", a % n + 1, " \n"[a == n + t]);
}

int main() {
    // 输入:点数 n,边数 m,起点 s,终点 t(1-based)
    scanf("%d%d%d", &n, &m, &s, &t);
    s--, t--; 
    //因为输入是 1 基(点编号 1…n),而这份代码里所有数组与逻辑都按0 基(0…n-1)来实现的,
    // 所以把 s、t 各减一,是为了把外部的 1 基编号转换成内部的 0 基编号,保持一致。

    // 读入 m 条有向边(a -> b,权重 c)
    for (int i = 0; i < m; ++i) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a - 1, b - 1, c); //a-1,b-1也是因为起点和终点都是0基
    }

    // 运行双层 Dijkstra
    solve();

    // 若 t 的次短路仍为 inf,表示不存在次短路
    if (dis2[t] == inf) {
        puts("-1");
    } else {
        // 输出次短路长度与路径(从 s 到 t 的第二短)
        printf("%d\n", dis2[t]);
        out(n + t); // 从“次短层的 t”开始回溯输出
    }
    return 0;
}

Scroll to Top