题目描述
LeetCode原题链接:1262. Greatest Sum Divisible by Three
Given an array nums
of integers, we need to find the maximum possible sum of elements of the array such that it is divisible by three.
Example 1:
Input: nums = [3,6,5,1,8] Output: 18 Explanation: Pick numbers 3, 6, 1 and 8 their sum is 18 (maximum sum divisible by 3).
Example 2:
Input: nums = [4] Output: 0 Explanation: Since 4 is not divisible by 3, do not pick any number.
Example 3:
Input: nums = [1,2,3,4,4] Output: 12 Explanation: Pick numbers 1, 3, 4 and 4 their sum is 12 (maximum sum divisible by 3).
Constraints:
1 <= nums.length <= 4 * 10^4
1 <= nums[i] <= 10^4
解法一:动态规划
这道题目的hint提示我们可以使用动态规划来解决:
Represent the state as DP[pos][mod]: maximum possible sum starting in the position "pos" in the array where the current sum modulo 3 is equal to mod.
先来看一下提示里对于dp数组的定义。dp[pos][mod]表示从pos位置开始的最大和且这个最大和与3取余得mod。有点绕人,什么意思呢?我们知道,任意数a与3取余只有3种情况:余0(即整除)、余1、余2。针对这三种情况,在[start, end]范围内就会有三个最大和。举个例子,比如对于数组[2, 1, 3, 4, 2, 5],在下标为[1, 4]范围内,即1、3、4、2这几个数中,余1的最大和为1+3+4+2=10,余2的最大和为1+3+4=8,整除的最大和为3+4+2=9。我们可以在遍历的过程中,每遇到一个新的数,就去与已经得到的和相加并与3取余,更新dp数组。这里起始位置pos其实意义不大,我们只需要知道已经遍历过的数中最大和与3的关系就好了,所以完全可以用一个一维数组来记录。dp[mod]表示当前位置整个数组(从下标0到当前下标)的最大和且这个最大和与3取余得mod。
需要注意,由于是一维数组无法保存之前的状态,所以当遍历到一个新的位置时,可以用一个临时数组暂时保存上一位置的结果,然后再去更新dp。
下面来看怎么更新dp。遍历到的数字num除3余几并不重要,同之前的和oldSum相加与3取余,得到要更新的dp的位置即可。所以状态转移方程为:
dp[(oldSum + num) % 3] = max(dp[(oldSum + num) % 3], oldSum + num)
这里的oldSum从前面提到的临时数组中取。最终结果因为要求整除的最大和,所以直接返回dp[0]即可。
代码示例(JavaScript)
1 var maxSumDivThree = function(nums) { 2 let dp = new Array(3); 3 dp.fill(0); 4 nums.forEach((num) => { 5 // save history sum 6 let temp = [...dp]; 7 temp.forEach((sum) => { 8 dp[(sum + num) % 3] = Math.max(dp[(sum + num) % 3], sum + num); 9 }); 10 }); 11 return dp[0]; 12 };
解法二:数学知识
小学数学应该讲过这么一个定理:若数字a和b分别除以数字c得到的余数相同,那么 (a-b) 必定能够整除c。
对应这道题,如果数组之和为a,当 a%3 = 0 的时候,a就是要求的最大值,直接返回;如果 a%3 != 0,那么我们需要在数组中找b,使 b%3 == a%3,同时b尽可能的小,这样 (a-b) 就能被3整除且最大;其中,b可以是单个数,也可以是某几个数的和。前面提到,如果某一个数不能被3整除,那么余数只有可能是1或2,我们在遍历的过程中,去找这两种情况的最小值。
怎么找呢?要知道,这个最小值可以是某一个数,也可以是某几个数的和。某一个数好说,直接看这个数除3余几就行了。某几个数的和怎么在一次遍历过程中求呢?这里又要涉及一个小学数学知识了。还是有两个数,我们设其为x和y,如果 x%3=m1, y%3=m2, 那么 (x+y)%3 就等于 (m1+m2)%3。比如8%3=2,4%3=1,那么 (8+4)%3 = 12%3 = (2+1)%3 = 0。很好,现在我们要找余数为1的和,那么这个数就可以是余数为1的数和余数为0的数相加;找余数为2的和,那么这个数就可以是两个余数为1的数相加。
最小余1的数、最小余2的数我们可以分别用两个变量modOne、modeTwo来表示,当遍历到一个新的位置时,根据当前位置上的数除以3的余数去更新modOne和modeTwo。同时,在这个遍历过程中,再用一个变量sum去累加每个位置上的数。遍历结束后sum就是整个数组的和,根据 sum%3 的情况来得到最终结果:
- sum%3 = 0 ---> return sum;
- sum%3 = 1 ---> return sum-modOne;
- sum%3 = 2 ---> return sum-modTwo;
注意,因为在求modOne和modTwo最小值的过程中有加法操作,因此为了防止溢出,它们的初始值设为Constraints中nums[i]的最大值即可。
代码示例(JavaScript)
1 var maxSumDivThree = function(nums) { 2 let sum = 0; 3 let modOne = Math.pow(10, 4), modTwo = Math.pow(10, 4); 4 nums.forEach((num) => { 5 sum += num; 6 if(num % 3 == 1) { 7 modTwo = Math.min(modTwo, num + modOne); 8 modOne = Math.min(modOne, num); 9 } 10 if(num % 3 == 2) { 11 modOne = Math.min(modOne, num + modTwo); 12 modTwo = Math.min(modTwo, num); 13 } 14 }); 15 if(sum % 3 == 0) return sum; 16 sum -= sum % 3 == 1 ? modOne : modTwo; 17 return sum; 18 };
注意第7、8行和第11、12行更新modOne和modTwo时候的顺序。第7行和第11行加法里面的modOne、modTwo要用旧的值而不是此轮遍历更新后的值,因此要写在前面。
https://leetcode.com/problems/greatest-sum-divisible-by-three/