787. K 站中转内最便宜的航班

有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flights[i] = [fromi, toi, pricei] ,表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi。
现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1。

示例 1:
输入: n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]] src = 0, dst = 2, k = 1 输出: 200 解释: 城市航班图如下 从城市 0 到城市 2 在 1 站中转以内的最便宜价格是 200,如图中红色所示。
示例 2:
输入: n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]] src = 0, dst = 2, k = 0 输出: 500 解释: 城市航班图如下 从城市 0 到城市 2 在 0 站中转以内的最便宜价格是 500,如图中蓝色所示。

提示:

  • 1 <= n <= 100
  • 0 <= flights.length <= (n * (n - 1) / 2)
  • flights[i].length == 3
  • 0 <= fromi, toi < n
  • fromi != toi
  • 1 <= pricei <= 104
  • 航班没有重复,且不存在自环
  • 0 <= src, dst, k < n
  • src != dst

思路:
方法1:DP
状态表示:f[i][j]表示经过i条边到达j的所有花费的集合。
属性:最小花费
状态转移:当最后处在j点时,考虑倒数第二点的位置k,是所有能一步到j点的所以点。
f[i][j] = Math.min(f[i - 1][k], f[i][j]) (k, j) ∈ flights
初始化:f[0][src] = 0
空间优化:考虑到f[i][j]只与f[i - 1][k]有关,故可用一维数组进行优化

  1. class Solution {
  2. public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
  3. int[] dist = new int[n];
  4. Arrays.fill(dist, 0x3f3f3f3f);
  5. dist[src] = 0;
  6. int[] back = new int[n];
  7. for (int i = 0; i <= k; i++) {
  8. back = Arrays.copyOf(dist, n);
  9. for (int[] edges : flights) {
  10. int a = edges[0], b = edges[1], c = edges[2];
  11. dist[b] = Math.min(dist[b], back[a] + c);
  12. }
  13. }
  14. if (dist[dst] >= 0x3f3f3f3f) return -1;
  15. return dist[dst];
  16. }
  17. }

方法2:SPFA(不带优化)
不带st数组判重,本质还是Bellman-Ford

  1. class Solution {
  2. int N = 10010;
  3. int[] h = new int[N], e = new int[N], w = new int[N], ne = new int[N];
  4. int n, idx;
  5. public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
  6. Arrays.fill(h, -1);
  7. this.n = n;
  8. for (int[] p : flights) {
  9. int a = p[0], b = p[1], c = p[2];
  10. add(a, b, c);
  11. }
  12. int[] dist = new int[n];
  13. Arrays.fill(dist, 0x3f3f3f3f);
  14. dist[src] = 0;
  15. // pos cnt fee
  16. Queue<int[]> q = new LinkedList<>();
  17. q.offer(new int[]{src, 0, 0});
  18. boolean[] st = new boolean[n];
  19. while (!q.isEmpty()) {
  20. int[] p = q.poll();
  21. int cur = p[0], cnt = p[1], fee = p[2];
  22. if (p[1] > k) break;
  23. for (int i = h[cur]; i != -1; i = ne[i]) {
  24. int j = e[i], cost = w[i];
  25. if (cost + fee < dist[j]) {
  26. dist[j] = cost + fee;
  27. q.offer(new int[]{j, cnt + 1, cost + fee});
  28. }
  29. }
  30. }
  31. return dist[dst] >= 0x3f3f3f3f ? -1 : dist[dst];
  32. }
  33. void add(int a, int b, int c) {
  34. e[idx] = b;
  35. w[idx] = c;
  36. ne[idx] = h[a];
  37. h[a] = idx++;
  38. }
  39. }

