题解:洛谷 P15803 物流网络
【题目来源】
洛谷:P15803 [GESP202603 七级] 物流网络 - 洛谷
【题目描述】
一个物流网络由 \(n\) 个城市和 \(m\) 条双向公路组成。每条公路都有两个属性:
- 运输费用 \(w_i\)
- 景观评分 \(b_i\)
当一辆运输车从城市 \(1\) 运送货物到城市 \(n\) 时,需要支付经过道路的运输费用之和。
为了推广旅游线路,物流公司推出了一项优惠政策:在运输路径上,可以免除景观评分最高的那条公路的运输费用。如果有多条公路的景观评分同为最大值,则只免除其中 一条 的费用。
请你计算,从城市 \(1\) 到城市 \(n\) 的最小运输费用。
【输入】
第一行两个整数 \(n,m\),分别表示城市数量和公路数量。
接下来 \(m\) 行,每行四个整数 \(u,v,w,b\),表示存在一条连接城市 \(u\) 和城市 \(v\) 的双向公路,其中 \(w\) 为运输费用,\(b\) 为景观评分。
【输出】
输出一个整数,表示从城市 \(1\) 到城市 \(n\) 的最小费用。
如果无法到达,输出 -1。
【输入样例】
3 3
1 2 10 5
2 3 20 6
1 3 100 1
【输出样例】
0
【核心思想】
-
问题分析:给定一个带权无向图,每条边有运输费用 \(w_i\) 和景观评分 \(b_i\)。从城市 \(1\) 到城市 \(n\) 的路径费用为路径上所有边费用之和减去该路径上景观评分最高的一条边的费用。求最小运输费用。这是一个最短路 + 枚举优化问题,核心在于将"免除最高评分边"的条件转化为枚举哪条边被免除,然后求对应的最短路。
-
算法选择:
- Dijkstra 算法:在正权图中求单源最短路,时间复杂度 \(O((n+m) \log n)\)
- 枚举优化策略:将边按景观评分升序排序,倒序枚举被免除的边,每次移除更高评分的边,避免重复计算
-
关键步骤:
- 读入与排序:读入所有边,按 \((b_i, w_i)\) 升序排序
- 构建邻接表:基于排序后的边构建邻接表,存储 \((目标城市, 边编号)\)
- 倒序枚举免除边(\(i\) 从 \(m\) 到 \(1\)):
- 第 \(i\) 条边是当前剩余边中景观评分最高的(因为已移除 \(i+1\) 到 \(m\) 的更高评分边)
- 执行 \(dijkstra(i)\):计算从 \(1\) 到 \(n\) 的最短路,经过第 \(i\) 条边时费用加 \(0\),其他边加 \(w\)
- 更新全局答案 \(ans \leftarrow \min(ans, dist[n])\)
- 从邻接表中移除第 \(i\) 条边(\(pop\_back\) 两端点)
- 输出结果:若 \(ans\) 仍为无穷大则输出 \(-1\),否则输出 \(ans\)
-
时间/空间复杂度:
- 时间复杂度:\(O(m \cdot (n+m) \log n)\),枚举 \(m\) 条边,每次 Dijkstra 为 \(O((n+m) \log n)\)
- 空间复杂度:\(O(n + m)\),邻接表、距离数组、优先队列
-
枚举与 Dijkstra 的核心思想:
- 问题转化:将"路径上免除最高评分边"转化为"枚举每条边作为被免除的边,求该边费用为 \(0\) 时的最短路"。由于路径上的最高评分边唯一确定,枚举所有边即可覆盖所有可能情况
- 倒序枚举优化:将边按评分升序排序后倒序处理,每次只需移除当前最高评分边,低评分边保留。这样第 \(i\) 轮枚举时,第 \(i\) 条边确实是当前图中评分最高的边,满足题意
- 邻接表动态维护:通过 \(pop\_back\) 从邻接表两端移除边,配合排序保证移除的是当前最高评分边,实现高效的图动态更新
- Dijkstra 的提前终止:一旦从优先队列中确定终点 \(n\) 的最短距离,即可 \(break\) 提前结束,减少不必要的计算
- 适用于带特殊优惠条件的最短路问题,通过枚举将复杂条件转化为标准最短路模型
【解题思路】



