D. The way home - 题解
比赛与标签
比赛: Codeforces Round 857 (Div. 1) 标签: binary search, data structures, dp, graphs, greedy, shortest paths, sortings 难度: *2100
喵~ 题目讲了什么呀?
你好呀,我是乐于助人的猫娘!这道题目其实是一个很有趣的回家之旅呐~
我们扮演的魔法师布迪尼,在城市1被抢劫了,只剩下 p
个金币。他的家在城市 n
。幸运的是,他可以在任何一个城市 i
进行表演来赚钱,每场表演能赚 w_i
个金币,表演次数不限!
整个国家有 m
条单向航线,每条航线从城市 a
到城市 b
需要花费 s
个金币。要坐飞机,我们的钱必须够数才行哦。
我们的目标是,用最少的总表演次数,成功从城市1回到城市 n
。如果回不了家,就告诉我们这个坏消息,喵~
简单来说就是:
- 起点: 城市 1,有
p
金币。 - 终点: 城市
n
。 - 目标: 最小化总表演次数。
- 行动:
- 在当前城市表演赚钱。
- 花钱乘坐飞机去另一个城市。
本猫娘的思考过程喵~
这道题呀,第一眼看上去就很像一个图论里的最短路问题,对吧?城市是图上的点,航班就是有向边。但是,边的“权重”是什么呢?是我们需要的表演次数,可这个次数不是固定的,它取决于我们到达一个城市时有多少钱,以及我们在哪里表演。这就有点复杂了呢。
为什么朴素的Dijkstra不行?
如果我们只把表演次数当作距离,用 dist[u]
表示到达城市 u
的最少表演次数,会遇到一个问题:
假设有两条路都能到达城市 u
:
- 表演了10次,到达时剩下100金币。
- 表演了10次,到达时剩下500金币。
虽然表演次数相同,但第二种情况明显更优,因为它能让我们在后续的旅途中少表演几次!所以,我们的状态不仅要关心表演次数,还要关心剩余金币。
进化!带状态的Dijkstra
既然这样,我们就需要一个更丰富的状态来描述我们的处境。一个自然的想法是:dist[u]
存储到达城市 u
的 {最少表演次数, 最多剩余金币}
。
但是,还有一个更关键的问题:我们在哪里表演赚钱最划算呢?
题目说“在城市 i
,他可以组织表演...”,这似乎暗示我们只能在当前所在的城市表演。但仔细想想,如果我们需要一大笔钱来坐飞机,而当前城市的 w_i
很低,但我们之前经过了一个 w_j
很高的城市 j
,我们最明智的选择是什么?当然是(概念上)回到城市 j
去表演啦!因为表演本身不消耗时间和其它成本,我们总可以利用上我们整个旅途已经解锁的最高赚钱效率!
这是一个非常重要的贪心思想!喵~ 也就是说,在任何需要用钱的时刻,我们都会用至今为止遇到过的最大的 w_i
来赚钱。
最终的解题思路!
基于这个贪心策略,我们的Dijkstra状态就清晰了!状态不仅仅是当前在哪座城市,还需要记录“至今为止解锁的最高赚钱效率”。
- 状态定义: 我们定义一个状态为
(u, max_w_idx)
,表示当前在城市u
,并且在从起点1到u
的路径上,所经过的城市中赚钱效率最高的那个城市的“排名”是max_w_idx
。(我们预先把所有城市的w_i
从大到小排个序,max_w_idx
就是这个排序后的索引)。 - 距离数组:
dist[u][max_w_idx]
存储一个pair<long long, long long>
,分别表示到达(u, max_w_idx)
这个状态所需要的{最少表演次数, 对应表演次数下的最大剩余金币}
。 - 优先队列: 使用优先队列(最小堆)来进行Dijkstra。队列中存储的元素是
State{perfs, money, u, max_w_idx}
。我们优先选择perfs
最小的,如果perfs
相同,就选择money
最多的(因为钱多不压身嘛~)。 - 初始化:
- 将所有城市的
w_i
从大到小排序,并记录每个城市的排名w_rank
。 dist
数组全部初始化为无穷大。- 将起点状态放入优先队列:
{0, p, 1, w_rank[1]}
。意思是,在城市1,表演0次,有p个金币,当前最高赚钱效率的城市就是城市1本身。
- 将所有城市的
- Dijkstra主循环:
- 取出队首状态
current = {k, money, u, max_w_idx}
。 - 如果这个状态已经不是最优的了(即
dist[u][max_w_idx]
中记录的表演次数更少,或者次数相同但钱更多),就跳过它。 - 如果
u
是终点n
,那么k
就是我们找到的答案!可以直接返回啦。 - 遍历从
u
出发的所有航班(u, v, cost)
:- 计算需要的表演次数:当前赚钱效率是
W = rank_to_w[max_w_idx]
。如果money < cost
,需要补足的钱是cost - money
,那么需要的表演次数就是ceil((cost - money) / W)
,用代码实现就是(cost - money + W - 1) / W
。 - 计算新状态:
- 总表演次数
next_k = k + needed_perfs
。 - 剩余金币
next_money = money + needed_perfs * W - cost
。 - 新的最高赚钱效率排名
next_max_w_idx = min(max_w_idx, w_rank[v])
。因为到达了新城市v
,我们解锁的最高赚钱效率可能是v
的,也可能是之前路径上的,取两者中效率更高的那个(也就是排名更靠前的那个)。
- 总表演次数
- 如果新状态比
dist[v][next_max_w_idx]
更优,就更新dist
并把新状态推入优先队列。
- 计算需要的表演次数:当前赚钱效率是
- 取出队首状态
这样,我们就能找到到达城市 n
的最小表演次数了!如果队列空了还没到终点,那就是无解,输出-1。
来看代码实现吧!
// 完整的AC代码,添加详细注释解释关键逻辑
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <map>
#include <tuple>
using namespace std;
// 用一个很大的数表示无穷大,喵~
const long long INF = 4e18;
// Dijkstra中优先队列的状态结构体
struct State {
long long perfs; // 总表演次数
long long money; // 剩余金币
int u; // 当前所在城市
int max_w_idx; // 路径上遇到的最高w值的排名(索引)
// 自定义比较函数,让优先队列变成最小堆
// 优先按表演次数升序,次数相同则按金币降序
bool operator>(const State& other) const {
if (perfs != other.perfs) {
return perfs > other.perfs;
}
return money < other.money;
}
};
void solve() {
int n;
long long m, p;
cin >> n >> m >> p;
vector<long long> w(n + 1);
vector<pair<long long, int>> w_sorted(n);
for (int i = 1; i <= n; ++i) {
cin >> w[i];
w_sorted[i - 1] = {w[i], i};
}
// 将城市的赚钱效率从高到低排序
sort(w_sorted.rbegin(), w_sorted.rend());
// 预处理,方便快速查找
vector<int> w_rank(n + 1); // 城市id -> 赚钱效率排名
vector<int> rank_to_city(n); // 排名 -> 城市id
vector<long long> rank_to_w(n); // 排名 -> 赚钱效率值
for (int i = 0; i < n; ++i) {
w_rank[w_sorted[i].second] = i;
rank_to_city[i] = w_sorted[i].second;
rank_to_w[i] = w_sorted[i].first;
}
// 邻接表存图
vector<vector<pair<int, int>>> adj(n + 1);
for (int i = 0; i < m; ++i) {
int u, v, s;
cin >> u >> v >> s;
adj[u].push_back({v, s});
}
// 距离数组,dist[城市][最高w值排名] = {最少表演次数, 最多金币}
vector<vector<pair<long long, long long>>> dist(n + 1, vector<pair<long long, long long>>(n, {INF, -1}));
// 优先队列,用于Dijkstra
priority_queue<State, vector<State>, greater<State>> pq;
// 初始化起点状态
int start_city = 1;
int start_max_w_idx = w_rank[start_city];
dist[start_city][start_max_w_idx] = {0, p};
pq.push({0, p, start_city, start_max_w_idx});
while (!pq.empty()) {
State current = pq.top();
pq.pop();
long long k = current.perfs;
long long money = current.money;
int u = current.u;
int max_w_idx = current.max_w_idx;
// 如果当前状态不是最优解,就跳过,喵~
if (k > dist[u][max_w_idx].first || (k == dist[u][max_w_idx].first && money < dist[u][max_w_idx].second)) {
continue;
}
// 到达终点啦!k就是答案!
if (u == n) {
cout << k << endl;
return;
}
// 当前可用的最高赚钱效率
long long current_w = rank_to_w[max_w_idx];
// 遍历所有从u出发的航班
for (const auto& edge : adj[u]) {
int to = edge.first;
int cost = edge.second;
long long needed_perfs = 0;
// 如果钱不够,就要表演赚钱
if (money < cost) {
long long money_needed = cost - money;
// 向上取整的经典写法
needed_perfs = (money_needed + current_w - 1) / current_w;
}
long long next_k = k + needed_perfs;
long long next_money = money + needed_perfs * current_w - cost;
// 到达新城市后,更新路径上的最高赚钱效率排名
int next_max_w_idx = min(max_w_idx, w_rank[to]);
// 松弛操作:如果找到了更优的路径
if (dist[to][next_max_w_idx].first > next_k ||
(dist[to][next_max_w_idx].first == next_k && dist[to][next_max_w_idx].second < next_money)) {
dist[to][next_max_w_idx] = {next_k, next_money};
pq.push({next_k, next_money, to, next_max_w_idx});
}
}
}
// 队列空了还没到终点,说明回不了家了QAQ
cout << -1 << endl;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
复杂度分析喵
时间复杂度: O(N * M * log(N)) 的说。 这里的状态数是
城市数 * 赚钱效率排名数
,即N * N
。每条图的边(u, v)
会对应N
条状态图中的边(因为max_w_idx
可以有N
种取值)。所以状态图的边数是O(N * M)
。Dijkstra算法的复杂度是O(E_state * log(V_state))
,这里就是O(N * M * log(N*N))
,简化后是O(N * M * log(N))
。考虑到题目给的N
和M
的总和限制,这个复杂度是可以通过的。空间复杂度: O(N * N) 的说。 主要是
dist
数组占用的空间,它的大小是(N+1) * N
,所以是O(N^2)
。
知识点与总结时间!
这道题真是一次有趣的冒险呢,它教会了我们几件重要的事情喵~
- Dijkstra的灵活应用: 不要把Dijkstra想得太死板!它的核心是找到一个“最优”路径,这个“最优”可以是我们定义的任何东西。在这里,它是一个包含
(表演次数, 剩余金币)
的复合状态。 - 状态设计是关键: 解决复杂图论问题的核心往往在于如何定义一个既能完整描述问题、又不会导致状态爆炸的状态。本题中,通过贪心策略,我们将“路径历史”压缩为“至今最高赚钱效率排名”这一单个维度,是解题的关键一步。
- 贪心思想的力量: “总是在赚钱效率最高的已解锁城市表演”这个贪心选择,极大地简化了问题。在算法设计中,要时刻寻找可以贪心的地方!
- 编程小技巧:
pair
和自定义的struct
在优先队列中的使用非常方便。- 向上取整
(a + b - 1) / b
是一个需要牢记的实用小公式。 - 处理多组测试用例时,记得每次都要清空和初始化数据结构哦。
希望这篇题解能帮到你!如果还有不明白的地方,随时可以再来问本猫娘哦!加油,你一定可以的,喵~ >w<