方法3:堆优化版Dijkstra
最小堆按最小花费来排序。
可能存在这样一种情况,有边数限制的情况下能从起点到终点,但花费超过了不考虑边数的最小花费。
所以更新边的时候需要考虑,尽管到该点的花费变多,但如果边数更少的话,可能对最终结果是有利的。
还有就是最终结果可能是处于最短边和最小花费的中间情况,这也是为什么用小根堆的原因之一。能保证花费少的先出队,而不是边数少的先出队

  1. class Solution {
  2. int N = 10010;
  3. int[] h = new int[N], e = new int[N], w = new int[N], ne = new int[N];
  4. int n, idx;
  5. public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
  6. Arrays.fill(h, -1);
  7. this.n = n;
  8. for (int[] p : flights) {
  9. int a = p[0], b = p[1], c = p[2];
  10. add(a, b, c);
  11. }
  12. int[] dist = new int[n];
  13. Arrays.fill(dist, 0x3f3f3f3f);
  14. dist[src] = 0;
  15. // pos cnt fee
  16. PriorityQueue<int[]> q = new PriorityQueue<>((o1, o2) -> o1[2] - o2[2]);
  17. q.offer(new int[]{src, 0, 0});
  18. int[] count = new int[n];
  19. Arrays.fill(count, 0x3f3f3f3f);
  20. count[src] = 0;
  21. k++;
  22. while (!q.isEmpty()) {
  23. int[] p = q.poll();
  24. int cur = p[0], cnt = p[1], fee = p[2];
  25. if (cnt + 1 > k) continue;
  26. for (int i = h[cur]; i != -1; i = ne[i]) {
  27. int j = e[i], cost = w[i];
  28. if (dist[j] > cost + fee) {
  29. dist[j] = cost + fee;
  30. count[j] = cnt + 1;
  31. q.offer(new int[]{j, cnt + 1, fee + cost});
  32. } else if (cnt + 1 <= count[j]) {
  33. count[j] = cnt + 1;
  34. q.offer(new int[]{j, cnt + 1, fee + cost});
  35. }
  36. }
  37. }
  38. return dist[dst] >= 0x3f3f3f3f ? -1 : dist[dst];
  39. }
  40. void add(int a, int b, int c) {
  41. e[idx] = b;
  42. w[idx] = c;
  43. ne[idx] = h[a];
  44. h[a] = idx++;
  45. }
  46. }

1928. 规定时间内到达终点的最小花费

一个国家有 n 个城市,城市编号为 0 到 n - 1 ,题目保证 所有城市 都由双向道路 连接在一起 。道路由二维整数数组 edges 表示,其中 edges[i] = [xi, yi, timei] 表示城市 xi 和 yi 之间有一条双向道路,耗费时间为 timei 分钟。两个城市之间可能会有多条耗费时间不同的道路,但是不会有道路两头连接着同一座城市。
每次经过一个城市时,你需要付通行费。通行费用一个长度为 n 且下标从 0 开始的整数数组 passingFees 表示,其中 passingFees[j] 是你经过城市 j 需要支付的费用。
一开始,你在城市 0 ,你想要在 maxTime 分钟以内 (包含 maxTime 分钟)到达城市 n - 1 。旅行的 费用 为你经过的所有城市 通行费之和包括 起点和终点城市的通行费)。
给你 maxTime,edges 和 passingFees ,请你返回完成旅行的 最小费用 ,如果无法在 maxTime 分钟以内完成旅行,请你返回 -1 。

示例 1:
image.png
输入:maxTime = 30, edges = [[0,1,10],[1,2,10],[2,5,10],[0,3,1],[3,4,10],[4,5,15]], passingFees = [5,1,2,20,20,3] 输出:11 解释:最优路径为 0 -> 1 -> 2 -> 5 ,总共需要耗费 30 分钟,需要支付 11 的通行费。
示例 2:
image.png
输入:maxTime = 29, edges = [[0,1,10],[1,2,10],[2,5,10],[0,3,1],[3,4,10],[4,5,15]], passingFees = [5,1,2,20,20,3] 输出:48 解释:最优路径为 0 -> 3 -> 4 -> 5 ,总共需要耗费 26 分钟,需要支付 48 的通行费。 你不能选择路径 0 -> 1 -> 2 -> 5 ,因为这条路径耗费的时间太长。
示例 3:
输入:maxTime = 25, edges = [[0,1,10],[1,2,10],[2,5,10],[0,3,1],[3,4,10],[4,5,15]], passingFees = [5,1,2,20,20,3] 输出:-1 解释:无法在 25 分钟以内从城市 0 到达城市 5 。

提示:

  • 1 <= maxTime <= 1000
  • n == passingFees.length
  • 2 <= n <= 1000
  • n - 1 <= edges.length <= 1000
  • 0 <= xi, yi <= n - 1
  • 1 <= timei <= 1000
  • 1 <= passingFees[j] <= 1000
  • 图中两个节点之间可能有多条路径。
  • 图中不含有自环。

