LC 689. 三个无重叠子数组的最大和
题目描述
这是 LeetCode 上的 689. 三个无重叠子数组的最大和 ,难度为 困难。
给你一个整数数组 nums 和一个整数 k ,找出三个长度为 k 、互不重叠、且 3 * k 项的和最大的子数组,并返回这三个子数组。
以下标的数组形式返回结果,数组中的每一项分别指示每个子数组的起始位置(下标从 $0$ 开始)。
如果有多个结果,返回字典序最小的一个。
示例 1:1
2
3
4
5
6输入:nums = [1,2,1,2,6,7,5,1], k = 2
输出:[0,3,5]
解释:子数组 [1, 2], [2, 6], [7, 5] 对应的起始下标为 [0, 3, 5]。
也可以取 [2, 1], 但是结果 [1, 3, 5] 在字典序上更大。
示例 2:1
2
3输入:nums = [1,2,1,2,1,2,1,2,1], k = 2
输出:[0,2,4]
提示:
- $1 <= nums.length <= 2 \times 10^4$
 - $1 <= nums[i] < 2^{16}$
 - $1 <= k <= floor(nums.length / 3)$
 
前缀和 + 序列 DP
若不考虑输出方案,仅是求「三个无重叠子数组的最大和」的最大值。
只需要使用动态规划求解即可:定义 $f[i][j]$ 为考虑前 $i$ 个数,凑成无重叠子数组数量为 $j$ 个时的最大值。
最终答案为 $f[n - 1][3]$。
不失一般性的考虑 $f[i][j]$ 该如何计算(以最优方案是否包含 $nums[i]$ 进行讨论):
- 最优方案中包含 $num[i]$:由于这 $j$ 个无重叠,因此前面的 $j - 1$ 个子数组不能覆盖 $[i - k + 1, i]$。即只能在 $[0, i - k]$ 中选 $j - 1$ 个子数组。此时有:
 
其中求解 $\sum_{idx = i - k + 1}^{i} nums[idx]$ 部分可以使用「前缀和」优化
- 最优方案不包含 $num[i]$:当明确了 $nums[i]$ 对最优方案无贡献,此时问题等价于考虑前 $i - 1$ 个数,凑成 $j$ 个不重叠子数组的最大值。此时有:
 
