news 2026/7/29 17:18:55

算法题 K 站中转内最便宜的航班

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
算法题 K 站中转内最便宜的航班

K 站中转内最便宜的航班

问题描述

n个城市,编号从0n - 1。给你一个航班数组flights,其中flights[i] = [from_i, to_i, price_i]表示从城市from_i到城市to_i的航班价格为price_i

给你三个整数src(出发城市)、dst(目的地城市)和k(最多中转次数),返回从srcdst最多经过 k 次中转的最便宜价格。如果没有这样的路线,返回-1

注意:中转次数 = 航班次数 - 1。例如,直接飞行(0次中转)使用1个航班。

示例

输入: n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1 输出: 700 解释: 在最多 1 次中转的情况下,最便宜的路线是 0->1->3,总费用为 100 + 600 = 700。

算法思路

带限制条件的最短路径问题,不能直接使用 Dijkstra 算法,Dijkstra 无法处理边数限制。

核心

  1. 中转次数:最多 k 次中转意味着最多使用 k+1 条边
  2. 动态规划:可以按边数(或中转次数)进行状态转移

方法

  • Bellman-Ford 算法:天然支持边数限制
  • 动态规划dp[stops][city]表示经过 stops 次中转到达 city 的最小费用
  • BFS + 剪枝:按层数遍历,效率低

Bellman-Ford 核心思想

  1. 初始化:起点费用为0,其他为无穷大
  2. 迭代 k+1 次(最多 k+1 条边)
  3. 每次迭代中,基于上一轮的结果更新当前轮的最短距离
  4. 使用临时数组避免在同一轮中多次更新

代码实现

方法一:Bellman-Ford 算法