思路:
带限制的最短路问题。从起点到终点所有总时间不超过maxTime的路径中选择一条花费最少的路径。
方法1:DP
状态表示: f[t][i]表示使用恰好 t 分钟到达城市 i 需要的最少通行费总和。
状态转移:
image.png

  1. class Solution {
  2. public int minCost(int maxTime, int[][] edges, int[] passingFees) {
  3. int n = passingFees.length;
  4. int[][] f = new int[maxTime + 1][n]; // f[i][j]表示费时为i到达j的最小开销
  5. for (int i = 0; i <= maxTime; i++)
  6. Arrays.fill(f[i], 0x3f3f3f3f);
  7. for (int i = 0; i <= maxTime; i++)
  8. f[i][0] = passingFees[0]; //f[i][0]表示费时i一直在起点状态的最小开销为passingfees[0]
  9. for (int i = 1; i <= maxTime; i++) {
  10. for (int[] edge : edges) {
  11. int a = edge[0], b = edge[1], w = edge[2];
  12. if (i >= w) {
  13. f[i][b] = Math.min(f[i][b], f[i - w][a] + passingFees[b]);
  14. f[i][a] = Math.min(f[i][a], f[i - w][b] + passingFees[a]);
  15. }
  16. }
  17. }
  18. int res = 0x3f3f3f3f;
  19. for (int i = 0; i <= maxTime; i++)
  20. res = Math.min(res, f[i][n - 1]);
  21. if (res == 0x3f3f3f3f) return -1;
  22. return res;
  23. }
  24. }
  1. 方法2dijkstra<br />类似于787dijkstra<br />从起点到终点所有总时间不超过maxTime的路径中选择一条花费最少的路径。<br />将花费作为优先队列的排序依据。<br />有可能出现这样一种情况:在maxTime的时间要求下能到目的地,但花费超过不考虑到达时间的最小花费。
  1. class Solution {
  2. int N = 2010;
  3. int[] h = new int[N], e = new int[N], ne = new int[N], w = new int[N];
  4. int n, idx;
  5. public int minCost(int maxTime, int[][] edges, int[] passingFees) {
  6. n = passingFees.length;
  7. Arrays.fill(h, -1);
  8. for (int[] edge : edges) {
  9. int a= edge[0], b = edge[1], c = edge[2];
  10. add(a, b, c);
  11. add(b, a, c);
  12. }
  13. int[] time = new int[n];
  14. Arrays.fill(time, 0x3f3f3f3f);
  15. time[0] = 0;
  16. int[] dist = new int[n];
  17. Arrays.fill(dist, 0x3f3f3f3f);
  18. dist[0] = passingFees[0];
  19. // pos, time, fee
  20. PriorityQueue<int[]> q = new PriorityQueue<>((o1, o2) -> o1[2] - o2[2]);
  21. q.offer(new int[]{0, 0, passingFees[0]});
  22. while (!q.isEmpty()) {
  23. int[] p = q.poll();
  24. int cur = p[0], t = p[1], fee = p[2];
  25. for (int i = h[cur]; i != -1; i = ne[i]) {
  26. int j = e[i], cost = passingFees[j], c = w[i];
  27. if (c + t > maxTime) continue;
  28. if (dist[j] > cost + fee) {
  29. dist[j] = cost + fee;
  30. time[j] = c + t;
  31. q.offer(new int[]{j, c + t, cost + fee});
  32. } else if (c + t < time[j]) {
  33. time[j] = c + t;
  34. q.offer(new int[]{j, c + t, cost + fee});
  35. }
  36. }
  37. }
  38. return dist[n - 1] >= 0x3f3f3f3f ? -1 : dist[n - 1];
  39. }
  40. void add(int a, int b, int c) {
  41. e[idx] = b;
  42. w[idx] = c;
  43. ne[idx] = h[a];
  44. h[a] = idx++;
  45. }
  46. }

方法3:做完LCP 35后类似的拆点法
只是时间复杂度太高了,空间复杂度也很高,不如方法2

  1. class Solution {
  2. int N = 2010;
  3. int[] h = new int[N], e = new int[N], ne = new int[N], w = new int[N];
  4. int n, idx;
  5. public int minCost(int maxTime, int[][] edges, int[] passingFees) {
  6. n = passingFees.length;
  7. Arrays.fill(h, -1);
  8. for (int[] edge : edges) {
  9. int a= edge[0], b = edge[1], c = edge[2];
  10. add(a, b, c);
  11. add(b, a, c);
  12. }
  13. int[][] dist = new int[n][maxTime + 1];
  14. for (int i = 1; i < n; i++)
  15. Arrays.fill(dist[i], 0x3f3f3f3f);
  16. Arrays.fill(dist[0], passingFees[0]);
  17. boolean[][] st = new boolean[n][maxTime + 1];
  18. // pos, time, fee
  19. PriorityQueue<int[]> q = new PriorityQueue<>((o1, o2) -> o1[2] - o2[2]);
  20. q.offer(new int[]{0, 0, passingFees[0]});
  21. int ans = -1;
  22. while (!q.isEmpty()) {
  23. int[] p = q.poll();
  24. int cur = p[0], time = p[1], fee = p[2];
  25. if (st[cur][time]) continue;
  26. st[cur][time] = true;
  27. if (cur == n - 1) {
  28. ans = fee;
  29. break;
  30. }
  31. for (int i = h[cur]; i != -1; i = ne[i]) {
  32. int j = e[i], path = w[i];
  33. int t = time + path, cost = fee + passingFees[j];
  34. if (t <= maxTime && dist[j][t] > cost) {
  35. dist[j][t] = cost;
  36. q.offer(new int[]{j, t, cost});
  37. }
  38. }
  39. }
  40. return ans;
  41. }
  42. void add(int a, int b, int c) {
  43. e[idx] = b;
  44. w[idx] = c;
  45. ne[idx] = h[a];
  46. h[a] = idx++;
  47. }
  48. }

