分层最短路(题解)
图论题解:k 进制路径权值 + 分层字典序 + STL 总结
关键词:0 边连通块、多源 BFS、字典序、rank 压缩、
sort、unique、lower_bound
[https://codeforces.com/group/OCCHyVcvHH/contest/720951/problem/E](E. Encoded Shortest Paths)
一、题目大意
给定一张无向图,点数不超过 2· 10^5,边数不超过 5· 10^5。
每条边有权值 w,满足:
0 <= w < k
考虑一条从点 1 出发的游走,经过的边权依次为:
w_1,w_2,...,w_L
这条游走的总权值定义为:
w_1k^{L-1}+w_2k^{L-2}+...+w_{L-1}k+w_L
这实际上就是把边权序列看成一个 k 进制数:
(w_1w_2... w_L)_k
要求对每个点 v,求从点 1 到点 v 的所有游走中的最小总权值,并对 998244353 取模。如果不可达,输出 -1。
二、核心观察
1. 开头连续的 0 不影响结果
如果一条游走的开头有若干个 0 边,例如:
[0,0,w]
那么:
0· k^2+0· k+w=w
所以开头的连续 0 可以全部删除,不会改变总权值。
把所有开头的 0 边删掉之后,剩下的部分称为这条游走的「有效核心」。
如果删除后什么都不剩,说明从 1 到该点只经过 0 边,答案就是 0。
2. 核心越短,数值一定越小
假设有效核心长度为 r>0。因为开头第一个数一定大于 0,所以该数满足:
k^{r-1} <= value < k^r
于是:
- 长度为
r_1的核心一定小于 - 长度为
r_2的核心,只要r_1<r_2。
例如:
max_{r_1}=k^{r_1}-1
而长度为 r_2 的数至少是:
k^{r_2-1}
因为 r_2-1>= r_1,所以一定有:
k^{r_1}-1 < k^{r_2-1}
因此,最优策略是:
先最小化有效核心长度。
3. 核心长度就是 BFS 层数
从点 1 出发,只经过 0 边能到达的所有点,称为第 0 层。
对于其他点,从第 0 层出发做一次普通 BFS,得到的层数就是最短有效核心长度。
之后在每一层内部,再选择字典序最小的边权序列即可。
三、算法步骤
第一步:求 0 边连通块
从点 1 出发,只走权值为 0 的边,做一次 BFS/DFS,得到第 0 层。
这些点的答案都是 0。
第二步:多源 BFS 分层
把第 0 层的所有点一起入队,做一次普通 BFS。
得到每个点的层号 dist[v]:
dist[v]=0:第 0 层;dist[v]=1:第 1 层;- 以此类推。
第三步:层内 DP
设 best[v] 表示从第 0 层到 v 的、在当前层内字典序最小的边权序列。
对于当前层的 v,只可能从上一层 u 转移过来:
best[v]=min_{(u,v,w),\ dist[u]=dist[v]-1}(best[u]+w)
这里 + 表示把边权 w 接在序列末尾。
因为同一层中所有 best[u] 的长度相同,所以比较 best[u]+w 时:
- 先比较
best[u]的字典序; - 如果
best[u]相同,再比较w。
于是可以把 best[u] 在这一层内压缩成一个排名 rank[u],候选关键字就是:
(rank[u], w)
每层处理流程:
- 对每个点
v,枚举上一层邻居u,选择最小的(rank[u],w); - 把当前层所有点的最优关键字排序;
- 去重;
- 用
lower_bound给当前层每个点分配新的rank; - 同时计算模意义下的答案:
ans[v]=(ans[pre[v]]· k+w)mod 998244353
四、正确性说明
引理 1:开头的连续 0 可以删除
因为:
[0,0,...,0,s_1,s_2,...,s_r]
和:
[s_1,s_2,...,s_r]
对应的 k 进制数值完全相同。
引理 2:核心长度最短的路径最优
有效核心长度为 r 时,数值范围为:
[k^{r-1},k^r-1]
长度更短的区间完全位于长度更长的区间左侧,所以先最小化核心长度。
引理 3:层内字典序最小等价于数值最小
固定核心长度后,所有游走都表示相同位数的 k 进制数。
位数相同的情况下,数值比较就是从左到右逐位比较,也就是字典序比较。
因此,先最小化核心长度,再在每一层内选择字典序最小的边权序列,得到的就是全局最优解。
五、复杂度
- 0 边连通块 BFS:
O(n+m) - 多源 BFS:
O(n+m) - 每层关键字排序:总计
O(nlog n) - 总时间复杂度:
O(nlog n+m) - 空间复杂度:
O(n+m)
六、参考代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 998244353;
const int MAXN = 200005;
const int MAXM = 500005;
int n, m, K;
int rk[MAXN];
int bst[MAXN];
int pre[MAXN];
int head[MAXN];
int nex[2 * MAXM];
int to[2 * MAXM];
int val[2 * MAXM];
int cnt;
ll ans[MAXN];
queue<int> q;
int dist[MAXN];
vector<vector<int>> level;
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &n, &m, &K);
for (int i = 1; i <= n; i++) {
head[i] = 0;
dist[i] = 0;
rk[i] = 0;
bst[i] = 0;
pre[i] = 0;
ans[i] = 0;
}
cnt = 0;
for (int i = 1; i <= m; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
nex[++cnt] = head[u];
head[u] = cnt;
to[cnt] = v;
val[cnt] = w;
nex[++cnt] = head[v];
head[v] = cnt;
to[cnt] = u;
val[cnt] = w;
}
while (!q.empty()) q.pop();
level.clear();
level.resize(n + 1);
// 第 1 层表示 0 边连通块
dist[1] = 1;
rk[1] = 0;
q.push(1);
level[1].push_back(1);
// 找 1 所在的整个 0 边连通块
while (!q.empty()) {
int x = q.front();
q.pop();
for (int i = head[x]; i; i = nex[i]) {
int y = to[i];
if (dist[y] == 1 || val[i] != 0) continue;
dist[y] = 1;
rk[y] = 0;
q.push(y);
level[1].push_back(y);
}
}
// 从整个 0 边连通块出发做多源 BFS
for (int i = 1; i <= n; i++) {
if (dist[i] == 1) q.push(i);
}
int Max_d = 1;
while (!q.empty()) {
int x = q.front();
q.pop();
for (int i = head[x]; i; i = nex[i]) {
int y = to[i];
if (dist[y] != 0) continue;
dist[y] = dist[x] + 1;
level[dist[y]].push_back(y);
Max_d = max(Max_d, dist[y]);
q.push(y);
}
}
// 从第 2 层开始做层内 DP
for (int i = 2; i <= Max_d; i++) {
vector<pair<int, int>> key;
for (int j = 0; j < (int)level[i].size(); j++) {
int u = level[i][j];
int bst_rk = n + 1;
int bst_w = INT_MAX;
for (int l = head[u]; l; l = nex[l]) {
int v = to[l];
if (dist[v] != i - 1) continue;
if (rk[v] < bst_rk ||
(rk[v] == bst_rk && val[l] < bst_w)) {
bst_rk = rk[v];
bst_w = val[l];
pre[u] = v;
}
}
key.push_back(make_pair(bst_rk, bst_w));
bst[u] = bst_w;
}
sort(key.begin(), key.end());
key.erase(unique(key.begin(), key.end()), key.end());
for (int j = 0; j < (int)level[i].size(); j++) {
int u = level[i][j];
pair<int, int> tt = make_pair(rk[pre[u]], bst[u]);
rk[u] = lower_bound(key.begin(), key.end(), tt) - key.begin();
ans[u] = (ans[pre[u]] * K + bst[u]) % MOD;
}
}
for (int i = 1; i <= n; i++) {
if (dist[i] == 0) {
printf("-1 ");
} else {
printf("%lld ", ans[i]);
}
}
printf("\n");
}
return 0;
}
七、今天问到的 STL 与函数总结
1. vector 的 resize
resize 改变的是 vector 的元素个数,也就是 size。
vector<int> a;
a.resize(5);
执行后:
a.size() == 5
a[0] == 0
a[1] == 0
a[2] == 0
a[3] == 0
a[4] == 0
resize 变大时,新元素补默认值;resize 变小时,多余元素被删除。
对于二维 vector:
vector<vector<int>> level;
level.resize(n + 1);
含义是:
外层
level有n+1个元素,每个元素都是一个空的vector<int>。
可以理解为:
level[0] 是一个空 vector
level[1] 是一个空 vector
level[2] 是一个空 vector
...
level[n] 是一个空 vector
之后可以把点放进对应层:
level[dist[y]].push_back(y);
注意,错误的写法:
level[i].resize(n);
是把第 i 层里面塞进 n 个默认值 0,而不是开 n 层。
2. resize 和 reserve
vector<int> a;
a.reserve(100);
只是预留容量:
a.size() == 0
a.capacity() >= 100
此时不能直接访问 a[0]。
而:
a.resize(100);
会真正改变元素个数:
a.size() == 100
总结:
| 函数 | 作用 | 能否直接访问下标 |
|---|---|---|
resize(n) |
改变元素个数为 n |
可以访问 0..n-1 |
reserve(n) |
预留至少 n 的容量 |
不能,size 仍为 0 |
clear() |
删除所有元素,size 变 0 |
不能 |
3. pair 与 make_pair
pair 的头文件是:
#include <utility>
常见用法:
pair<int, int> p = make_pair(1, 2);
make_pair 会自动推导类型:
auto p = make_pair(1, 2); // pair<int, int>
auto q = make_pair(1, 2LL); // pair<int, long long>
注意:
make_pair
是函数,不是类型模板。
所以不能写:
vector<make_pair<int,int>>
正确的是:
vector<pair<int,int>>
或者:
vector<pair<int,int> >
然后插入时用:
key.push_back(make_pair(bst_rk, bst_w));
也可以直接写:
key.push_back({bst_rk, bst_w});
4. sort
头文件:
#include <algorithm>
用法:
sort(a.begin(), a.end());
数组:
sort(a, a + n);
对于 pair,默认比较规则是:
先比较 first
如果 first 相同,再比较 second
例如:
(0, 9) < (1, 2)
(1, 2) < (1, 3)
正好符合题目中:
先比较上一层 rank
再比较当前边权 w
5. unique
unique 只能删除相邻重复元素,所以必须先排序。
sort(key.begin(), key.end());
key.erase(unique(key.begin(), key.end()), key.end());
unique 不会真正删除元素,它只是把重复元素移到后面,并返回新的逻辑结尾。
必须配合 erase:
auto newEnd = unique(key.begin(), key.end());
key.erase(newEnd, key.end());
6. lower_bound
头文件:
#include <algorithm>
作用:
在有序区间中找到第一个大于等于给定值的位置。
int pos = lower_bound(a.begin(), a.end(), x) - a.begin();
对于 pair:
int rank =
lower_bound(key.begin(), key.end(), curKey)
- key.begin();
lower_bound 返回的是迭代器,减去 begin() 之后得到下标,也就是排名。
要求:
- 区间必须有序;
- 返回值是迭代器,不是下标;
- 如果 key 不存在,返回插入位置,不一定返回
end()。
复杂度:
O(log n)
7. queue
头文件:
#include <queue>
常用操作:
queue<int> q;
q.push(x);
q.front();
q.pop();
q.empty();
BFS 中常用:
while (!q.empty()) {
int u = q.front();
q.pop();
// ...
}
8. INT_MAX
头文件:
#include <climits>
表示 int 的最大值:
int bst_w = INT_MAX;
用于初始化最小值时很方便。
不要写超出 int 范围的大常量,例如:
int x = 0x7F7F7F7F7F7F7F;
这会产生溢出警告,开了 -Werror 时可能直接编译失败。
八、常见错误
错误 1:二维 vector 只 resize 了 n
level.resize(n);
但层号从 1 开始,最大可能到 n,所以应该:
level.resize(n + 1);
否则会出现 level[n] 越界,导致 RE。
错误 2:用 ans[i] == 0 判断不可达
0 是合法答案。
应该用:
if (dist[i] == 0) printf("-1 ");
错误 3:多个测试用例不清空数组
for (int i = 1; i <= n; i++) {
head[i] = 0;
dist[i] = 0;
rk[i] = 0;
bst[i] = 0;
pre[i] = 0;
ans[i] = 0;
}
否则前一个测试用例的数据会影响当前测试用例。
错误 4:lower_bound 前忘记排序
sort(key.begin(), key.end());
key.erase(unique(key.begin(), key.end()), key.end());
// 现在才能 lower_bound
九、总结
这道题的关键不是直接做最短路,而是先观察总权值的 k 进制表示:
开头的 0 可以删除
↓
先最小化有效核心长度
↓
用 0 边连通块 + 多源 BFS 分层
↓
同一层内按字典序比较
↓
用 rank 压缩关键字,避免构造大整数
代码实现上,只需要维护:
dist[v] // 层号
rk[v] // 字典序排名
pre[v] // 最优前驱
bst[v] // 最优路径最后一条边权
ans[v] // 答案 mod MOD
其中 key = (rank[u], w) 是整个层内 DP 的核心。
最后再复述一遍最重要的几个 STL 点:
vector::resize 改变元素个数
vector::reserve 只预留容量
make_pair 是函数,pair 是类型
sort 前 lower_bound 才有效
unique 必须配合 erase
lower_bound 返回迭代器,减去 begin 得到下标
希望这篇题解和 STL 总结能帮你把这道题以及相关实现彻底理清。

浙公网安备 33010602011771号