<think>
我们参考上述引用内容,特别是关于分层图最短路在洛谷P4568题中的应用。
题目大意:有n个城市,m条航线,每条航线连接两个城市,有花费。现在有k次免费乘坐航线的机会,求从起点s到终点t的最小花费。
分层图最短路的思路:
1. 构建k+1层图(0到k层),每一层都是原图的复制。
2. 对于原图中的一条边(u, v, w),我们在每一层内部都建立双向边(即同层之间正常连边,边权为w)。
3. 在层与层之间,我们建立单向边(从当前层指向下一层),边权为0,表示使用一次免费机会。具体来说,对于每一层i(0<=i<k),从第i层的u节点向第i+1层的v节点连一条权值为0的边,同时从第i层的v节点向第i+1层的u节点连一条权值为0的边(因为是无向图)。
4. 然后我们以第0层的起点为源点,求单源最短路(可以使用Dijkstra算法)。
5. 最后,我们检查每一层的终点(即第0层到第k层的终点)的最短路值,取最小值作为答案。
注意:由于有k+1层,所以节点编号需要重新映射。通常,第i层的节点j的编号可以表示为:j + i * n(其中n是原图节点数,节点编号从0或1开始,题目中节点编号从0开始,共n个节点)。
举例说明:
原图有n个节点,那么第0层节点编号为0~n-1,第1层节点编号为n~2n-1,...,第k层节点编号为k*n ~ (k+1)*n-1。
建图步骤:
1. 首先,读入n, m, k, s, t(注意题目中节点编号从0开始)。
2. 对于每条边(u, v, w):
a. 在每一层内部(0~k层)都建立双向边,即同层内u和v之间边权为w。
但是,我们不需要显式地建k+1层,而是通过节点编号的映射,在每一层内部都建立相同的边。
具体操作:对于每一层i(0<=i<=k),在节点(u+i*n)和(v+i*n)之间建立双向边,边权为w。
但实际上,我们可以在读入每条边后,循环i从0到k,分别添加边:从u+i*n到v+i*n的边权为w的双向边。
b. 然后,在层与层之间,对于i从0到k-1,建立从u+i*n到v+(i+1)*n的单向边(边权0),以及从v+i*n到u+(i+1)*n的单向边(边权0)。
这表示在节点u(第i层)使用一次免费机会,可以到达节点v(第i+1层);同理,在节点v(第i层)使用一次免费机会,可以到达节点u(第i+1层)。
3. 运行最短路算法(如Dijkstra),起点为s(第0层起点,即s+0*n = s)。
4. 遍历每一层的终点t,即dis[t + i * n](i从0到k),取最小值。
但是,我们也可以不显式地建k+1层图,而是采用动态规划的思想,在状态中增加一维表示使用了几次免费机会。不过,分层图建图是最直观的。
另外,注意空间:节点总数为n*(k+1),边数:每一层有m条边(双向边,相当于2m条有向边),层与层之间有2m条有向边(每层之间只有一层有向边,共k层),所以总边数=(k+1)*2m + k*2m = (2k+2+2k)*m/2? 实际为:
同层边:每一层有m条双向边,即每层2m条有向边,共k+1层,所以同层边总数为 (k+1)*2m。
层间边:每一条原边会在层间产生2条有向边(从当前层的u到下一层的v,以及当前层的v到下一层的u),而层间边共有k层(从0到k-1),所以层间边总数为 k*2m。
因此总边数 = (k+1)*2m + k*2m = (4k+2)*m? 注意双向边我们建了两次(同层内双向边建两条有向边,层间边也是两个方向分别建一次,但是层间边是单向的,所以层间边只有两个方向各建一条?其实层间边是单向的,我们建了两条:u->v'和v->u',所以是2条)。实际上,同层边:每读入一条边(u,v,w),我们在每一层都建两条有向边:u+i*n -> v+i*n (w) 和 v+i*n -> u+i*n (w),共k+1层,所以2*(k+1)条有向边。
层间边:同样,每读入一条边(u,v,w),对于每一层i(0<=i<k),我们建两条有向边:u+i*n -> v+(i+1)*n (0) 和 v+i*n -> u+(i+1)*n (0),共k层,所以2*k条有向边。
总边数:每条原边对应 (2*(k+1) + 2*k) = 4k+2 条有向边。
当k较大时,边数会很大,所以要注意数据范围。本题中,k<=10,n<=10000,m<=50000,所以总边数最大为:50000*(4*10+2)=50000*42=2100000,在可接受范围内。
代码步骤(以Dijkstra为例):
1. 初始化dis数组,大小为n*(k+1),初始化为一个很大的数。dis[起点] = 0,起点为s(即s+0*n)。
2. 优先队列(小根堆),将(0, s)入队(注意:这里s表示第0层的起点)。
3. 当队列非空,取出堆顶元素,设当前节点u,当前距离d。
- 如果d不等于dis[u],跳过。
- 否则,遍历u的所有出边,进行松弛操作。
4. 最终,遍历所有层(0到k)的终点t,取最小值。
注意:节点编号的转换:原图中的节点编号x,在第i层的编号为x+i*n。
下面给出一个伪代码(实际代码需要根据具体输入调整):
假设节点编号从0开始。
伪代码:
n, m, k, s, t = 输入
// 总节点数
total_nodes = n * (k+1);
// 建图,用邻接表
vector<vector<pair<int, int>>> graph(total_nodes); // 邻接表,pair<邻居, 边权>
// 读入m条边
for i=0 to m-1:
u, v, w = 输入
for layer in 0 to k:
// 同层内建双向边
from1 = u + layer * n;
to1 = v + layer * n;
graph[from1].push_back({to1, w});
graph[to1].push_back({from1, w});
if layer < k:
// 当前层到下一层,建单向边(两条,因为无向图)
from2 = u + layer * n;
to2 = v + (layer+1)*n;
graph[from2].push_back({to2, 0});
from3 = v + layer * n;
to3 = u + (layer+1)*n;
graph[from3].push_back({to3, 0});
// 注意:上面的层间边建了两次(两个方向),但实际上题目是无向图,所以这样建是正确的。
// 但是,这里有一个问题:同层内建双向边时,每一层都建了双向边,而层间边则只建了从当前层到下一层的单向边(且两个方向都建了)。
// 运行Dijkstra
dis = [inf] * total_nodes
dis[s] = 0; // 第0层的起点s
priority_queue<pair<int, int>> pq; // (距离, 节点) 小根堆,距离取负或者用greater
pq.push({0, s});
while not pq.empty():
(d, u) = pq.top(); pq.pop();
if d != dis[u]: continue;
for each edge in graph[u]:
v = edge.first, w = edge.second;
if dis[u] + w < dis[v]:
dis[v] = dis[u] + w;
pq.push({dis[v], v});
// 遍历所有层的终点
ans = inf;
for i in 0 to k:
ans = min(ans, dis[t + i*n]);
输出 ans;
但是,注意:上面的建图方式可能会导致层内边重复建边?实际上,对于每条边,我们在每一层内都建了双向边,同时对于层间边,我们在相邻两层之间建了单向边(表示免费)。这样建图是符合题意的。
然而,上述建图在层内边部分,每个无向边在每一层内都被拆成了两条有向边,而层间边部分,每个无向边在相邻两层之间也被拆成了两条有向边(但是是单向的,从当前层指向下一层)。
但是,引用[3]提到:层间会存在从上一层的点1到下一层的点0、点2的0权边。这里只建了单向边(从上层到下层),所以我们的建图符合。
但是,我们注意到,层间边是单向的,只能从当前层到下一层,不能从下一层回到上一层,这符合免费机会只能使用,不能退回。
另外,我们也可以不建同层内的双向边,而是只建原图的双向边,然后层间边也建双向边?不对,层间边是单向的,因为我们只能使用免费机会从当前层到下一层。
因此,上述建图方式是正确的。
但是,还有一种常见的建图方式:只建原图(第0层)的边,然后在层间边时,不仅建0权边,同时也保留原边到下一层?不对,实际上,我们有两种选择:使用免费或者不使用免费。
所以,对于同一条边,我们在同一层内建边(表示不使用免费),然后在当前层和下一层之间建0权边(表示使用免费)。这样,每一条边我们实际上建了两次:一次是正常边(同层内),一次是0权边(层间边)。
因此,另一种建图方式是:
for i in 0 to m-1:
u, v, w = 输入
for layer in 0 to k:
// 同层内建双向边(正常通过,不消耗免费次数)
graph[u+layer*n].push_back({v+layer*n, w});
graph[v+layer*n].push_back({u+layer*n, w});
// 如果当前不是最后一层,则建层间边(消耗一次免费机会,0权)
if layer < k:
// 使用免费,则从当前层的u到下一层的v,0权
graph[u+layer*n].push_back({v+(layer+1)*n, 0});
graph[v+layer*n].push_back({u+(layer+1)*n, 0});
注意:这样建图,每一条边在同层内建了双向边,同时又在层间建了双向0权边?但是实际上,我们只需要建单向的层间边(因为免费只能从当前层到下一层,不能反向)。所以这里的层间边也是单向的(从当前层指向下一层),而且建了两个方向:u->v'和v->u'。这样,无论从u还是v出发,都可以使用免费机会到达下一层的对应节点。
因此,两种方式都可以。但是,上面这种方式实际上和前面第一种方式是一样的。
然而,我们观察:这样建图后,同层内的边是双向的(表示这条边可以双向通行,且不消耗免费次数),而层间边是单向的(从当前层到下一层,且0权),并且也是双向的(因为原图是无向图,所以我们在两个方向都建了层间边)。这样,就可以保证图的连通性。
所以,我们按照上述伪代码编写即可。
但是,注意:题目中起点和终点是给定的s和t。我们只需要从s(第0层)出发,到任意一层的t(即t+i*n)即可。
最后,注意:有可能不使用免费机会就是最优解,所以我们要考虑0到k层。
另外,有可能k次免费机会用不完,所以我们要在所有层(0到k)的终点中取最小值。
下面给出一个C++的代码框架(实际提交代码需要根据题目调整):
```cpp
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;
const int MAXN = 10010;
const int MAXK = 11;
const int INF = 0x3f3f3f3f;
int n, m, k, s, t;
int total_nodes;
vector<pair<int, int>> graph[MAXN * MAXK]; // 注意:总节点数为n*(k+1),k最大为10,所以MAXK=11,MAXN=10010,总节点数110110,数组大小要够
int dis[MAXN * MAXK];
void dijkstra(int start) {
memset(dis, INF, sizeof(dis));
dis[start] = 0;
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
pq.push({0, start});
while (!pq.empty()) {
int d = pq.top().first;
int u = pq.top().second;
pq.pop();
if (d != dis[u]) continue;
for (auto &edge : graph[u]) {
int v = edge.first;
int w = edge.second;
if (dis[u] + w < dis[v]) {
dis[v] = dis[u] + w;
pq.push({dis[v], v});
}
}
}
}
int main() {
cin >> n >> m >> k;
cin >> s >> t;
total_nodes = n * (k+1);
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
// 对于每一层
for (int layer = 0; layer <= k; layer++) {
int uu = u + layer * n;
int vv = v + layer * n;
// 同层内建双向边
graph[uu].push_back({vv, w});
graph[vv].push_back({uu, w});
// 如果当前不是最后一层,再建层间边(单向,从当前层指向下一层,0权)
if (layer < k) {
// 从当前层的u到下一层的v
graph[uu].push_back({v + (layer+1)*n, 0});
// 从当前层的v到下一层的u
graph[vv].push_back({u + (layer+1)*n, 0});
}
}
}
dijkstra(s); // 起点是第0层的s
int ans = INF;
for (int i = 0; i <= k; i++) {
ans = min(ans, dis[t + i * n]);
}
cout << ans << endl;
return 0;
}
```
但是,注意:上面的建图方式会导致同层内的边被重复建边?比如,对于同一层,一条边(u,v)会被建两次:一次是uu->vv,一次是vv->uu。然后层间边也是,每个节点都会建两条。这没有问题,因为是无向图。
但是,上面代码中,同层内我们建了双向边,层间边也建了双向边(实际上是两个方向的单向边)。但是,层间边我们只建了当前层到下一层,没有下一层到当前层的边(因为免费机会只能使用不能退回),所以是单向的。
然而,我们发现在同层内已经可以双向通行了,所以层间边在两个方向都建了(即从u到下一层的v,以及从v到下一层的u),这样在下一层内,我们可以通过同层内的双向边再走,所以是正确的。
但是,上面的代码有一个问题:在层间边部分,我们建的是0权边,但是注意,题目中免费乘坐,所以边权为0。
但是,我们这样建图,会导致边数巨大,但题目数据范围(k<=10, m<=50000)可以接受。
另外,注意:节点编号是从0开始的,所以我们在输入时要注意题目给的节点编号是否从0开始。洛谷题目中,节点编号是0~n-1。
但是,上述代码在层间边部分,我们建的是0权边,但是同层内是原权值。这样,当我们在同一层内行走时,花费正常累加;当使用免费机会时,通过0权边进入下一层,不增加花费。
注意:我们建图时,每一层内部都建了完整的原图(包括双向边),而层间边则是在相邻层之间建立的单向0权边(两个方向都建了,因为原图是无向图,所以两个方向都要建)。
但是,这样建图后,从第0层到第k层,我们可以通过多次使用免费机会到达更高的层,且每次使用免费机会都是通过0权边。
因此,上述代码可以解决此问题。
但是,我们还需要注意:有可能存在重边?题目没有说,所以按常规,重边我们不需要特殊处理,因为Dijkstra算法会自然选择最短的边(当有重边时,我们建图会建多条边,然后松弛时会选择最短的路径)。
但是,上面的代码中,对于同一条边,我们在每一层都建了,而层间边也建了,所以重边不影响。
但是,我们还可以优化建图:实际上,同层内的边对于每一层都是一样的,我们可以只建一层,然后层间边只建一次?不对,因为每一层内部都是独立的,所以必须每一层都建。
所以,按照上述代码即可。
但是,我们注意到,在层间边部分,我们建的是0权边,而题目中免费乘坐,所以0权是正确的。
最后,注意:起点是s(第0层),终点是t(任意一层),所以我们在最后比较的是t, t+n, t+2n, ... , t+k*n。
但是,有可能存在不连通的情况?题目保证有解,所以不用考虑。
另外,还有一种情况:可能到达终点的层数小于k,所以我们在所有层中取最小值。
因此,我们输出所有层终点的最小值。
但是,上述代码在洛谷上可能会超时?因为总边数大约为 (4k+2)*m,当k=10时,4*10+2=42,m最大50000,则边数约为2100000,节点数为10000*11=110000,Dijkstra算法的时间复杂度为O((n+m)log n),这里n=110000, m=2100000,所以总操作次数大约是2210000*log(110000) ≈ 2210000 * 17 ≈ 37570000,在C++中可能勉强通过(注意常数优化)。
我们可以尝试优化:使用邻接表建图,并且使用优先队列(小根堆),应该可以通过。
另外,我们还可以使用分层图的一种常见优化:不显式建图,而是在Dijkstra中动态转移状态。即状态为(节点, 使用免费次数),然后对于每个节点,我们可以选择使用免费或者不使用免费。这样,我们不需要实际建立多层图的节点,而是通过状态扩展。
具体做法:
定义dis[i][j]表示到达节点i,使用了j次免费机会的最小花费。
然后,从起点(s,0)开始,对于当前状态(u, c),我们有两种选择:
1. 对于一条从u到v的边,权值为w,不使用免费:那么状态转移到(v, c),花费dis[u][c]+w。
2. 如果c<k,使用免费:那么状态转移到(v, c+1),花费dis[u][c](因为免费,所以不加w)。
注意:无向图,所以每个节点的邻居都要考虑。
这样,我们相当于在状态空间上做Dijkstra。状态数为n*(k+1),边数:每个状态u会尝试其所有邻居,然后有两种选择(使用免费或不使用),所以每个状态最多扩展2*deg(u)条边?实际上,对于每个邻居,我们有两种选择?不对,我们有两种选择:使用免费或不使用。所以对于每条边,我们有两种转移方式。
代码框架:
dis[u][c]: 表示到达节点u,使用了c次免费机会的最小花费。
初始化:dis[s][0]=0,其他为INF。
优先队列:存 (花费, 节点, c)
每次取出花费最小的状态。
遍历当前节点的所有邻居v,边权为w:
1. 不使用免费:则新花费 = dis[u][c] + w,状态(v, c)
2. 如果c<k,使用免费:则新花费 = dis[u][c],状态(v, c+1)
然后松弛。
这种方法和分层图建图本质相同,但避免了显式建多层图,代码更简洁。
下面给出这种方法的伪代码:
```cpp
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;
const int MAXN = 10010;
const int MAXK = 11;
const int INF = 0x3f3f3f3f;
int n, m, k, s, t;
vector<pair<int, int>> graph[MAXN]; // 原图的邻接表
int dis[MAXN][MAXK]; // dis[i][j]: 到节点i,使用了j次免费机会的最小花费
struct Node {
int d, u, c; // 当前花费,节点,使用免费次数
Node(int d, int u, int c) : d(d), u(u), c(c) {}
bool operator>(const Node& other) const {
return d > other.d;
}
};
void dijkstra() {
memset(dis, INF, sizeof(dis));
dis[s][0] = 0;
priority_queue<Node, vector<Node>, greater<Node>> pq;
pq.push(Node(0, s, 0));
while (!pq.empty()) {
Node node = pq.top(); pq.pop();
int d = node.d, u = node.u, c = node.c;
if (d != dis[u][c]) continue;
for (auto &edge : graph[u]) {
int v = edge.first, w = edge.second;
// 不使用免费
if (dis[u][c] + w < dis[v][c]) {
dis[v][c] = dis[u][c] + w;
pq.push(Node(dis[v][c], v, c));
}
// 使用免费(如果还有免费次数)
if (c < k) {
if (dis[u][c] < dis[v][c+1]) {
dis[v][c+1] = dis[u][c];
pq.push(Node(dis[v][c+1], v, c+1));
}
}
}
}
}
int main() {
cin >> n >> m >> k;
cin >> s >> t;
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
graph[u].push_back({v, w});
graph[v].push_back({u, w}); // 无向图
}
dijkstra();
int ans = INF;
for (int i = 0; i <= k; i++) {
ans = min(ans, dis[t][i]);
}
cout << ans << endl;
return 0;
}
```
这种方法更常用,且节省内存(不需要建立多层图,只需要原图),同时边数没有增加,每个状态扩展的次数和原图中节点的度有关。
注意:在松弛时,不使用免费:转移到(v, c),使用免费:转移到(v, c+1)(如果c<k)。
所以,推荐使用这种方法。
但是,注意:无向图,所以原图的邻接表要建双向边。
在洛谷上,由于k<=10,n<=10000,所以状态数最多为10000*11=110000,优先队列的操作次数大约为O(状态数*log(状态数)),可以接受。
因此,我们使用第二种方法(动态规划+最短路)来解决此题。
下面,我们给出完整的洛谷P4568的AC代码(注意输入输出,题目要求):
注意:题目中节点编号0~n-1,边可能有重边和自环?题目没有说,但一般不会有自环,重边的话,我们建图时直接加入,Dijkstra会处理重边(因为每次松弛都是取最小花费)。
但是,注意:重边在邻接表中会存储多次,然后我们每次枚举所有出边,所以重边会被多次考虑,自然取最小值。
代码(C++):
</think>### 洛谷P4568飞行路线分层图最短路解决方案
#### 核心思路:分层图最短路
分层图最短路的核心思想是将原图复制成 $k+1$ 层($k$ 为免费次数),每层表示使用不同次数的免费机会。层内保留原边权值,层间通过 **0权边** 连接,表示使用免费机会。最终答案在终点 $t$ 的所有分层中取最小值。
#### 算法实现(动态规划+Dijkstra)
```cpp
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;
const int MAXN = 10010;
const int MAXK = 11;
const int INF = 0x3f3f3f3f;
int n, m, k, s, t;
vector<pair<int, int>> graph[MAXN]; // 原图邻接表
int dis[MAXN][MAXK]; // dis[i][j]: 到达节点i使用j次免费的最小花费
struct Node {
int d, u, c; // 当前花费、节点、使用免费次数
Node(int d, int u, int c) : d(d), u(u), c(c) {}
bool operator>(const Node& other) const {
return d > other.d;
}
};
void dijkstra() {
memset(dis, 0x3f, sizeof(dis)); // 初始化为极大值
dis[s][0] = 0;
priority_queue<Node, vector<Node>, greater<Node>> pq;
pq.push(Node(0, s, 0));
while (!pq.empty()) {
Node node = pq.top(); pq.pop();
int d = node.d, u = node.u, c = node.c;
if (d != dis[u][c]) continue; // 已处理更优解
for (auto &edge : graph[u]) {
int v = edge.first, w = edge.second;
// 不使用免费机会
if (dis[u][c] + w < dis[v][c]) {
dis[v][c] = dis[u][c] + w;
pq.push(Node(dis[v][c], v, c));
}
// 使用免费机会(需有剩余次数)
if (c < k && dis[u][c] < dis[v][c+1]) {
dis[v][c+1] = dis[u][c];
pq.push(Node(dis[v][c+1], v, c+1));
}
}
}
}
int main() {
cin >> n >> m >> k >> s >> t;
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
graph[u].push_back({v, w});
graph[v].push_back({u, w}); // 无向图双向建边
}
dijkstra();
int ans = INF;
for (int i = 0; i <= k; i++) {
ans = min(ans, dis[t][i]); // 取所有分层的最小值
}
cout << ans << endl;
return 0;
}
```
#### 关键点解析
1. **状态定义**
$dis[i][j]$ 表示到达节点 $i$ 时使用 $j$ 次免费机会的最小花费。
2. **状态转移**
- **不使用免费**:$dis[v][c] = \min(dis[v][c], dis[u][c] + w)$
- **使用免费**:$dis[v][c+1] = \min(dis[v][c+1], dis[u][c])$(需 $c < k$)
3. **时间复杂度**
$O((n \cdot k) \log(m \cdot k))$,满足题目数据范围($n \leq 10^4, k \leq 10$)。
#### 分层图示例
对于原始边 $(u,v,w)$:
- **层内边**:保留权值 $w$(图中实线)
- **层间边**:添加 $0$ 权边(图中虚线)
```
第0层(未使用免费):u0 --w-- v0
↓0 ↓0
第1层(使用1次免费):u1 --w-- v1
↓0 ↓0
... ...
```
通过层间 $0$ 权边实现免费转移,最终取终点 $t$ 在所有层的最小值[^1][^2][^3]。
---