news 2026/10/15 8:21:25

贪心算法解决旅行家预算问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
贪心算法解决旅行家预算问题

1. 问题背景与需求分析

旅行家预算问题是一个经典的贪心算法应用场景,源自洛谷在线评测系统的P1016题目。该问题描述了一位旅行家驾驶汽车以固定速度在一条直线上行驶,需要从起点到达终点。沿途有若干加油站,每个加油站有不同的油价。旅行家需要在保证能够到达终点的前提下,计算出最少的加油费用。

这个问题的现实意义非常明显——它直接模拟了我们在长途自驾游时的真实决策过程。油箱容量有限,加油站油价各异,如何规划加油策略才能既保证不抛锚又最省钱?这不仅是算法竞赛中的经典题型,更是日常生活中每个人都会遇到的实用问题。

2. 问题建模与算法选择

2.1 输入参数定义

题目给出的关键参数包括:

  • D1:起点到终点的总距离(单位:公里)
  • C:油箱的最大容量(单位:升)
  • D2:汽车每升油可以行驶的距离(单位:公里/升)
  • N:沿途加油站的数量
  • 每个加油站的Di和Pi:表示该加油站距离起点的距离和每升油的价格

通过这些参数,我们可以计算出几个重要中间量:

  • 满油行驶距离 = C * D2
  • 相邻加油站间距 = Di+1 - Di

2.2 贪心算法适用性分析

这个问题具有典型的贪心选择性质——在每个决策点(加油站)做出局部最优选择,最终能够得到全局最优解。具体来说,在每个加油站,我们只需要考虑在当前油量和价格条件下,如何加油才能在到达下一个更便宜的加油站(或终点)时花费最少。

贪心算法之所以适用,是因为它具有以下两个关键性质:

  1. 最优子结构:问题的最优解包含子问题的最优解
  2. 贪心选择性质:局部最优选择能导致全局最优解

3. 算法实现细节

3.1 预处理阶段

在开始算法前,需要进行几个重要预处理:

  1. 将终点视为一个虚拟加油站,距离为D1,油价为0(因为不需要在终点加油)
  2. 检查相邻加油站间距是否超过满油行驶距离,若是则直接判定无解
  3. 将所有加油站按距离排序(题目通常已排序,但需要确认)
struct Station { double distance; double price; }; vector<Station> stations; // 添加起点和终点 stations.push_back({0, 0}); // 起点油价设为0 // 添加中间加油站... stations.push_back({D1, 0}); // 终点油价设为0 // 检查是否可达 for(int i = 1; i < stations.size(); i++) { if(stations[i].distance - stations[i-1].distance > C * D2) { cout << "No Solution" << endl; return 0; } }

3.2 核心贪心策略

算法的主要流程如下:

  1. 初始化当前油量current_gas = 0,总花费total_cost = 0
  2. 从起点出发,对于每个加油站i: a. 寻找下一个比当前加油站便宜的加油站j(或终点) b. 计算到达j需要的油量need_gas = (stations[j].distance - stations[i].distance)/D2 c. 如果当前油量不足need_gas:
    • 在当前加油站加刚好够到j的油量
    • 更新current_gas和total_cost d. 如果当前油量足够:
    • 不加油,直接开到j e. 如果找不到更便宜的加油站:
    • 在当前加油站加满油,开到下一个加油站
