1262. 可被三整除的最大和
难度中等229
给你一个整数数组 nums
,请你找出并返回能被三整除的元素最大和。
示例 1:
输入:nums = [3,6,5,1,8]
输出:18
解释:选出数字 3, 6, 1 和 8,它们的和是 18(可被 3 整除的最大和)。
示例 2:
输入:nums = [4]
输出:0
解释:4 不能被 3 整除,所以无法选出数字,返回 0。
示例 3:
输入:nums = [1,2,3,4,4]
输出:12
解释:选出数字 1, 3, 4 以及 4,它们的和是 12(可被 3 整除的最大和)。
提示:
1 <= nums.length <= 4 * 10^4
1 <= nums[i] <= 10^4
状态机DP
https://leetcode.cn/problems/greatest-sum-divisible-by-three/solution/dong-tai-gui-hua-yu-zhuang-tai-zhuan-yi-by-christm/
1、状态定义
dp[i][0]
表示nums[0...i]
模三余零的最大和dp[i][1]
表示nums[0...i]
模三余一的最大和dp[i][2]
表示nums[0...i]
模三余二的最大和零状态
:当前数字最大和模三余零一状态
:当前数字最大和模三余一二状态
:当前数字最大和模三余二
2、状态转移
对于任意一种状态,下一步我们都有两种选择,一是选择当前元素,二是不选择当前元素
dp[i][*] = max{dp[i-1][*],dp[i-1][*] + nums[i]} (* 取值为 0,1,2)
以上是常见的动态规划的递推结构
本题的状态转移显而易见,以当前状态是零状态
为例。我们可以想到,前一个状态无非是零状态、一状态、二状态
,三种情况,针对这三种情况我们分类讨论即可
class Solution {
public int maxSumDivThree(int[] nums) {
int n = nums.length;
int[][] dp = new int[n+1][3];
// 初值,不存在余1和余2的情况
dp[0][0] = 0; dp[0][1] = Integer.MIN_VALUE; dp[0][2] = Integer.MIN_VALUE;
for(int i = 1; i <= n; i++){
if(nums[i-1] % 3 == 0){
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][0] + nums[i-1]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][1] + nums[i-1]);
dp[i][2] = Math.max(dp[i-1][2], dp[i-1][2] + nums[i-1]);
}else if(nums[i-1] % 3 == 1){
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][2] + nums[i-1]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + nums[i-1]);
dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1] + nums[i-1]);
}else{ // nums[i-1] % 3 == 2
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + nums[i-1]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][2] + nums[i-1]);
dp[i][2] = Math.max(dp[i-1][2], dp[i-1][0] + nums[i-1]);
}
}
return dp[n][0]; // 返回模三余0 的值
}
}
21.4.5 网易互联网C++笔试改版成 可被6整除的最大和
22.3.26 美团改成7了
动态规划(更一般性的状态机DP)
https://leetcode.cn/problems/greatest-sum-divisible-by-three/solution/liang-chong-suan-fa-tan-xin-dong-tai-gui-tsll/
讨论:
用【选或不选】的思路,考虑最后一个数 x = nums[n-1]
如果 x mod 3 = 0,那么 x 一定要选,问题变成从 nums[0] 到 nums[n-2] 中寻找能被 3 整除的元素最大和。
如果 x mod 3 = 1:
如果不选 x,和上面一样,问题变成从 nums[0] 到 nums[n-2] 中寻找能被 3 整除的元素最大和 s0。
如果选 x,问题变成从 nums[0] 到 nums[n-2] 中寻找最大元素和s2 满足 s2 mod 3 = 2,答案为 max(so,s2 +x)。
如果 x mod 3 = 2:
如果不选 x,和上面一样,问题变成从 nums[0] 到 nums[n-2] 中寻找能被 3 整除的元素最大和 s0。
如果选 x,问题变成从 nums[0] 到 nums[n-2] 中寻找最大元素和s1 满足 s1 mod 3 = 1,答案为 max(so,s1 +x)。
---------------------------------------------------
上述讨论,刻画了这道题的两个重要参数
i: 表示从 nums[0]到 nums[i] 中选数
j: 表示所选数字之和 s 需要满足 s mod 3 = j。
那么原问题就是 (i = n-1,j = 0),上述讨论得到的子问题有 (i= n-2,j=0),(i = n - 2,j = 1),(i = n - 2,j = 2)。
注: 为什么要从最后一个数开始讨论? 主要是为了方便后面把记忆化搜索改成递推。当然,你从第一个数开始讨论也是可以的。
记忆化搜索
class Solution:
def maxSumDivThree(self, nums: List[int]) -> int:
# 定义 dfs(i,j) 表示从 nums[0] 到 nums[i] 中选数, 所选数字之和 s 满足 s mod 3 =j的前提下, s的最大值
@cache
def dfs(i, j: int) -> int:
if i < 0: return -inf if j else 0
return max(dfs(i-1, j), dfs(i-1, (j + nums[i]) % 3) + nums[i])
return dfs(len(nums)-1, 0)
转成递推
class Solution {
public int maxSumDivThree(int[] nums) {
int n = nums.length;
int[][] f = new int[n+1][3];
f[0][0] = 0;
f[0][1] = Integer.MIN_VALUE;
f[0][2] = Integer.MIN_VALUE;
for(int i = 0; i < n; i++){
for(int j = 0; j < 3; j++)
f[i+1][j] = Math.max(f[i][j], f[i][(j+nums[i]) % 3] + nums[i]);
}
return f[n][0];
}
}
空间优化:(用滚动数组优化空间)
由于 f[i + 1]
只依赖 f
,那么 f[i - 1]
及其之前的数据就没用了
class Solution {
public int maxSumDivThree(int[] nums) {
int n = nums.length;
int[] f = new int[3];
f[0] = 0;
f[1] = Integer.MIN_VALUE;
f[2] = Integer.MIN_VALUE;
for(int i = 0; i < n; i++){
int[] tmp = Arrays.copyOf(f, 3);
for(int j = 0; j < 3; j++)
f[j] = Math.max(tmp[j], tmp[(j+nums[i]) % 3] + nums[i]);
tmp = f;
}
return f[0];
}
}