可被三(五、七)整除的最大和
题目描述
给出一个整数数组 nums, 找到其中可以被3 整除的最大子序列和。
输入:nums = [3,6,5,1,8] 输出:18 解释:选出数字 3, 6, 1 和 8,它们的和是 18(可被 3 整除的最大和)。
这个题目经常变,就是变成能被 6 整除,能被 7 整除这些。
流程分析
这乍一看是个0 1 背包问题,每个问题有选和不选两种可能性, 不过在01背包里面选了的话是影响了背包的容量。在这个题呢,选择了某个数之后,一个是影响已经选了的总和,另一个呢就是会影响这个和对3的余数(这个感觉还是很难想的,不过做过了就知道了),在本题呢,就选择余数作为 dp 的第二维度。因此dp 含义为:
dp[i][j]:[0…i) 个数,任意组合,余数为 j 的情况下,可得到的最大和。
主要的问题点呢,在于当选择了这个数后,余数这一维的状态是如何转移的呢,可以先试着暴力写下:
public static int maxSumDivThree(int[] nums) {
int n = nums.length;
int[][] dp = new int[nums.length + 1][3];
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
dp[0][2] = Integer.MIN_VALUE;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 3; j++) {
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 if (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];
}
for 循环其中的,又可以简化成:
public static int maxSumDivThree(int[] nums) {
int n = nums.length;
int[][] dp = new int[nums.length + 1][3];
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
dp[0][2] = Integer.MIN_VALUE;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 3; j++) {
int x = nums[i-1] % 3;
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][(3 - x + 0) % 3] + nums[i - 1]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][(3 - x + 1) % 3] + nums[i - 1]);
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][(3 - x + 2) % 3] + nums[i - 1]);
}
}
return dp[n][0];
}
又继续简化代码呢:
public static int maxSumDivThree(int[] nums) {
int n = nums.length;
int[][] dp = new int[nums.length + 1][3];
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
dp[0][2] = Integer.MIN_VALUE;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 3; j++) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][(j + 3 - nums[i-1] % 3) % 3] + nums[i - 1]);
}
}
return dp[n][0];
}
最后附上完整的代码,其中包含了,对数器,及dfs 的暴力解法:、
package leetcode;
import java.util.Collections;
import java.util.Map;
import util.Util;
public class BiggestThree {
public static void main(String[] args) {
int testTime = 150000;
int maxVal = (int)1e5;
int maxLen = 15;
System.out.println("测试开始");
for (int i = 0; i < testTime; i++) {
baolires = Integer.MIN_VALUE;
// 生成随机数组
int[] nums = Util.geneArr(maxVal, maxLen);
dfs(nums, nums.length - 1, 0);
int dpres = easyMaxSum(nums);
if (baolires != dpres) {
Util.print1DArray(nums);
System.out.println("baoli: " + baolires);
System.out.println("dp: " + dpres);
System.out.println("出错了");
}
}
System.out.println("测试结束");
}
static long baolires = Integer.MIN_VALUE;
// 当前来到看 i 位置,i 及其 之前自由组合所能达到的最大和
public static void dfs(int[] nums, int i, int path) {
if (i == -1) {
if (path % 3 == 0)
baolires = Math.max(baolires, path);
} else {
// 选当前数
dfs(nums, i - 1, path);
// 不选当前数
dfs(nums, i - 1, path + nums[i]);
}
}
public static int easyMaxSum(int[] nums) {
int n = nums.length;
long[][] dp = new long[n + 1][3];
// for (int i = 0; i < n + 1; i++) {
// for (int j = 0; j < 3; j++) {
// dp[i][j] = Integer.MIN_VALUE;
// }
// }
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
dp[0][2] = $$;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 3; j++) {
// 如果有负数的话
long select = dp[i - 1][(j + 3 - nums[i - 1] % 3) % 3] + nums[i - 1];
long notSelect = dp[i-1][j];
dp[i][j] = Math.max(select, notSelect);
}
}
return (int)dp[n][0];
}
public static int maxSumDivThree(int[] nums) {
int n = nums.length;
int[][] dp = new int[nums.length + 1][3];
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
dp[0][2] = Integer.MIN_VALUE;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 3; j++) {
/*
int x = nums[i-1] % 3;
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][(3 - x + 0) % 3] + nums[i - 1]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][(3 - x + 1) % 3] + nums[i - 1]);
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][(3 - x + 2) % 3] + nums[i - 1]);
*/
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 if (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];
}
}
其中有哪些需要注意的点呢?
- 可以看到其中的 dp 数组初始化为 long ,但是最小值的话赋值为 I n t e g e r . M I N _ V A L U E Integer.MIN\_VALUE Integer.MIN_VALUE, 是为了防止数组中可能有负数的话,如果 dp 数组还是int 类型的话,那么 I n t e g e r . M I N _ V A L U E Integer.MIN\_VALUE Integer.MIN_VALUE 加上一个负数,就变成正数,溢出了。