LCP 35. 电动车游城市

小明的电动车电量充满时可行驶距离为 cnt,每行驶 1 单位距离消耗 1 单位电量,且花费 1 单位时间。小明想选择电动车作为代步工具。地图上共有 N 个景点,景点编号为 0 ~ N-1。他将地图信息以 [城市 A 编号,城市 B 编号,两城市间距离] 格式整理在在二维数组 paths,表示城市 A、B 间存在双向通路。初始状态,电动车电量为 0。每个城市都设有充电桩,charge[i] 表示第 i 个城市每充 1 单位电量需要花费的单位时间。请返回小明最少需要花费多少单位时间从起点城市 start 抵达终点城市 end。
示例 1:
输入:paths = [[1,3,3],[3,2,1],[2,1,3],[0,1,4],[3,0,5]], cnt = 6, start = 1, end = 0, charge = [2,10,4,1]
输出:43
解释:最佳路线为:1->3->0。
在城市 1 仅充 3 单位电至城市 3,然后在城市 3 充 5 单位电,行驶至城市 5。
充电用时共 310 + 51= 35
行驶用时 3 + 5 = 8,此时总用时最短 43。
image.png
示例 2:
输入:paths = [[0,4,2],[4,3,5],[3,0,5],[0,1,5],[3,2,4],[1,2,8]], cnt = 8, start = 0, end = 2, charge = [4,1,1,3,2]
输出:38
解释:最佳路线为:0->4->3->2。
城市 0 充电 2 单位,行驶至城市 4 充电 8 单位,行驶至城市 3 充电 1 单位,最终行驶至城市 2。
充电用时 42+28+31 = 27
行驶用时 2+5+4 = 11,总用时最短 38。
*提示:

  • 1 <= paths.length <= 200
  • paths[i].length == 3
  • 2 <= charge.length == n <= 100
  • 0 <= path[i][0],path[i][1],start,end < n
  • 1 <= cnt <= 100
  • 1 <= path[i][2] <= cnt
  • 1 <= charge[i] <= 100
  • 题目保证所有城市相互可以到达

思路:拆点 + dijkstra
本题需要考虑的问题是在当前点充不充电,如果充电,充多少电?
我们将(pos, power)二元组视为节点,然后建图,以(start,0)为起点跑一遍Dijkstra即可得到结果。
新图的节点数为图上DP - 图5,边数为图上DP - 图6C为最大电量,M为原来的边数。

  1. class Solution {
  2. int N = 410;
  3. int[] h = new int[N], e = new int[N], ne = new int[N], w = new int[N];
  4. int n, idx;
  5. public int electricCarPlan(int[][] paths, int cnt, int start, int end, int[] charge) {
  6. n = charge.length;
  7. Arrays.fill(h, -1);
  8. for (int[] p : paths) {
  9. int a = p[0], b = p[1], c = p[2];
  10. add(a, b, c);
  11. add(b, a, c);
  12. }
  13. int[][] dist = new int[n][cnt + 1];
  14. for (int i = 0; i < n; i++)
  15. Arrays.fill(dist[i], 0x3f3f3f3f);
  16. dist[start][0] = 0;
  17. boolean[][] st = new boolean[n][cnt + 1];
  18. // pos, power, time
  19. PriorityQueue<int[]> q = new PriorityQueue<>((o1, o2) -> (o1[2] - o2[2]));
  20. q.offer(new int[]{start, 0, 0});
  21. int ans = 0;
  22. while (!q.isEmpty()) {
  23. int[] p = q.poll();
  24. int cur = p[0], power = p[1], time = p[2];
  25. if (st[cur][power]) continue;
  26. st[cur][power] = true;
  27. if (cur == end) {
  28. ans = time;
  29. break;
  30. }
  31. // 充一次电
  32. if (power < cnt && time + charge[cur] < dist[cur][power + 1]) {
  33. dist[cur][power + 1] = time + charge[cur];
  34. q.offer(new int[]{cur, power + 1, time + charge[cur]});
  35. }
  36. //去其它点
  37. for (int i = h[cur]; i != -1; i = ne[i]) {
  38. int j = e[i], path = w[i];
  39. if (power >= path && dist[j][power - path] > time + path) {
  40. dist[j][power - path] = time + path;
  41. q.offer(new int[]{j, power - path, time + path});
  42. }
  43. }
  44. }
  45. return ans;
  46. }
  47. void add(int a, int b, int c) {
  48. e[idx] = b;
  49. w[idx] = c;
  50. ne[idx] = h[a];
  51. h[a] = idx++;
  52. }
  53. }

