最短路
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();
}
}

浙公网安备 33010602011771号