次短路
单源次短路径指的是在某个无向图 \(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;
}

浙公网安备 33010602011771号