importjava.util.*;classSolution{/** * 使用Bellman-Ford算法解K站中转内最便宜的航班 * * @param n 城市数量 * @param flights 航班信息数组,每个元素为[出发城市, 到达城市, 价格] * @param src 出发城市 * @param dst 目的地城市 * @param k 最多中转次数 * @return 最便宜价格,如果不可达返回-1 */publicintfindCheapestPrice(intn,int[][]flights,intsrc,intdst,intk){// 初始化距离数组,dist[i]表示到达城市i的最小费用int[]dist=newint[n];Arrays.fill(dist,Integer.MAX_VALUE);dist[src]=0;// Bellman-Ford算法:最多进行k+1轮松弛操作// 最多k次中转,所以最多k+1条边for(intstops=0;stops<=k;stops++){// 创建临时数组,避免在同一轮中使用更新后的值int[]tempDist=Arrays.copyOf(dist,n);booleanupdated=false;// 遍历所有航班for(int[]flight:flights){intfrom=flight[0];intto=flight[1];intprice=flight[2];// 如果from城市在上一轮中可达,尝试更新to城市的费用if(dist[from]!=Integer.MAX_VALUE){intnewCost=dist[from]+price;if(newCost<tempDist[to]){tempDist[to]=newCost;updated=true;}}}// 如果本轮没有更新,提前结束if(!updated){break;}// 更新距离数组dist=tempDist;}// 返回结果returndist[dst]==Integer.MAX_VALUE?-1:dist[dst];}}

方法二:动态规划

classSolution{/** * 使用动态规划解K站中转内最便宜的航班 * * @param n 城市数量 * @param flights 航班信息 * @param src 出发城市 * @param dst 目的地城市 * @param k 最多中转次数 * @return 最便宜价格 */publicintfindCheapestPrice(intn,int[][]flights,intsrc,intdst,intk){// dp[i][j] 表示经过最多i次中转到达城市j的最小费用// i的范围是0到k+1(0次中转到k+1次中转)int[][]dp=newint[k+2][n];// 初始化:所有费用设为无穷大for(inti=0;i<=k+1;i++){Arrays.fill(dp[i],Integer.MAX_VALUE);}// 起点费用为0(0次中转到达起点)for(inti=0;i<=k+1;i++){dp[i][src]=0;}// 动态规划填表for(intstops=1;stops<=k+1;stops++){// 继承上一轮的结果(不使用新的航班)for(intcity=0;city<n;city++){dp[stops][city]=dp[stops-1][city];}// 尝试使用所有航班进行更新for(int[]flight:flights){intfrom=flight[0];intto=flight[1];intprice=flight[2];// 如果from城市在stops-1次中转内可达if(dp[stops-1][from]!=Integer.MAX_VALUE){intnewCost=dp[stops-1][from]+price;dp[stops][to]=Math.min(dp[stops][to],newCost);}}}// 返回最多k次中转(即k+1条边)的结果returndp[k+1][dst]==Integer.MAX_VALUE?-1:dp[k+1][dst];}}

方法三:Dijkstra 变种(带状态)

importjava.util.*;classSolution{/** * 使用Dijkstra算法的变种,状态包含(城市, 中转次数) */publicintfindCheapestPrice(intn,int[][]flights,intsrc,intdst,intk){// 构建邻接表List<int[]>[]graph=newList[n];for(inti=0;i<n;i++){graph[i]=newArrayList<>();}for(int[]flight:flights){graph[flight[0]].add(newint[]{flight[1],flight[2]});}// 优先队列:[费用, 城市, 中转次数]PriorityQueue<int[]>pq=newPriorityQueue<>((a,b)->a[0]-b[0]);pq.offer(newint[]{0,src,0});// 记录到达每个城市在不同中转次数下的最小费用int[][]minCost=newint[n][k+2];for(inti=0;i<n;i++){Arrays.fill(minCost[i],Integer.MAX_VALUE);}minCost[src][0]=0;while(!pq.isEmpty()){int[]current=pq.poll();intcost=current[0];intcity=current[1];intstops=current[2];// 如果到达目的地,返回费用if(city==dst){returncost;}// 如果中转次数已达到上限,不能继续if(stops>k){continue;}// 遍历邻居for(int[]neighbor:graph[city]){intnextCity=neighbor[0];intprice=neighbor[1];intnewCost=cost+price;intnewStops=stops+1;// 如果找到更优的路径if(newCost<minCost[nextCity][newStops]){minCost[nextCity][newStops]=newCost;pq.offer(newint[]{newCost,nextCity,newStops});}}}return-1;}}

算法分析

  • 时间复杂度

    • Bellman-Ford:O(k × E),其中E是航班数量
    • 动态规划:O(k × E)
    • Dijkstra变种:O(E × k × log(V × k))
  • 空间复杂度

    • Bellman-Ford:O(V)
    • 动态规划:O(V)
    • Dijkstra变种:O(V × k)

算法过程

n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1

Bellman-Ford

初始化

  • dist = [0, ∞, ∞, ∞]

第1轮(0次中转,1条边)

  • 处理航班[0,1,100]:dist[1] = min(∞, 0+100) = 100
  • 其他航班的from城市不可达
  • dist = [0, 100, ∞, ∞]

第2轮(1次中转,2条边)

  • 基于第1轮的dist = [0, 100, ∞, ∞]
  • 处理航班[1,3,600]:dist[3] = 100 + 600 = 700
  • 处理航班[1,2,100]:dist[2] = 100 + 100 = 200
  • 航班[2,3,200]不能使用,因为第1轮中dist[2] = ∞
  • 所以dist[3] = 700

测试用例

publicstaticvoidmain(String[]args){Solutionsolution=newSolution();// 测试用例1:标准示例int[][]flights1={{0,1,100},{1,2,100},{2,0,100},{1,3,600},{2,3,200}};System.out.println("Test 1: "+solution.findCheapestPrice(4,flights1,0,3,1));// 700// 测试用例2:无法到达int[][]flights2={{0,1,100},{1,2,100}};System.out.println("Test 2: "+solution.findCheapestPrice(3,flights2,0,2,0));// -1 (需要1次中转,但k=0)// 测试用例3:直接可达int[][]flights3={{0,1,100}};System.out.println("Test 3: "+solution.findCheapestPrice(2,flights3,0,1,0));// 100// 测试用例4:多条路径int[][]flights4={{0,1,100},{0,2,500},{1,2,100}};System.out.println("Test 4: "+solution.findCheapestPrice(3,flights4,0,2,1));// 200// 测试用例5:中转次数为0int[][]flights5={{0,1,100},{1,2,100},{0,2,500}};System.out.println("Test 5: "+solution.findCheapestPrice(3,flights5,0,2,0));// 500// 测试用例6:大价格值int[][]flights6={{0,1,1000000},{1,2,1000000}};System.out.println("Test 6: "+solution.findCheapestPrice(3,flights6,0,2,1));// 2000000// 测试用例7:起点等于终点int[][]flights7={{0,1,100}};System.out.println("Test 7: "+solution.findCheapestPrice(2,flights7,0,0,0));// 0// 测试用例8:复杂的多路径int[][]flights8={{0,1,1},{0,2,5},{1,2,1},{2,3,1}};System.out.println("Test 8: "+solution.findCheapestPrice(4,flights8,0,3,1));// -1 (需要2次中转)System.out.println("Test 9: "+solution.findCheapestPrice(4,flights8,0,3,2));// 3 (0->1->2->3)// 测试用例10:环路情况int[][]flights10={{0,1,100},{1,2,100},{2,0,100},{1,3,600}};System.out.println("Test 10: "+solution.findCheapestPrice(4,flights10,0,3,1));// 700}

关键点

  1. 中转次数

    • k次中转 = k+1条边
    • Bellman-Ford需要执行k+1轮
  2. 临时数组

    • 使用临时数组避免在同一轮中多次更新
    • Bellman-Ford算法的核心
  3. 边界情况处理

    • 起点等于终点:费用为0
    • 无法到达:返回-1
    • 直接可达:1条边,0次中转

常见问题

  1. 为什么不能用Dijkstra算法?

    • Dijkstra假设一旦找到最短路径就不会被更新
    • 这里路径可能更长但中转次数更少,需要考虑所有可能性
  2. 临时数组?

    • 确保每轮操作只基于上一轮的结果
    • 避免在同一轮中使用刚更新的值,导致错误的多步更新
  3. 如何处理负权边?

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

Quill vs 传统文本编辑:开发效率对比

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个对比演示项目&#xff0c;展示Quill富文本编辑器与传统textarea实现的区别&#xff1a;1. 实现相同的编辑功能&#xff08;文本格式、图片插入&#xff09;2. 比较代码复杂…

作者头像 李华
网站建设 2026/7/28 14:49:07

js for in零基础入门指南

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个js for in学习应用&#xff0c;提供交互式教程和新手友好的界面。点击项目生成按钮&#xff0c;等待项目生成完整后预览效果 理解for...in循环的基础概念 作为一个JavaScr…

作者头像 李华
网站建设 2026/7/28 18:35:40

26、文件和目录管理:全面解析与操作指南

文件和目录管理:全面解析与操作指南 1. 查找文件 在指定目录中查找特定文件名的功能可以通过 find_file_in_dir() 函数实现,该函数使用 readdir() 来遍历目录内容。以下是具体的代码实现: /** find_file_in_dir - searches the directory path for a* file named fi…

作者头像 李华
网站建设 2026/7/29 4:17:11

30、深入理解Linux内存管理:机制、优化与实践

深入理解Linux内存管理:机制、优化与实践 1. 内存映射与高级分配 在Linux系统中,内存映射是一种重要的内存管理技术。例如,通过 mmap 函数可以将 /dev/zero 设备文件的一部分映射到内存中,示例代码如下: /* map [0,page size) of /dev/zero */ p = mmap (NULL, …

作者头像 李华
网站建设 2026/7/28 12:05:54

零基础学SM2:国密算法入门指南

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 制作一个SM2算法教学Demo&#xff1a;1. 图形化展示椭圆曲线数学原理 2. 分步骤演示密钥生成过程 3. 提供可交互的加密/解密示例 4. 常见问题解答模块。使用Jupyter Notebook实现&a…

作者头像 李华
网站建设 2026/7/28 7:32:43

MySQL存储过程VS传统SQL:性能对比实测

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个性能测试场景&#xff0c;比较MySQL存储过程和传统SQL语句在处理批量数据时的效率差异。具体要求&#xff1a;1. 生成测试数据&#xff08;10万条记录&#xff09;&#xf…

作者头像 李华