



一、题目
有:
-
n个城市
-
m条双向道路
每条道路:
u v w b
含义:
u ----- v
w:运输费用
b:景观评分
运输:
从城市1到城市n。
优惠:
路径中景观评分最高的一条道路免费。
求:
最小运输费用。
二、先不要写代码,理解本质
我们先看一个例子。
道路:
10,b=5
1 ------------ 2
|
|
20,b=8
|
|
3
正常:
10+20=30
但是:
景观最高:
b=8
所以:
20免费。
答案:
10
学生第一反应:
枚举所有路径。
例如:
DFS:
1→2→3
1→4→5→3
...
问题:
路径数量爆炸。
所以:
不能枚举路径。
三、关键核心观察
问同学们:
如果我告诉你:
哪条边免费
问题是不是简单了?
例如:
假设:
2---3
免费。
那么:
答案:
1到2最短距离
+
3到n最短距离
也就是:
dist(1,2)+dist(3,n)
所以:
真正的问题:
找到那条应该免费的边。
四、为什么可以按 b 排序?
这是本题的重点。
我们按照:
景观评分:
小 → 大
例如:
道路:
A b=2
B b=5
C b=9
处理顺序:
A
B
C
当处理 C:
因为:
C的景观最大。
所以:
如果最终路径经过C:
C一定可以免费。
于是:
答案:
1到C左边
+
C右边到n
五、理解程序的大框架
首先要建立图。
但是注意:
这里不是一开始所有道路都加入。
而是:
一条一条加入。
为什么?
因为:
当前正在处理的道路:
就是:
"可能免费的最大景观道路"。
六、建立两个距离数组
代码:
vector<long long> dist1(n+1,1e18);
vector<long long> distn(n+1,1e18);
两个数组。
dist1
含义:
当前已经加入的道路中,从城市1到每个城市的最短距离。
例如:
dist1[5]
表示:
1 → 5
最低费用。
初始化:
dist1[1]=0;
因为:
自己到自己:
0。
distn
含义:
当前道路中,从城市n到每个城市的最短距离。
初始化:
distn[n]=0;
为什么需要两个?
因为免费道路:
u -------- v
可能:
左边来自1:
1 → u
右边到n:
v → n
也可能反过来。
七、最关键的一行代码
ans=min(
ans,
min(
dist1[road.u]+distn[road.v],
dist1[road.v]+distn[road.u]
));
这是整个题目的灵魂。
假设当前道路:
u -------- v
费用w
景观b
现在假设:
它就是最高景观道路。
因为免费:
所以:
不用加w。
情况1:
1 → u
免费
v → n
费用:
dist1[u]+distn[v]
情况2:
1 → v
免费
u → n
费用:
dist1[v]+distn[u]
取最小。
八、加入当前道路
代码:
graph[road.u].push_back(&road);
graph[road.v].push_back(&road);
什么意思?
之前:
图里面:
景观更小的道路
现在:
加入:
当前道路
以后:
它可以参与后面的最短路。
九、relax函数
代码:
const auto relax =
[&](vector<long long> &dist, queue<int> &q)
它做什么?
一句话:
新加入道路后,重新传播可能变短的距离。
看:
if(dist[u]+road->w < dist[v])
经典松弛。
如果:
经过u到v更短:
更新。
例如:
原来:
dist[v]=100
发现:
dist[u]=20
边=30
那么:
20+30=50
更优。
更新:
dist[v]=50
十、完整程序:
cpp
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
const long long INF = 1e18;
// 道路
struct Road
{
int u; // 起点
int v; // 终点
int w; // 费用
int b; // 景观
// 排序规则
bool operator < (const Road &other) const
{
if(b != other.b)
return b < other.b;
return w < other.w;
}
};
// 图中的边
struct Edge
{
int to;
int cost;
};
// 更新最短距离
void relax(
vector<vector<Edge>>& graph,
vector<long long>& dist,
queue<int>& q
)
{
while(!q.empty())
{
int u = q.front();
q.pop();
for(auto edge : graph[u])
{
int v = edge.to;
int w = edge.cost;
// 松弛操作
if(dist[u] + w < dist[v])
{
dist[v] = dist[u] + w;
q.push(v);
}
}
}
}
int main()
{
int n,m;
cin >> n >> m;
vector<Road> roads(m);
// 输入道路
for(int i=0;i<m;i++)
{
cin >> roads[i].u
>> roads[i].v
>> roads[i].w
>> roads[i].b;
}
// 按景观排序
sort(
roads.begin(),
roads.end()
);
// 邻接表
vector<vector<Edge>> graph(n+1);
// 从1号点出发的距离
vector<long long> distFromStart(
n+1,
INF
);
// 从n号点出发的距离
vector<long long> distFromEnd(
n+1,
INF
);
distFromStart[1]=0;
distFromEnd[n]=0;
queue<int> startQueue;
queue<int> endQueue;
long long answer = INF;
// 逐条加入道路
for(int i=0;i<m;i++)
{
Road road = roads[i];
/*
假设当前道路免费
情况1:
1 -> u
免费
v -> n
情况2:
1 -> v
免费
u -> n
*/
answer = min(
answer,
min(
distFromStart[road.u]
+
distFromEnd[road.v],
distFromStart[road.v]
+
distFromEnd[road.u]
)
);
// 加入道路
graph[road.u].push_back(
{
road.v,
road.w
}
);
graph[road.v].push_back(
{
road.u,
road.w
}
);
// 更新起点距离
startQueue.push(road.u);
startQueue.push(road.v);
relax(
graph,
distFromStart,
startQueue
);
// 更新终点距离
endQueue.push(road.u);
endQueue.push(road.v);
relax(
graph,
distFromEnd,
endQueue
);
}
if(answer == INF)
cout << -1 << endl;
else
cout << answer << endl;
return 0;
}
十一、程序详细解析:
整个程序可以分成:
-
道路结构设计
-
排序
-
建图
-
维护两个最短路数组
-
枚举免费道路更新答案
-
动态加入道路并更新距离
下面逐步拆解。
1、整体流程图
先看程序的大框架:
输入道路
|
v
按照景观 b 排序
|
v
建立空图
distFromStart:
1号点到所有点距离
distFromEnd:
n号点到所有点距离
|
v
逐条处理道路
① 假设当前道路免费
更新答案
② 加入道路
③ 更新两个最短距离
|
v
输出答案
2、头文件
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
分别对应:
| 头文件 | 作用 |
|---|---|
| iostream | 输入输出 |
| vector | 动态数组 |
| queue | 队列 |
| algorithm | sort、min |
3、INF定义
const long long INF = 1e18;
表示:
无穷大。
为什么不用:
int
因为:
道路费用可能累加。
例如:
100000条道路:
100000 × 100000
=
1e10
超过 int:
2.1e9
所以:
使用:
long long
3、道路结构体 Road
代码:
struct Road
{
int u;
int v;
int w;
int b;
};
一条道路:
u -------- v
包含:
| 变量 | 含义 |
|---|---|
| u | 城市u |
| v | 城市v |
| w | 费用 |
| b | 景观值 |
例如:
输入:
1 3 20 8
表示:
1 -------- 3
费用20
景观8
4、为什么 Road 要重载 <
代码:
bool operator < (const Road &other) const
{
if(b != other.b)
return b < other.b;
return w < other.w;
}
这是为了:
sort(roads.begin(),roads.end());
排序。
5、规则:
第一关键字:
景观 b
小的在前:
例如:
b=3
b=5
b=8
第二关键字:
如果景观一样:
费用小的先。
例如:
(费用10, 景观5)
(费用20, 景观5)
排序:
10,5
20,5
6、Edge结构体
struct Edge
{
int to;
int cost;
};
这个和 Road 不一样。
Road:
保存原始道路信息。
Edge:
保存图里的邻接信息。
例如道路:
1 ---- 2
费用10
加入图:
graph[1]
{
to=2,
cost=10
}
graph[2]
{
to=1,
cost=10
}
7、relax函数
函数:
cpp
void relax(
vector<vector<Edge>>& graph,
vector<long long>& dist,
queue<int>& q
){
.......
}
作用:
新加入道路后,重新传播最短距离。
参数1:
graph
当前图。
参数2:
dist
距离数组。
可能:
distFromStart
也可能:
distFromEnd
参数3:
queue<int>& q
需要重新检查的城市。
8、relax内部
(1)取队首城市
int u=q.front();
q.pop();
例如:
队列:
2 5
取:
2
(2) 遍历邻边
for(auto edge : graph[u])
例如:
当前城市:
2
邻接:
2--1
2--3
2--5
全部检查。
(3)松弛
核心:
if(dist[u]+w < dist[v])
什么意思?
假设:
现在:
到u距离 = 20
道路:
u---v
费用30
那么:
经过u到v:
20+30=50
如果:
原来:
dist[v]=100
那么:
更新。
9、读取输入
int n,m;
cin>>n>>m;
城市数:
n
道路数:
m
10、保存道路
vector<Road> roads(m);
开一个数组:
保存所有道路。
输入:
for(int i=0;i<m;i++)
{
cin>>
roads[i].u
...
}
例如:
输入:
1 2 10 5
保存:
roads[0]
u=1
v=2
w=10
b=5
11、排序
sort(
roads.begin(),
roads.end()
);
目的:
让道路:
按照景观:
小 → 大
例如:
原来:
8
2
5
变:
2
5
8
12、建立图
vector<vector<Edge>> graph(n+1);
邻接表。
为什么:
n+1?
因为:
城市编号:
1~n
0不用。
13、两个距离数组
distFromStart
vector<long long> distFromStart(n+1,INF);
表示:
1号城市到其它城市
初始化:
distFromStart[1]=0;
因为:
自己到自己:
0。
distFromEnd
vector<long long> distFromEnd(n+1,INF);
表示:
n号城市到其它城市
初始化:
distFromEnd[n]=0;
14、为什么需要两个距离?(重点)
当前免费道路:
u -------- v
如果免费:
路径可能:
方向1
1 → u
免费
v → n
费用:
distFromStart[u]
+
distFromEnd[v]
方向2
1 → v
免费
u → n
费用:
distFromStart[v]
+
distFromEnd[u]
所以必须两个方向。
15、主循环
for(int i=0;i<m;i++)
逐条处理排序后的道路。
16、最核心代码
answer = min(
answer,
min(
distFromStart[road.u]
+
distFromEnd[road.v],
distFromStart[road.v]
+
distFromEnd[road.u]
)
);
这是整题灵魂。
假设:
当前道路:
u -------- v
并且:
它是最高景观道路。
那么:
它免费。
所以:
不用加:
road.w
两个方案:
方案1:
1 → u
免费
v → n
方案2:
1 → v
免费
u → n
取小。
17、加入道路
graph[road.u].push_back(
{
road.v,
road.w
}
);
加入:
u到v。
另一方向:
graph[road.v].push_back(
{
road.u,
road.w
}
);
因为:
道路双向。
18、更新起点距离
startQueue.push(road.u);
startQueue.push(road.v);
新加入道路:
影响:
u、v附近。
加入队列。
然后:
relax(
graph,
distFromStart,
startQueue
);
更新:
1号点距离。
19、更新终点距离
同理:
relax(
graph,
distFromEnd,
endQueue
);
不过:
这里维护的是:
n到其它点。
20、输出
if(answer==INF)
cout<<-1;
说明:
无法到达。
否则:
输出最小费用。
十二、总结算法思想
普通最短路:
固定道路
求最短
本题:
道路有特殊优惠:
最高景观道路免费
怎么办?
转换:
枚举免费道路
↓
左边最短路
+
右边最短路
↓
更新答案
十三、一句话记忆
排序让当前道路成为"可能免费的最高景观道路",维护两个方向的最短距离,然后尝试免费。
十二、最后总结
这道题可以浓缩成一句话:
按景观值排序,每次把当前道路看成"免费道路",利用已经加入的小景观道路维护从起点和终点的最短距离,更新答案。