【算法标签】
普及+ #Dijkstra
【代码详解】
// 84分版本
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5005, M = 5005 * 2;
int n, m, u, v, w, b, bw, ans = 1e18; // ans初始化为无穷大
int dist[N]; // 存储到每个节点的最小距离
// 边结构体
struct Node
{
int v; // 目标城市
int w; // 公路运输费用
int b; // 公路景观评分
int bw; // 当前路径中景观评分最高的公路的运输费用
};
vector<Node> a[N]; // 邻接表存储图
queue<Node> q; // BFS队列
// 广度优先搜索
void bfs()
{
memset(dist, 0x3f, sizeof(dist)); // 初始化距离为无穷大
q.push({1, 0, 0, 0}); // 从城市1开始,初始运输费用0,最大景观评分0,最大景观评分的公路费用0
while (!q.empty())
{
int u = q.front().v; // 当前城市
int w = q.front().w; // 当前累计运输费用
int b = q.front().b; // 当前路径中的最大景观评分
int bw = q.front().bw; // 当前路径中最大景观评分对应的公路运输费用
q.pop();
// 如果当前路径的运输费用减去最大景观评分的公路费用不小于已记录的最优值,跳过
if (w - bw >= dist[u])
{
continue;
}
dist[u] = w - bw; // 更新到城市u的最优值(实际需要支付的费用)
// 如果到达目标城市n,更新全局答案
if (u == n)
{
ans = min(ans, w - bw);
}
// 遍历当前城市的所有邻接公路
for (int i = 0; i < a[u].size(); i++)
{
// 如果新公路的景观评分更高,或者景观评分相同但运输费用更高
if (a[u][i].b > b || (a[u][i].b == b && a[u][i].w > bw))
{
// 更新最大景观评分的公路,并记录其运输费用
q.push({a[u][i].v, a[u][i].w + w, a[u][i].b, a[u][i].w});
}
else
{
// 否则保持原来的最大景观评分的公路
q.push({a[u][i].v, a[u][i].w + w, b, bw});
}
}
}
}
signed main()
{
cin >> n >> m; // 输入城市数量n和公路数量m
// 输入每条公路的信息
while (m--)
{
cin >> u >> v >> w >> b;
a[u].push_back({v, w, b}); // 添加双向公路
a[v].push_back({u, w, b}); // 添加双向公路
}
bfs(); // 执行BFS搜索
// 输出结果
if (ans == 1e18) // 如果没有找到路径
{
cout << -1 << endl;
}
else
{
cout << ans << endl; // 输出最小运输费用
}
return 0;
}
// AC版本
#include <bits/stdc++.h>
using namespace std;
#define int long long
typedef pair<int, int> PII; // 优先队列元素类型:(距离, 城市)
const int N = 5005;
// 公路结构体
struct Node
{
int u, v, w, b; // 连接的两个城市u,v,运输费用w,景观评分b
} e[5005]; // 存储所有公路
int n, m, u, v, w, idx, d; // 输入变量
int dist[N]; // 最短距离数组
int ans = 1e18; // 最终答案,初始化为无穷大
vector<PII> a[N]; // 邻接表,存储(目标城市, 公路编号)
priority_queue<PII, vector<PII>, greater<PII> > heap; // 小顶堆优先队列
bool st[N]; // Dijkstra中的已确定标记数组
// 排序比较函数:先按景观评分b升序,评分相同则按运输费用w升序
bool cmp(Node x, Node y)
{
if (x.b == y.b) // 景观评分相同
{
return x.w < y.w; // 运输费用小的在前
}
return x.b < y.b; // 景观评分小的在前
}
// Dijkstra算法,x表示当前尝试免去费用的公路编号
void dijkstra(int x)
{
memset(st, 0, sizeof(st)); // 重置已确定标记
memset(dist, 0x3f, sizeof(dist)); // 初始化距离为无穷大
dist[1] = 0; // 起点到自身的距离为0
heap.push({0, 1}); // 起点入队,距离为0
while (!heap.empty())
{
auto t = heap.top();
heap.pop();
int u = t.second; // 当前城市
int distance = t.first; // 当前距离
if (st[u] == true) // 如果已确定最短路径,跳过
{
continue;
}
st[u] = true; // 标记为已确定
if (u == n) // 如果到达终点城市n
{
ans = min(ans, dist[n]); // 更新全局最优解
break; // 找到一条到n的路径就可以提前结束
}
// 遍历当前城市的所有邻接边
for (int i = 0; i < a[u].size(); i++)
{
int v = a[u][i].first; // 邻接城市
int idx = a[u][i].second; // 公路编号
// 新距离 = 当前距离 + (如果是免去的公路则加0,否则加公路费用)
int nd = distance + (idx == x ? 0 : e[idx].w);
if (nd < dist[v]) // 如果找到更短路径
{
dist[v] = nd; // 更新最短距离
heap.push({nd, v}); // 入队
}
}
}
}
signed main()
{
cin >> n >> m; // 输入城市数量和公路数量
// 输入所有公路信息
for (int i = 1; i <= m; i++)
{
cin >> e[i].u >> e[i].v >> e[i].w >> e[i].b;
}
// 将公路按景观评分升序排序,评分相同则按费用升序
sort(e + 1, e + m + 1, cmp);
// 构建邻接表
for (int i = 1; i <= m; i++)
{
a[e[i].u].push_back({e[i].v, i}); // 双向公路
a[e[i].v].push_back({e[i].u, i}); // 双向公路
}
// 从评分最高的公路开始倒序枚举
for (int i = m; i >= 1; i--)
{
// 尝试免去第i条公路(当前剩余公路中景观评分最高的)
dijkstra(i);
// 移除这条公路,为下一轮准备
a[e[i].u].pop_back();
a[e[i].v].pop_back();
}
// 输出结果
if (ans == 1e18) // 如果没有找到从1到n的路径
{
cout << -1 << endl;
}
else
{
cout << ans << endl; // 输出最小运输费用
}
return 0; // 程序正常结束
}
【运行结果】
3 3
1 2 10 5
2 3 20 6
1 3 100 1
0
浙公网安备 33010602011771号