当前位置: 首页 > news >正文

蓝桥杯省赛真题解析:用线段树+优先队列搞定‘小蓝的旅行计划’(附Java完整代码)

蓝桥杯省赛算法精解:线段树与优先队列在旅行加油问题中的协同应用

第一次看到"小蓝的旅行计划"这道题时,很多选手会被题目中复杂的加油规则和油箱限制条件弄得晕头转向。这道来自蓝桥杯省赛的真题,表面上看是一个简单的贪心问题,但深入分析后会发现它巧妙地融合了区间维护动态决策两大算法难点。本文将带你从问题本质出发,通过线段树与优先队列的协同工作,找到最优解的实现路径。

1. 问题重述与核心难点分析

题目描述小蓝需要驾驶汽车穿越一系列加油站,每个加油站i有三个关键参数:

  • 到达下一个加油站的距离dis[i]
  • 当前加油站的油价cost[i]
  • 该加油站单次加油量的上限lim[i]

油箱总容量为m,要求计算出完成整个旅程的最小油费。如果没有可行方案,则返回-1。

为什么简单的优先队列解法会失效?

初看这个问题,很多同学会想到用优先队列(最小堆)来维护油价最低的加油站,采用贪心策略每次在油价最低的站点加油。这种方法在没有油箱限制时确实有效,但引入油箱容量限制后会出现两个致命问题:

  1. 油量动态变化难以追踪:在某站加油后,后续站点的剩余油量状态都会改变
  2. 区间最大值维护需求:需要知道两个加油站之间行驶过程中的最大油量消耗
// 简单优先队列解法的问题示例 PriorityQueue<GasStation> queue = new PriorityQueue<>(); queue.addAll(stations); // 无法处理油量限制导致的动态调整

2. 算法框架设计:双数据结构协同

要解决这个难题,我们需要设计一个双数据结构系统

  1. 优先队列:负责快速获取当前可选的油价最低加油站
  2. 线段树:实时维护任意区间内的油量最大值

2.1 线段树的角色与实现

线段树在此问题中主要负责高效处理以下两种操作:

  • 区间查询:查询[i,j]区间内的最大油量值
  • 区间更新:当在某站加油后,更新相关区间的油量值
class SegmentTree { int[] maxRest; // 区间最大剩余油量 int[] lazy; // 懒惰标记 void pushUp(int rt) { maxRest[rt] = Math.max(maxRest[rt<<1], maxRest[rt<<1|1]); } void pushDown(int rt) { if(lazy[rt] != 0) { lazy[rt<<1] += lazy[rt]; lazy[rt<<1|1] += lazy[rt]; maxRest[rt<<1] += lazy[rt]; maxRest[rt<<1|1] += lazy[rt]; lazy[rt] = 0; } } int query(int L, int R, int l, int r, int rt) { if(L <= l && r <= R) return maxRest[rt]; pushDown(rt); int mid = (l + r) >> 1; int res = 0; if(L <= mid) res = Math.max(res, query(L, R, l, mid, rt<<1)); if(R > mid) res = Math.max(res, query(L, R, mid+1, r, rt<<1|1)); return res; } void update(int L, int R, int val, int l, int r, int rt) { if(L <= l && r <= R) { lazy[rt] += val; maxRest[rt] += val; return; } pushDown(rt); int mid = (l + r) >> 1; if(L <= mid) update(L, R, val, l, mid, rt<<1); if(R > mid) update(L, R, val, mid+1, r, rt<<1|1); pushUp(rt); } }

2.2 优先队列的优化使用

优先队列需要存储加油站信息并按油价排序,但需要注意:

  1. 动态调整:当某加油站的剩余可加油量为0时,需从队列中移除
  2. 批量处理:当油量不足时,可能需要连续从多个低价加油站加油
PriorityQueue<GasStation> queue = new PriorityQueue<>( (a, b) -> Long.compare(a.cost, b.cost) ); // 加油站类定义 class GasStation { int id; long cost; int limit; // 构造函数等... }

3. 关键算法步骤详解

3.1 初始化阶段

  1. 构建空的线段树,用于维护各站点的油量信息
  2. 将所有加油站按油价排序存入优先队列
  3. 初始化当前油量vol = 油箱容量m
SegmentTree st = new SegmentTree(n); PriorityQueue<GasStation> queue = new PriorityQueue<>(); for(int i = 0; i < n; i++) { queue.add(new GasStation(i, cost[i], lim[i])); } int vol = m;

3.2 主循环处理

对于每个加油站i(1 ≤ i ≤ n):

