携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第22天,可被3整除的最大和[贪心 || 动态规划] - 掘金 (juejin.cn)
前言
往往贪心可以体现在状态转移过程中的max()/min(),当然也可以直接贪心求解。可被3整除的最大和同时考察贪心 & 动态规划,而且不同于一般动态规划,状态转移时需要逆向转移,上一个状态转移到下一个的任意状态,就更新那个状态。
一、可被3整除的最大和
二、贪心 & 动规
1、贪心
// 可被3整除的最大和。
public class MaxSumDivThree {
/*
从nums中选一些元素,这些元素之和能被3整除,即是3的倍数。
可以理解为把数组分成多个不一定连续的子数组,然后它们的和都是3的倍数,那么组合起来就是一个大数组,且和一定是3的倍数。
但这是有问题的,这里贪心要最大,即会出现把有些数用了,但是会导致其他大数用不起来,比如同余数的5/8,能组合5就能组合8,要是必须舍去一个就bbq了。
有3种值,1 + 3x | 2 + 3x | 3x,3 个 1 + 3x可自己搭档,3个2 + 3x可自己搭档,1 + 3x + 2 + 3x也可以搭档。
当余数为0时,return sum;
当余数为1时,sum -= min(前一个最小的1+3x,前两个最小的2+3x),注:2+3x+2+3x = 1 + 3 * (2x + 1) = 1 + 3y;
当余数为2时,sum -= min(前一个最小的2+3x,前两个最小的1+3x),注:1+3x+1+3x = 2 + 3 * 2x = 2 + 3y;
*/
public int maxSumDivThree(int[] nums) {
// 贪心,去最小。
Arrays.sort(nums);
// 奇数偶数分类。
List<Integer> odd = new ArrayList<>();// 1 + 3x;
List<Integer> even = new ArrayList<>();// 2 + 3x;
int sum = 0;
for (int i : nums) {
sum += i;
// 只记录前两个。
if (i % 3 == 1 && odd.size() < 2) odd.add(i);
if (i % 3 == 2 && even.size() < 2) even.add(i);
}
int mod = sum % 3;
if (mod == 0) return sum;
int n1, n2;
if (mod == 1) {
n1 = odd.size() > 0 ? odd.get(0) : sum;
n2 = even.size() > 1 ? even.get(0) + even.get(1) : sum;
} else {
n1 = even.size() > 0 ? even.get(0) : sum;
n2 = odd.size() > 1 ? odd.get(0) + odd.get(1) : sum;
}
sum -= Math.min(n1, n2);
return sum;
}
}
2、贪心优化
// 两个1+3x,两个2+3x的小改进。
class MaxSumDivThree2 {
/*
从nums中选一些元素,这些元素之和能被3整除,即是3的倍数。
可以理解为把数组分成多个不一定连续的子数组,然后它们的和都是3的倍数,那么组合起来就是一个大数组,且和一定是3的倍数。
但这是有问题的,这里贪心要最大,即会出现把有些数用了,但是会导致其他大数用不起来,比如同余数的5/8,能组合5就能组合8,要是必须舍去一个就bbq了。
有3种值,1 + 3x | 2 + 3x | 3x,3 个 1 + 3x可自己搭档,3个2 + 3x可自己搭档,1 + 3x + 2 + 3x也可以搭档。
当余数为0时,return sum;
当余数为1时,sum -= min(前一个最小的1+3x,前两个最小的2+3x),注:2+3x+2+3x = 1 + 3 * (2x + 1) = 1 + 3y;
当余数为2时,sum -= min(前一个最小的2+3x,前两个最小的1+3x),注:1+3x+1+3x = 2 + 3 * 2x = 2 + 3y;
*/
public int maxSumDivThree(int[] nums) {
// 求元素和。
int sum = Arrays.stream(nums).sum();
// 只要前面最小的两个数。
int[] odd = new int[]{sum, sum};// 1 + 3x;
int[] even = new int[]{sum, sum};// 2 + 3x;
for (int i : nums) {
// 只记录前两个。
if (i % 3 == 1 && i < odd[1]) {
odd[1] = odd[0];
if (i < odd[0]) odd[0] = i;
else odd[1] = i;
}
if (i % 3 == 2 && i < even[1]) {
even[1] = even[0];
if (i < even[0]) even[0] = i;
else even[1] = i;
}
}
int mod = sum % 3;
if (mod == 0) return sum;
int n1 = mod == 1 ? odd[0] : odd[0] + odd[1];
int n2 = mod == 1 ? even[0] + even[1] : even[0];
sum -= Math.min(n1, n2);
return sum;
}
}
3、状压动规
// 一题多解,融汇贯通,动态规划。
class MaxSumDivThree3 {
/*
有3种值,1 + 3x | 2 + 3x | 3x,3 个 1 + 3x可自己搭档,3个2 + 3x可自己搭档,1 + 3x + 2 + 3x也可以搭档。
设最终状态为f[n][0],表示n个数和除3余数为0的最大值。
f[n][0] 由 f[n - 1][0] + nums[n - 1] || f[n - 1][1] + nums[n - 1] || f[n - 1][2] + nums[n - 1]转变成余数为0的一个 与 f[n - 1][0]取max
但是这样求状态很麻烦,不如反向思考,让f[n-1][0/1/2]+nums[n-1]对3取余,就去更新那个f[n][0/1/2]
注:由于当前状态只和前一个有关,则用一维状态f即可。
*/
public int maxSumDivThree(int[] nums) {
int[] f = new int[3];
int a, b, c;
for (int n : nums) {
a = f[0] + n;
b = f[1] + n;
c = f[2] + n;
// 逆向思考,各自更新一下状态。
f[a % 3] = Math.max(f[a % 3], a);
f[b % 3] = Math.max(f[b % 3], b);
f[c % 3] = Math.max(f[c % 3], c);
}
return f[0];
}
}
总结
1)贪心思维,需挖掘题目的个性条件,根据规律来贪心。
2)动态规划,需要把大问题分解成相同性质但规模更小的之问题,比如数组可以划成子数组,话虽简单,要多练习才能准确的找到中间的多个状态,和最终需要的一个状态,以及状态的转移(正向转移/逆向转移)。