double current_gas = 0; double total_cost = 0; int current_pos = 0; while(current_pos < stations.size() - 1) { int next_pos = current_pos + 1; double min_price = stations[current_pos].price; int cheapest_pos = current_pos; // 寻找下一个更便宜的加油站 while(next_pos < stations.size() && stations[next_pos].distance - stations[current_pos].distance <= C * D2) { if(stations[next_pos].price < min_price) { min_price = stations[next_pos].price; cheapest_pos = next_pos; break; } next_pos++; } if(cheapest_pos != current_pos) { // 找到更便宜的加油站 double need_gas = (stations[cheapest_pos].distance - stations[current_pos].distance) / D2; if(current_gas < need_gas) { total_cost += (need_gas - current_gas) * stations[current_pos].price; current_gas = 0; } else { current_gas -= need_gas; } current_pos = cheapest_pos; } else { // 没找到更便宜的,加满油到下一个加油站 if(current_pos == stations.size() - 1) break; total_cost += (C - current_gas) * stations[current_pos].price; current_gas = C - (stations[current_pos+1].distance - stations[current_pos].distance)/D2; current_pos++; } }

4. 边界条件与特殊情况处理

4.1 不可达情况判断

在算法开始前,必须检查相邻加油站间距是否超过满油行驶距离C*D2。如果存在这样的间距,说明无论如何都无法到达终点,应直接输出"No Solution"。

4.2 终点处理技巧

将终点视为一个油价为0的虚拟加油站非常关键。这样在寻找更便宜加油站时,终点总是会被优先选择,确保我们不会错过最优解。

4.3 浮点数精度问题

由于涉及距离和油量的计算,浮点数精度可能影响结果。建议:

  1. 使用double而非float
  2. 比较时使用epsilon而非直接==
  3. 输出时按要求保留小数位数
const double EPS = 1e-6; if(fabs(a - b) < EPS) { // 视为相等 }

5. 算法优化与性能分析

5.1 时间复杂度优化

原始算法在最坏情况下时间复杂度为O(N^2),因为对于每个加油站可能需要遍历后面所有加油站。可以通过以下优化:

  1. 预处理每个加油站后面第一个更便宜的加油站位置(单调栈方法)
  2. 这样可以将时间复杂度降为O(N)
vector<int> next_cheaper(N+2); stack<int> s; for(int i = 0; i < stations.size(); i++) { while(!s.empty() && stations[s.top()].price > stations[i].price) { next_cheaper[s.top()] = i; s.pop(); } s.push(i); } while(!s.empty()) { next_cheaper[s.top()] = stations.size() - 1; // 指向终点 s.pop(); }

5.2 空间复杂度分析

算法只需要O(N)的额外空间存储加油站信息,空间复杂度优秀,适合处理大规模数据。

6. 实际应用与变种问题

6.1 现实生活中的应用

这个算法可以直接应用于:

  1. 自驾游路线规划
  2. 物流运输成本优化
  3. 电动汽车充电站选择
  4. 航空燃油补给策略

6.2 常见变种问题

  1. 有限加油站次数:限制最多可以加油k次
  2. 不同速度消耗:不同路段油耗不同
  3. 时间约束:需要在特定时间到达某些加油站
  4. 多车辆协同:多辆车需要协调加油策略

7. 常见错误与调试技巧

7.1 典型错误案例

  1. 没有正确处理终点情况,导致少算或多算
  2. 浮点数比较未考虑精度,导致判断错误
  3. 没有检查不可达情况,导致无限循环
  4. 油量计算时单位混淆(距离/油耗)

7.2 调试建议

  1. 先用手算小规模测试用例验证算法逻辑
  2. 打印中间变量(当前位置、当前油量、总花费)
  3. 特别注意边界条件(起点、终点、满油刚好到达等情况)
  4. 比较不同算法的中间结果差异

提示:在实现时,可以先不考虑优化,写出清晰的暴力解法,再逐步优化。贪心算法的正确性证明往往比实现更重要。

8. 代码实现完整示例

#include <iostream> #include <vector> #include <algorithm> #include <cmath> #include <stack> using namespace std; const double EPS = 1e-6; struct Station { double distance; double price; }; int main() { double D1, C, D2, P; int N; cin >> D1 >> C >> D2 >> P >> N; vector<Station> stations; stations.push_back({0, P}); // 起点加油站 for(int i = 0; i < N; i++) { double Di, Pi; cin >> Di >> Pi; stations.push_back({Di, Pi}); } stations.push_back({D1, 0}); // 终点虚拟加油站 // 按距离排序 sort(stations.begin(), stations.end(), [](const Station& a, const Station& b) { return a.distance < b.distance; }); // 检查是否可达 for(int i = 1; i < stations.size(); i++) { if(stations[i].distance - stations[i-1].distance > C * D2 + EPS) { cout << "No Solution" << endl; return 0; } } // 预处理每个位置后面第一个更便宜的加油站 vector<int> next_cheaper(stations.size()); stack<int> s; for(int i = 0; i < stations.size(); i++) { while(!s.empty() && stations[s.top()].price > stations[i].price) { next_cheaper[s.top()] = i; s.pop(); } s.push(i); } while(!s.empty()) { next_cheaper[s.top()] = stations.size() - 1; s.pop(); } double current_gas = 0; double total_cost = 0; int current_pos = 0; while(current_pos < stations.size() - 1) { int cheaper_pos = next_cheaper[current_pos]; if(cheaper_pos != current_pos) { // 可以到达更便宜的加油站 double need_gas = (stations[cheaper_pos].distance - stations[current_pos].distance) / D2; if(current_gas < need_gas) { total_cost += (need_gas - current_gas) * stations[current_pos].price; current_gas = 0; } else { current_gas -= need_gas; } current_pos = cheaper_pos; } else { // 需要加满油到下一个加油站 if(current_pos == stations.size() - 1) break; total_cost += (C - current_gas) * stations[current_pos].price; current_gas = C - (stations[current_pos+1].distance - stations[current_pos].distance)/D2; current_pos++; } } printf("%.2f\n", total_cost); return 0; }

9. 算法正确性证明

贪心算法的正确性可以通过以下方式证明:

  1. 最优子结构性质:假设我们已经到达加油站i,那么剩余问题(从i到终点)的最优解与之前的选择无关,只与当前油量和位置有关。

  2. 贪心选择性质:在每个加油站,选择加刚好够到达下一个更便宜加油站的油量(或终点)是最优的。因为:

    • 如果在更贵的加油站多加油,必然不如在便宜加油站加油划算
    • 如果加的油不足以到达下一个更便宜加油站,可能会导致后面不得不在更贵的地方加油
    • 加满油只有在后面没有更便宜加油站时才需要
  3. 数学归纳法:可以证明对于n个加油站的问题,贪心选择能够导致全局最优解。

10. 不同语言的实现差异

虽然算法逻辑相同,但不同语言实现时有以下注意事项:

10.1 Python实现

  • 注意浮点数精度问题
  • 可以使用列表和字典简化加油站存储
  • 输出格式化使用format或f-string

10.2 Java实现

  • 使用BigDecimal处理高精度计算
  • 自定义Comparator进行排序
  • 注意对象和基本类型的区别

10.3 JavaScript实现

  • 注意Number的精度限制
  • 排序时注意比较函数的返回值
  • 使用toFixed进行输出格式化

提示:无论使用哪种语言,核心算法逻辑不变,主要是语法和精度处理的差异。建议先用最熟悉的语言实现,再考虑移植到其他语言。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 8:21:25

diagram-design:用代码思维管理图表,告别风格失控与维护噩梦

1. 从一张草图到一套系统&#xff1a;diagram-design 到底在解决什么问题第一次听到 “diagram-design” 这个词&#xff0c;很多人会下意识觉得它只是“画图”的另一种说法。但真正在项目里被图表折磨过的人知道&#xff0c;画图本身从来不是最痛的部分。最痛的是&#xff1a;…

作者头像 李华
网站建设 2026/10/15 8:21:11

AIO Sandbox:集成浏览器/Shell/MCP的容器化Agent开发环境

1. 项目速览&#xff1a;一个容器装下 Agent 的全部工具链先说结论&#xff1a;AIO Sandbox 是个很有意思的开源项目&#xff0c;它把浏览器、Shell、文件系统、MCP Server、VSCode 这些 Agent 日常要用的东西&#xff0c;全部塞进了一个 Docker 容器里。你通过浏览器访问这个容…

作者头像 李华
网站建设 2026/10/15 8:21:11

微电网二次控制关键技术解析与工程实践

1. 微电网二次控制的核心挑战微电网作为分布式能源的重要载体&#xff0c;其控制策略直接关系到供电可靠性和电能质量。传统的一次控制&#xff08;Primary Control&#xff09;通过下垂控制实现功率分配&#xff0c;但存在稳态误差问题。这时就需要二次控制&#xff08;Second…

作者头像 李华
网站建设 2026/10/15 8:21:03

CDA历年真题逐步开放,高效刷题方法论与备考策略解析

1. 备考CDA这些年&#xff0c;真题一直是被卡得最死的一环先说句实在话&#xff0c;看到"CDA历年真题逐步开放"这条通知的时候&#xff0c;我脑子里闪过的第一个念头不是"终于有题做了"&#xff0c;而是"这届考生运气是真不错"。这句话放在数据分…

作者头像 李华
网站建设 2026/10/15 8:20:37

Spark Streaming消费Kafka稳定性核心参数解析

1. 项目概述&#xff1a;为什么这两个参数能决定Spark Streaming任务的生死线在某公司实时日志分析平台的运维现场&#xff0c;我连续三天凌晨三点被告警电话叫醒——Spark Streaming作业突然挂掉&#xff0c;Kafka消费位点停滞&#xff0c;下游Flink实时大屏数据断更。排查日志…

作者头像 李华
网站建设 2026/10/15 8:20:28

15GB Docker镜像离线部署与GPU推理调优:MonkeyOCRv2内网落地全记录

搞离线部署的朋友应该都体会过这种滋味&#xff1a;外面环境里明明跑得好好的服务&#xff0c;一到内网就变得无比拧巴。依赖拉不下来、模型权重动辄几个 G、Docker 镜像在网络不好的时候传到怀疑人生。MonkeyOCRv2 这个项目当初就卡在这一环——一个内部封装的光学字符识别推理…

作者头像 李华