  1. 消耗dis[i]油量到达该站
  2. 检查油量是否不足(vol < 0)
    • 不足时从优先队列取油价最低的加油站补油
  3. 处理当前加油站的加油可能性
for(int i = 1; i <= n; i++) { vol -= dis[i]; // 消耗油量到达本站 // 油量不足时的处理 while(vol < 0 && !queue.isEmpty()) { GasStation best = queue.poll(); int maxAdd = Math.min( m - st.query(best.id, i-1), // 考虑区间最大油量限制 best.limit // 本站加油上限 ); if(maxAdd <= 0) continue; if(maxAdd <= -vol) { // 可以加满需求油量 totalCost += best.cost * maxAdd; vol += maxAdd; best.limit = 0; st.update(best.id, i-1, maxAdd); } else { // 只能加部分油量 totalCost += best.cost * (-vol); best.limit = maxAdd + vol; st.update(best.id, i-1, -vol); queue.add(best); // 放回队列 vol = 0; } } if(vol < 0) { // 油量仍不足且无加油站可用 System.out.println(-1); return; } // 处理当前加油站 if(vol > 0) { st.update(i, i, vol); } queue.add(new GasStation(i, cost[i], Math.min(lim[i], m - vol))); }

3.3 复杂度分析

该算法的时间复杂度主要来自:

  1. 优先队列操作:O(n log n)
  2. 线段树查询和更新:每次O(log n),共O(n log n)

总时间复杂度为O(n log n),空间复杂度O(n),完全适用于题目给定的数据范围。

4. 完整Java实现与关键注释

以下是整合后的完整解决方案,包含详细的注释说明:

import java.util.*; public class TravelPlan { static class GasStation implements Comparable<GasStation> { int id; long cost; int limit; public GasStation(int id, long cost, int limit) { this.id = id; this.cost = cost; this.limit = limit; } @Override public int compareTo(GasStation o) { return Long.compare(this.cost, o.cost); } } static class SegmentTree { int[] maxRest; int[] lazy; int n; public SegmentTree(int n) { this.n = n; maxRest = new int[4 * n]; lazy = new int[4 * n]; } void pushUp(int rt) { maxRest[rt] = Math.max(maxRest[rt<<1], maxRest[rt<<1|1]); } void pushDown(int rt) { if(lazy[rt] != 0) { lazy[rt<<1] += lazy[rt]; lazy[rt<<1|1] += lazy[rt]; maxRest[rt<<1] += lazy[rt]; maxRest[rt<<1|1] += lazy[rt]; lazy[rt] = 0; } } int query(int L, int R) { return query(L, R, 1, n, 1); } private int query(int L, int R, int l, int r, int rt) { if(L <= l && r <= R) return maxRest[rt]; pushDown(rt); int mid = (l + r) >> 1; int res = 0; if(L <= mid) res = Math.max(res, query(L, R, l, mid, rt<<1)); if(R > mid) res = Math.max(res, query(L, R, mid+1, r, rt<<1|1)); return res; } void update(int L, int R, int val) { update(L, R, val, 1, n, 1); } private void update(int L, int R, int val, int l, int r, int rt) { if(L <= l && r <= R) { lazy[rt] += val; maxRest[rt] += val; return; } pushDown(rt); int mid = (l + r) >> 1; if(L <= mid) update(L, R, val, l, mid, rt<<1); if(R > mid) update(L, R, val, mid+1, r, rt<<1|1); pushUp(rt); } } public static long solve(int m, int[] dis, int[] cost, int[] lim) { int n = dis.length - 1; // 假设dis[0]不用,从1开始 SegmentTree st = new SegmentTree(n); PriorityQueue<GasStation> queue = new PriorityQueue<>(); for(int i = 1; i <= n; i++) { queue.add(new GasStation(i, cost[i], lim[i])); } long totalCost = 0; int vol = m; for(int i = 1; i <= n; i++) { vol -= dis[i]; while(vol < 0 && !queue.isEmpty()) { GasStation best = queue.poll(); int maxAdd = Math.min( m - st.query(best.id, i-1), best.limit ); if(maxAdd <= 0) continue; if(maxAdd <= -vol) { totalCost += best.cost * maxAdd; vol += maxAdd; best.limit = 0; st.update(best.id, i-1, maxAdd); } else { totalCost += best.cost * (-vol); best.limit = maxAdd + vol; st.update(best.id, i-1, -vol); queue.add(best); vol = 0; } } if(vol < 0) return -1; if(vol > 0) { st.update(i, i, vol); } queue.add(new GasStation(i, cost[i], Math.min(lim[i], m - vol))); } return totalCost; } public static void main(String[] args) { // 示例输入 int m = 10; int[] dis = {0, 2, 3, 5}; // dis[0]不用 int[] cost = {0, 3, 2, 4}; // cost[0]不用 int[] lim = {0, 5, 3, 2}; // lim[0]不用 long result = solve(m, dis, cost, lim); System.out.println("最小油费: " + result); } }

5. 算法优化与边界情况处理

在实际编码竞赛中,还需要考虑以下优化和边界情况:

  1. 输入输出优化:使用BufferedReader代替Scanner处理大规模输入
  2. 提前终止:当油量不足且无加油站可用时立即返回-1
  3. 初始化检查:总距离是否超过初始油量能支持的最大距离
  4. 零距离站点:处理相邻加油站距离为0的特殊情况
// 输入优化示例 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st = new StringTokenizer(br.readLine()); int n = Integer.parseInt(st.nextToken()); int m = Integer.parseInt(st.nextToken());

对于树状数组和线段树的选用,虽然两者都能解决区间查询问题,但在这个问题中线段树更合适,因为:

  1. 需要支持区间更新操作
  2. 查询的是区间最大值而非简单求和
  3. 代码结构更清晰,易于调试

在真实的竞赛场景中,建议先写出基础版本确保正确性,再根据时间限制决定是否进行常数优化。这道题的解题过程很好地展示了如何将现实问题抽象为算法模型,并通过数据结构的组合应用来解决复杂约束条件下的优化问题。

http://www.jsqmd.com/news/724359/

相关文章:

  • 《Windows Internals》读书笔记 10.4.4:WMI 提供程序(Providers)——WMI 与底层系统资源之间真正的桥梁
  • 【MySQL | 第八篇】索引的使用
  • 文本换行处理
  • Unity游戏自动翻译终极指南:XUnity.AutoTranslator让外语游戏秒变中文
  • 注入灵魂:从架构设计到数据能力的“降维打击”
  • 千问 LeetCode 1932.合并多棵二叉搜索树public TreeNode canMerge(List<TreeNode> trees)
  • Windows驱动管理终极指南:DriverStoreExplorer让你轻松掌控驱动程序
  • 海外短剧APP开发,从0到1:硬刚谷歌商店合规,打通海外多币种支付!
  • 单细胞分析避坑指南:用DoubletFinder精准揪出那些“伪装”的双细胞(附完整R代码)
  • 【C#】三菱PLC MC协议通信:1E帧与3E帧报文解析+C#上位机源码(附完整工程)
  • 4月30日
  • 如何在3分钟内获取VMware Workstation Pro 17免费许可证密钥:虚拟化入门完整指南
  • Transformer在文档级事件抽取中的应用与优化
  • Heretic-v1.2.0烧蚀GLM4.7,离线环境进行
  • 2026 年 6 款热门文档生成工具实测盘点:覆盖论文、文案、办公全场景
  • Go 语言从入门到进阶 | 第 19 章:测试与基准测试
  • 千问 LeetCode 1932.合并多棵二叉搜索树 TypeScript实现
  • 外边距问题 塌陷问题 HTML CSS
  • 主从DNS服务器实验
  • Element UI el-select全选功能避坑指南:数据量大时卡顿、样式错位、v-model失效怎么办?
  • 别再只盯着带宽了!深入DP1.2协议,看懂“链路速率与像素时钟解耦”到底多重要
  • MySQL 索引失效的典型案例分析
  • 如何用AI插件让Zotero文献管理效率提升300%?探索GPT智能分析新范式
  • XHS-Downloader:如何用开源工具高效管理你的小红书数字资产?
  • 从零吃透YOLOv1-v3:发展脉络、核心原理与实战必备知识点
  • DeepSeek LeetCode 1938.查询最大基因差 public int[] maxGeneticDifference(int[] parents, int[][] queries)
  • 魔兽争霸3终极优化指南:5分钟解决所有兼容性问题
  • 别再折腾root了!用Finalshell一键连接Ubuntu普通用户,附权限配置全攻略
  • HikariCP连接池配置避坑指南:从`connection-timeout: 30000ms`报错聊起,我的Spring Boot调优实战
  • window11使用wsl2下载编译android 8代码,并用emulator运行