AIGC标识 分层最短路(题解)

图论题解: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 时:

  1. 先比较 best[u] 的字典序;
  2. 如果 best[u] 相同,再比较 w。

于是可以把 best[u] 在这一层内压缩成一个排名 rank[u],候选关键字就是:

(rank[u], w)

每层处理流程:

  1. 对每个点 v,枚举上一层邻居 u,选择最小的 (rank[u],w);
  2. 把当前层所有点的最优关键字排序;
  3. 去重;
  4. 用 lower_bound 给当前层每个点分配新的 rank;
  5. 同时计算模意义下的答案:
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() 之后得到下标,也就是排名。

要求:

  1. 区间必须有序;
  2. 返回值是迭代器,不是下标;
  3. 如果 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 总结能帮你把这道题以及相关实现彻底理清。

posted @ 2026-10-13 12:14  k_stefani  阅读(14)  评论(0)    收藏  举报