最终 $f[i][j]$ 为上述两种方案中的最大值。
然后考虑「如何回溯出字典序最小的具体方案」,常规的回溯具体方案的做法是,从最终答案 $f[n - 1][3]$ 开始往回追溯。
利用 $f[n - 1][3]$ 仅由两个可能的节点($f[n - 2][3]$ 和 $f[n - 1 - k][2]$)转移而来,通过判断 $f[n - 1][3]$ 等于 $f[n - 2][3]$ 还是 $f[n - 1 - k][2] + \sum_{idx = n - k }^{n - 1} nums[idx]$ 来决定回溯点为何值。
但该做法只能确保回溯出字典序最大的方案是正确(当两个可能的前驱节点都能转移到 $f[i][j]$ 时优先采纳靠后的位置),而我们需要回溯出字典序最小的方案。
在上述解法的基础上,有两种「求解字典序最小具体方案」的做法:
将正序 DP 调整为反序 DP。修改状态定义为 $f[i][j]$ 为考虑 $[i, n - 1]$ 中进行选择,凑出无重叠子数组数量为 $j$ 个时的最大值(最终答案为 $f[0][3]$)。转移过程分析同理,然后从下标 $idx = 0$ 开始进行回溯,优先采纳 $idx$ 小的方案即可;
仍然采取正序 DP 的做法,但对原数组进行翻转,从而将回溯「字典序最大」转换为「字典序最小」具体方案。
一些细节:为了避免对边界的处理,我们令动规数组 $f$ 和前缀和数组 $sum$ 的下标从 $1$ 开始。
Java 代码(反序 DP 做法):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Solution {
    public int[] maxSumOfThreeSubarrays(int[] nums, int k) {
        int n = nums.length;
        long[] sum = new long[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        long[][] f = new long[n + 10][4];
        for (int i = n - k + 1; i >= 1; i--) {
            for (int j = 1; j < 4; j++) {
                f[i][j] = Math.max(f[i + 1][j], f[i + k][j - 1] + sum[i + k - 1] - sum[i - 1]);
            }
        }
        int[] ans = new int[3];
        int i = 1, j = 3, idx = 0;
        while (j > 0) {
            if (f[i + 1][j] > f[i + k][j - 1] + sum[i + k - 1] - sum[i - 1]) {
                i++;
            } else {
                ans[idx++] = i - 1;
                i += k; j--;
            }
        }
        return ans;
    }
}
C++ 代码(反序 DP 做法):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class Solution {
public:
    vector<int> maxSumOfThreeSubarrays(vector<int>& nums, int k) {
        int n = nums.size();
        vector<long long> sumv(n + 1, 0);
        for (int i = 1; i <= n; i++) sumv[i] = sumv[i - 1] + nums[i - 1];
        vector<vector<long long>> f(n + 10, vector<long long>(4, 0));
        for (int i = n - k + 1; i >= 1; i--) {
            for (int j = 1; j < 4; j++) {
                f[i][j] = max(f[i + 1][j], f[i + k][j - 1] + sumv[i + k - 1] - sumv[i - 1]);
            }
        }
        vector<int> ans(3);
        int i = 1, j = 3, idx = 0;
        while (j > 0) {
            if (f[i + 1][j] > f[i + k][j - 1] + sumv[i + k - 1] - sumv[i - 1]) {
                i++;
            } else {
                ans[idx++] = i - 1;
                i += k; j--;
            }
        }
        return ans;
    }
};
Python 代码(反序 DP 做法):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Solution:
    def maxSumOfThreeSubarrays(self, nums: List[int], k: int) -> List[int]:
        n = len(nums)
        sumv = [0] * (n + 1)
        for i in range(1, n + 1):
            sumv[i] = sumv[i - 1] + nums[i - 1]
        f = [[0] * 4 for _ in range(n + 10)]
        for i in range(n - k + 1, 0, -1):
            for j in range(1, 4):
                f[i][j] = max(f[i + 1][j], f[i + k][j - 1] + sumv[i + k -1] - sumv[i - 1])
        ans = [0] * 3
        i, j, idx = 1, 3, 0
        while j > 0:
            if f[i + 1][j] > f[i + k][j - 1] + sumv[i + k -1] - sumv[i - 1]:
                i += 1
            else:
                ans[idx] = i - 1
                idx += 1
                i += k
                j -= 1
        return ans
TypeScript 代码(反序 DP 做法):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function maxSumOfThreeSubarrays(nums: number[], k: number): number[] {
    const n = nums.length;
    const sumv = new Array(n + 1).fill(0);
    for (let i = 1; i <= n; i++) sumv[i] = sumv[i - 1] + nums[i - 1];
    const f = new Array(n + 10).fill(0).map(() => new Array(4).fill(0));
    for (let i = n - k + 1; i >= 1; i--) {
        for (let j = 1; j < 4; j++) {
            f[i][j] = Math.max(f[i + 1][j], f[i + k][j - 1] + sumv[i + k - 1] - sumv[i - 1]);
        }
    }
    const ans = new Array(3).fill(0);
    let i = 1, j = 3, idx = 0;
    while (j > 0) {
        if (f[i + 1][j] > f[i + k][j - 1] + sumv[i + k - 1] - sumv[i - 1]) {
            i++;
        } else {
            ans[idx++] = i - 1;
            i += k; j--;
        }
    }
    return ans;
};
Java 代码(翻转数组做法):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34class Solution {
    public int[] maxSumOfThreeSubarrays(int[] nums, int k) {
        int n = nums.length;
        reverse(nums);
        long[] sum = new long[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        long[][] f = new long[n + 10][4];
        for (int i = k; i <= n; i++) {
            for (int j = 1; j < 4; j++) {
                f[i][j] = Math.max(f[i - 1][j], f[i - k][j - 1] + sum[i] - sum[i - k]);
            }
        }
        int[] ans = new int[3];
        int i = n, j = 3, idx = 0;
        while (j > 0) {
            if (f[i - 1][j] > f[i - k][j - 1] + sum[i] - sum[i - k]) {
                i--;
            } else {
                ans[idx++] = n - i;
                i -= k; j--;
            }
        }
        return ans;
    }
    void reverse(int[] nums) {
        int n = nums.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int c = nums[l];
            nums[l++] = nums[r];
            nums[r--] = c;
        }
    }
}
C++ 代码(翻转数组做法):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26class Solution {
public:
    vector<int> maxSumOfThreeSubarrays(vector<int>& nums, int k) {
        reverse(nums.begin(), nums.end());
        int n = nums.size();
        vector<long long> sumv(n + 1, 0);
        for (int i = 1; i <= n; i++) sumv[i] = sumv[i - 1] + nums[i - 1];
        vector<vector<long long>> f(n + 2, vector<long long>(4, 0));
        for (int i = k; i <= n; i++) {
            for (int j = 1; j < 4; j++) {
                f[i][j] = max(f[i - 1][j], f[i - k][j - 1] + sumv[i] - sumv[i - k]);
            }
        }
        vector<int> ans(3);
        int i = n, j = 3, idx = 0;
        while (j > 0) {
            if (f[i - 1][j] > f[i - k][j - 1] + sumv[i] - sumv[i - k]) {
                i--;
            } else {
                ans[idx++] = n - i;
                i -= k; j--;
            }
        }
        return ans;
    }
};
Python 代码(翻转数组做法):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Solution:
    def maxSumOfThreeSubarrays(self, nums: List[int], k: int) -> List[int]:
        nums.reverse()
        n = len(nums)
        sumv = [0] * (n + 1)
        for i in range(1, n + 1):
            sumv[i] = sumv[i-1] + nums[i-1]
        f = [[0] * 4 for _ in range(n + 2)]
        for i in range(k, n + 1):
            for j in range(1, 4):
                f[i][j] = max(f[i - 1][j], f[i - k][j - 1] + sumv[i] - sumv[i - k]);
        ans = [0] * 3
        i, j, idx = n, 3, 0
        while j > 0:
            if f[i - 1][j] > f[i - k][j - 1] + sumv[i] - sumv[i - k]:
                i -= 1
            else:
                ans[idx] = n - i 
                idx += 1
                i -= k
                j -= 1
        return ans
TypeScript 代码(翻转数组做法):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function maxSumOfThreeSubarrays(nums: number[], k: number): number[] {
    nums.reverse();
    const n = nums.length;
    const sumv = new Array(n + 1).fill(0);
    for (let i = 1; i <= n; i++) sumv[i] = sumv[i - 1] + nums[i - 1];
    const f = Array.from({length: n + 2}, () => new Array(4).fill(0));
    for (let i = k; i <= n; i++) {
        for (let j = 1; j < 4; j++) {
                f[i][j] = Math.max(f[i - 1][j], f[i - k][j - 1] + sumv[i] - sumv[i - k]);
        }
    }
    const ans = new Array(3).fill(0);
    let i = n, j = 3, idx = 0;
    while (j > 0) {
        if (f[i - 1][j] > f[i - k][j - 1] + sumv[i] - sumv[i - k]) {
            i--;
        } else {
            ans[idx++] = n - i;
            i -= k;
            j--;
        }
    }
    return ans;
};
- 时间复杂度:$O(n)$
 - 空间复杂度:$O(n)$
 
最后
这是我们「刷穿 LeetCode」系列文章的第 No.689 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!