|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[1537. 最大得分](https://leetcode.cn/problems/get-the-maximum-score/solution/by-ac_oier-ht78/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「前缀和」、「构造」、「双指针」、「序列 DP」、「动态规划」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +你有两个 有序 且数组内元素互不相同的数组 `nums1` 和 `nums2` 。 |
| 10 | + |
| 11 | +一条 合法路径 定义如下: |
| 12 | + |
| 13 | +* 选择数组 `nums1` 或者 `nums2` 开始遍历(从下标 $0$ 处开始)。 |
| 14 | +* 从左到右遍历当前数组。 |
| 15 | +* 如果你遇到了 `nums1` 和 `nums2` 中都存在的值,那么你可以切换路径到另一个数组对应数字处继续遍历(但在合法路径中重复数字只会被统计一次)。 |
| 16 | + |
| 17 | +得分定义为合法路径中不同数字的和。 |
| 18 | + |
| 19 | +请你返回所有可能合法路径中的最大得分。 |
| 20 | + |
| 21 | +由于答案可能很大,请你将它对 $10^9 + 7$ 取余后返回。 |
| 22 | + |
| 23 | +示例 1: |
| 24 | + |
| 25 | +``` |
| 26 | +输入:nums1 = [2,4,5,8,10], nums2 = [4,6,8,9] |
| 27 | +
|
| 28 | +输出:30 |
| 29 | +
|
| 30 | +解释:合法路径包括: |
| 31 | +[2,4,5,8,10], [2,4,5,8,9], [2,4,6,8,9], [2,4,6,8,10],(从 nums1 开始遍历) |
| 32 | +[4,6,8,9], [4,5,8,10], [4,5,8,9], [4,6,8,10] (从 nums2 开始遍历) |
| 33 | +最大得分为上图中的绿色路径 [2,4,6,8,10] 。 |
| 34 | +``` |
| 35 | +示例 2: |
| 36 | +``` |
| 37 | +输入:nums1 = [1,3,5,7,9], nums2 = [3,5,100] |
| 38 | +
|
| 39 | +输出:109 |
| 40 | +
|
| 41 | +解释:最大得分由路径 [1,3,5,100] 得到。 |
| 42 | +``` |
| 43 | +示例 3: |
| 44 | +``` |
| 45 | +输入:nums1 = [1,2,3,4,5], nums2 = [6,7,8,9,10] |
| 46 | +
|
| 47 | +输出:40 |
| 48 | +
|
| 49 | +解释:nums1 和 nums2 之间无相同数字。 |
| 50 | +最大得分由路径 [6,7,8,9,10] 得到。 |
| 51 | +``` |
| 52 | +示例 4: |
| 53 | +``` |
| 54 | +输入:nums1 = [1,4,5,8,9,11,19], nums2 = [2,3,4,11,12] |
| 55 | +
|
| 56 | +输出:61 |
| 57 | +``` |
| 58 | + |
| 59 | +提示: |
| 60 | +* $1 <= nums1.length <= 10^5$ |
| 61 | +* $1 <= nums2.length <= 10^5$ |
| 62 | +* $1 <= nums1[i], nums2[i] <= 10^7$ |
| 63 | +* `nums1` 和 `nums2` 都是严格递增的数组。 |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +### 前缀和 + 构造(分段计算) |
| 68 | + |
| 69 | +一个简单且正确的做法,是我们构造一种决策方案,使得能够直接计算出最大得分。 |
| 70 | + |
| 71 | +首先,在最佳路径中所有的公共点都必然会经过,因此我们可以将值相等的点进行合并,即看作同一个点。 |
| 72 | + |
| 73 | +利用两个数组均满足「单调递增」,我们可以通过 $O(n + m)$ 的复杂度统计出那些公共点,以二元组 $(i, j)$ 的形式存储到 `list` 数组(二元组含义为 $nums1[i] = nums2[j]$)。 |
| 74 | + |
| 75 | +对于 `list` 中的每对相邻元素(相邻公共点),假设为 $(a_i, b_i)$ 和 $(c_i, d_i)$,我们可以通过「前缀和」计算出 $nums1[a_i ... c_i]$ 以及 $nums2[b_i ... d_i]$ 的和,从而决策出在 $nums1[a_i]$(或者说是 $nums2[b_i]$,这两个是同一个点)时,我们应当走哪一段。 |
| 76 | + |
| 77 | +当计算完所有公共点之间的得分后,对于最佳路线的首位两端,也是结合「前缀和」做同样的逻辑处理即可。 |
| 78 | + |
| 79 | +代码: |
| 80 | +```Java |
| 81 | +class Solution { |
| 82 | + int MOD = (int)1e9 + 7; |
| 83 | + public int maxSum(int[] nums1, int[] nums2) { |
| 84 | + int n = nums1.length, m = nums2.length; |
| 85 | + long[] s1 = new long[n + 10], s2 = new long[m + 10]; |
| 86 | + for (int i = 1; i <= n; i++) s1[i] = s1[i - 1] + nums1[i - 1]; |
| 87 | + for (int i = 1; i <= m; i++) s2[i] = s2[i - 1] + nums2[i - 1]; |
| 88 | + List<int[]> list = new ArrayList<>(); |
| 89 | + for (int i = 0, j = 0; i < n && j < m; ) { |
| 90 | + if (nums1[i] == nums2[j]) list.add(new int[]{i, j}); |
| 91 | + if (nums1[i] < nums2[j]) i++; |
| 92 | + else j++; |
| 93 | + } |
| 94 | + long ans = 0; |
| 95 | + for (int i = 0, p1 = -1, p2 = -1; i <= list.size(); i++) { |
| 96 | + int idx1 = 0, idx2 = 0; |
| 97 | + if (i < list.size()) { |
| 98 | + int[] info = list.get(i); |
| 99 | + idx1 = info[0]; idx2 = info[1]; |
| 100 | + } else { |
| 101 | + idx1 = n - 1; idx2 = m - 1; |
| 102 | + } |
| 103 | + long t1 = s1[idx1 + 1] - s1[p1 + 1], t2 = s2[idx2 + 1] - s2[p2 + 1]; |
| 104 | + ans += Math.max(t1, t2); |
| 105 | + p1 = idx1; p2 = idx2; |
| 106 | + } |
| 107 | + return (int)(ans % MOD); |
| 108 | + } |
| 109 | +} |
| 110 | +``` |
| 111 | +* 时间复杂度:$O(n + m)$ |
| 112 | +* 空间复杂度:$O(n + m)$ |
| 113 | + |
| 114 | +--- |
| 115 | + |
| 116 | +### 序列 DP |
| 117 | + |
| 118 | +另外一个较为常见的做法是「序列 DP」做法。 |
| 119 | + |
| 120 | +定义 $f[i]$ 代表在 `nums1` 上进行移动,到达 $nums1[i]$ 的最大得分;定义 $g[j]$ 代表在 `nums2` 上进行移动,到达 $nums[j]$ 的最大得分。 |
| 121 | + |
| 122 | +由于两者的分析是类似的,我们以 $f[i]$ 为例进行分析即可。 |
| 123 | + |
| 124 | +不失一般性考虑 $f[i]$ 如何转移,假设当前处理到的是 $nums1[i]$,根据 $nums1[i]$ 是否为公共点,进行分情况讨论: |
| 125 | + |
| 126 | +* $nums1[i]$ 不为公共点,此时只能由 $nums[i - 1]$ 转移而来,即有 $f[i] = f[i - 1] + nums[i]$; |
| 127 | +* $nums1[i]$ 为公共点(假设与 $nums2[j]$ 公共),此时能够从 $nums1[i - 1]$ 或 $nums2[j - 1]$ 转移而来,我们需要取 $f[i - 1]$ 和 $g[j - 1]$ 的最大值,即有 $f[i] = g[j] = \max(f[i - 1], g[j - 1]) + nums1[i]$。 |
| 128 | + |
| 129 | +更重要的是,我们需要确保计算 $f[i]$ 时,$g[j - 1]$ 已被计算完成。 |
| 130 | + |
| 131 | +由于最佳路线必然满足「单调递增」,因此我们可以使用「双指针」来对 $f[i]$ 和 $g[j]$ 同时进行转移,每次取值小的进行更新,从而确保更新过程也是单调的,即当需要计算 $f[i]$ 时,比 $nums1[i]$ 小的 $f[X]$ 和 $g[X]$ 均被转移完成。 |
| 132 | + |
| 133 | +代码: |
| 134 | +```Java |
| 135 | +class Solution { |
| 136 | + int MOD = (int)1e9 + 7; |
| 137 | + public int maxSum(int[] nums1, int[] nums2) { |
| 138 | + int n = nums1.length, m = nums2.length; |
| 139 | + long[] f = new long[n + 1], g = new long[m + 1]; |
| 140 | + int i = 1, j = 1; |
| 141 | + while (i <= n || j <= m) { |
| 142 | + if (i <= n && j <= m) { |
| 143 | + if (nums1[i - 1] < nums2[j - 1]) { |
| 144 | + f[i] = f[i - 1] + nums1[i - 1]; |
| 145 | + i++; |
| 146 | + } else if (nums2[j - 1] < nums1[i - 1]) { |
| 147 | + g[j] = g[j - 1] + nums2[j - 1]; |
| 148 | + j++; |
| 149 | + } else { |
| 150 | + f[i] = g[j] = Math.max(f[i - 1], g[j - 1]) + nums1[i - 1]; |
| 151 | + i++; j++; |
| 152 | + } |
| 153 | + } else if (i <= n) { |
| 154 | + f[i] = f[i - 1] + nums1[i - 1]; |
| 155 | + i++; |
| 156 | + } else { |
| 157 | + g[j] = g[j - 1] + nums2[j - 1]; |
| 158 | + j++; |
| 159 | + } |
| 160 | + } |
| 161 | + return (int) (Math.max(f[n], g[m]) % MOD); |
| 162 | + } |
| 163 | +} |
| 164 | +``` |
| 165 | +* 时间复杂度:$O(n + m)$ |
| 166 | +* 空间复杂度:$O(n + m)$ |
| 167 | + |
| 168 | +--- |
| 169 | + |
| 170 | +### 最后 |
| 171 | + |
| 172 | +这是我们「刷穿 LeetCode」系列文章的第 `No.1537` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 173 | + |
| 174 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 175 | + |
| 176 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 177 | + |
| 178 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 179 | + |
0 commit comments