小美的区域会议

小美是美团总部的高管,她想要召集一些美团的区域负责人来开会,已知美团的业务区域划分可以用一棵树来表示,树上有 n 个节点,每个节点分别代表美团的一个业务区域,每一个区域有一个负责人,这个负责人的级别为 A[i]
已知小美召集人员开会必须满足以下几个条件:
1.小美召集的负责人所在的区域必须构成一个非空的连通的图,即选取树上的一个连通子图。
2.这些负责人中,级别最高的和级别最低的相差不超过 k 。
请问小美有多少种召集负责人的方式,当且仅当选取的集合不同时我们就认为两种方式不同。由于方案数可能非常大,所以请对 10^9+7 取模。

输入:
- 输入第一行包含两个整数 n 和 k ,表示区域的数量,和不能超过的级别。
- 接下来有 n-1 行,每行有两个正整数 a 和 b ,表示 a 号区域和 b 号区域有一条边。
- 最后一行有 n 个整数,第 i 个整数表示 i 号区域负责人的级别。
输出:
- 输出仅包含一个整数,表示可以选择的方案数对 10^9+7 取模之后的结果。
示例:

输入:
5 1
1 2
2 3
3 4
4 5
2 2 2 2 2
输出:15
解释:显然一个区域的方案有 {1},{2},{3},{4},{5},两个区域的方案有 4 个,三个区域的方案有 3 个,四个区域的方案有 2 个,五个区域的方案有 1 个,共 15 个。
提示:
1 <= n, k <= 2000
1 <= a, b <= n

思路:
一个树上DP问题,当然不能直接深度优先一次解决,因为与等级k有关,无法利用等级最大值和最小值进行状态转移
需要暴力DP,考虑每一个节点作为组中等级最小的成员,然后进行树形DP,当有和他等级一致的节点时,只选比它下标大的,这样做防止重复

  1. import java.util.*;
  2. public class Main {
  3. static final int MOD = (int)(1e9 + 7);
  4. public static void main(String[] args) {
  5. Scanner sc = new Scanner(System.in);
  6. int n = sc.nextInt(), k = sc.nextInt();
  7. List<Integer>[] edges = new ArrayList[n];
  8. for (int i = 0; i < n; i++)
  9. edges[i] = new ArrayList<>();
  10. for (int i = 1; i < n; i++) {
  11. int a = sc.nextInt() - 1, b = sc.nextInt() - 1;
  12. edges[a].add(b);
  13. edges[b].add(a);
  14. }
  15. int[] a = new int[n];
  16. for (int i = 0; i < n; i++) {
  17. a[i] = sc.nextInt();
  18. }
  19. int res = 0;
  20. for (int i = 0; i < n; i++)
  21. //第一个i表示当前节点,第二个i表示本次深搜的依据节点,其他节点等级必须大于等于i的等级,如果等级一致则下标必须大于i,最后一个参数表示其从哪个节点转移过来的,防止自环
  22. res = (res + dfs(edges, i, i, a, k, -1)) % MOD;
  23. System.out.println(res);
  24. }
  25. static int dfs(List<Integer>[] edges, int u, int o, int[] a, int k, int fa) {
  26. long cnt = 1;
  27. for (int ne : edges[u]) {
  28. if (fa == ne) continue;
  29. int x = a[ne] - a[o];
  30. if (x == 0 && ne > o || x > 0 && x <= k)
  31. cnt = (cnt * (dfs(edges, ne, o, a, k, u) + 1)) % MOD;
  32. }
  33. return (int)(cnt % MOD);
  34. }
  35. }