题目描述
原题链接: LeetCode.2008 出租车的最大盈利
解题思路
- 按二分查找和动态规划标签搜题库翻到的题目, 相当于揣着俩提示来做题了;
- 总体来说可以从下车地点编号或者前i个行程这两种维度进行DP;
- 思路一:
- 将行程按照下车地点编号由小到大排序;
- dp[i]定义为排序后在[0...i]行程中选择接单能获得的最大盈利值;
- 不接行程i能得到的最大盈利值是dp[i-1], 选择接行程i这单能得到的最大盈利值是 dp[k] + 当前行程可盈利值, 其中k是[0, i-1]范围内满足下车地点不晚于当前行程上车地点的最后一个行程下标;
- 即递推公式为 dp[i] = Math.max(dp[i-1], dp[k] + ride[1] - ride[0] + ride[2]);
- 设行程个数为m, 时间复杂度是排序O(m\(log_2 m\))+DP过程中二分对应的也是O(m\(log_2 m\)), 空间复杂度O(m)。
- 思路二:
- 将行程按照下车地点编号由小到大排序;
- dp[i]定义为到达地点i时能获得的最大盈利值, 但是由于行程即使有序下车地点仍然是离散不连续的, 所以再定义List
idxList记录哪些地点已经处理过会有乘客下车的行程; - 遍历有序行程数据, 记当前行程上车地点为start, 下车地点为end, 可盈利值为profit, 考虑dp[end]有三种情况:
- 前面处理过在start位置下车的行程, 从该行程结束立刻接单, 能得到盈利值是 dp[start] + profit;
- dp[start]为初始值0则表示没有在start位置下车的前置行程, 要在idxList中通过二分查找到小于start的最大值idx中将 dp[idx] + profit 赋值给dp[end], 含义是在送完idx下车的乘客后一直空车到start位置再接当前这单所得到的的盈利值;
- 还有种情况是存在冲突的前置行程而且放弃当前行程就能得到更大盈利值, 这里就要最初就定义最大值变量记录所有已处理行程的最大盈利值。
- 时间复杂度也是O(m\(log_2 m\)), 但是空间复杂度是O(n);
- 思路解释起来比较绕(可能是表达能力有限)但是我个人感觉这种以地点编号维度进行递推是更容易想到的思路。虽然在昨天 LeetCode.1235 规划兼职工作 那题卡在时间不连续没有顺着思路写出正确代码, 但是今天这道类似问题能写出来就算是有进步了, 值得记录一下。
- 思路三:
- 写对数器自行验证上述两种代码都是正确解法后才发现性能并不能谈得上优秀, 翻了示例代码才发现这题并不需要二分;
- 先用数组哈希将行程信息按照下车地点聚合起来, 数组元素是所有在该地点下车的行程信息所组成的一个链表, 这一步也是保证行程信息有序但是基于桶排序直接把排序的时间复杂度优化到O(m);
- 仍然是以地点维度进行DP, dp[i]定义为到达地点i时能获得的最大盈利值;
- 仍然定义变量max记录当前获得的最大盈利值, 从地点编号2开始处理dp数据, 只需要考虑两种情况:
- i位置没有需要乘客下车的行程, 空车到达i位置能获得的最大盈利是前面其它行程带来的, 即dp[i] = max;
- i位置有乘客下车的行程, 遍历链表上所有行程, max = Math.max(max, dp[r.start] + r.profit), 因为是连续遍历更新dp数组, 所以每个dp[start]都是正确的最大值, 简化了思路二中需要在idxList中二分查找的步骤;
- 时间复杂度O(m + n), 空间复杂度也是O(m + n);
- 证明了以地点维度进行DP确实是正确也可以是高效的, 选择哪种维度要看具体问题给出的数据规模;
- 虽然自行写出了前面两种思路的代码, 但还是没能进一步想到最优的第三种思路, 学无止境要继续努力, 只能说学到就是赚到了, 这题值得记录。
解题代码
-
排序后按照前i个行程进行DP:
/** * 以行程个数为维度的DP+二分法, 但是性能不是最优的 * 执行用时: 72 ms , 在所有 Java 提交中击败了 50.25% 的用户 * 内存消耗: 70.94 MB , 在所有 Java 提交中击败了 7.39% 的用户 */ public long maxTaxiEarnings(int n, int[][] rides) { if (rides.length == 1) { return profit(rides[0]); } int cnt = rides.length; Arrays.sort(rides, (r1, r2) -> r1[1] - r2[1]); // dp[i]表示在[0..i]行程中选择接单能盈利的最大值 long[] dp = new long[cnt]; dp[0] = profit(rides[0]); for (int i = 1; i < cnt; i++) { int[] ride = rides[i]; int start = ride[0]; // 二分法找到满足rides[idx][1]<=start的最大idx值 int left = 0, right = i - 1, idx = -1; while (left <= right) { int mid = left + ((right - left) >> 1); if (rides[mid][1] > start) { right = mid - 1; } else { idx = mid; left = mid + 1; } } dp[i] = Math.max(dp[i - 1], (idx == -1 ? 0 : dp[idx]) + profit(ride)); } return dp[cnt - 1]; } private int profit(int[] ride) { return ride[1] - ride[0] + ride[2]; }
-
排序后按照下车地点进行DP:
/** * 以下车地点编号为维度的DP+二分, 性能不算好 * 执行用时: 89 ms , 在所有 Java 提交中击败了 16.26% 的用户 * 内存消耗: 63.29 MB , 在所有 Java 提交中击败了 87.69% 的用户 */ public long maxTaxiEarnings(int n, int[][] rides) { if (rides.length == 1) { return rides[0][1] - rides[0][0] + rides[0][2]; } long[] dp = new long[n + 1]; Arrays.sort(rides, (r1, r2) -> r1[1] - r2[1]); List<Integer> idxList = new ArrayList<>(); int preEnd = 0; long max = 0; for (int[] ride : rides) { int end = ride[1], start = ride[0]; /* * 接这单分为两种情况 * 1、前面已经计算过以当前start为end的最大盈利值, 接单后的盈利值直接累加 * 2、否则要找到距离当前start最近且已经计算过最大盈利的一个下车地点编号 */ int idx = dp[start] > 0 ? start : binarySearchIdx(idxList, start); dp[end] = Math.max(dp[end], dp[idx] + end - start + ride[2]); if (max > dp[end]) {// 存在某个与当前行程冲突但是下车位置能获得更大盈利的情况, 即不接这单能获得更大盈利 dp[end] = max; } else { max = dp[end]; } if (end != preEnd) { idxList.add(end); preEnd = end; } } return max; } /** * 在集合中二分查找不超过start的最大编号 */ private int binarySearchIdx(List<Integer> list, int start) { int left = 0, right = list.size() - 1, ans = 0; while (left <= right) { int mid = left + ((right - left) >> 1); if (list.get(mid) > start) { right = mid - 1; } else { ans = list.get(mid); left = mid + 1; } } return ans; }
-
哈希/桶排序后按照下车地点进行DP:
/** * 从时间最优示例代码中看到的另一种基于哈希表的DP解法 * 执行用时: 8 ms , 在所有 Java 提交中击败了 100.00% 的用户 * 内存消耗: 65.93 MB , 在所有 Java 提交中击败了 67.49% 的用户 */ public long maxTaxiEarnings3(int n, int[][] rides) { if (rides.length == 1) { return rides[0][1] - rides[0][0] + rides[0][2]; } // arr[i]是所有在i处下车的行程链表信息 Ride[] arr = new Ride[n + 1]; for (int[] r : rides) { int end = r[1]; arr[end] = new Ride(r[0], end - r[0] + r[2], arr[end]); } long max = 0; long[] dp = new long[n + 1]; for (int i = 2; i <= n; i++) { for (Ride r = arr[i]; r != null; r = r.next) { max = Math.max(dp[r.start] + r.profit, max); } dp[i] = max; } return max; } private class Ride { int start; int profit; Ride next; public Ride(int start, int profit, Ride next) { this.start = start; this.profit = profit; this.next = next; } }