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
這道題給了一個數組 nums,讓找出最大的元素之和,使得其可以整除3,注意這里不是子數組之和,並不需要連續,而是相當於某個子集之和。所以博主最先想到的方法是用類似 Subsets 中的方法生成所有的子集,並在生成的過程中計算每個子集的數字之和,然后判斷若能整除3,則更新結果 res,但是這種方法卻超時了。博主心想就一道 Medium 的題難道還需要用什么高端的方法,原來真的是有很高效的解法呢!首先來分析這道題,數字之和需要除以3,那么余數只有三種情況,余數為0即為整除,也是本題需要求解的情況,或者是余數為1或者為2。最好的情況就是整個數組之和除以3正好余數為0,這也是最大的數字之和,但是當整個數組之和除以3余1怎么辦?如果能在數組中找到一個除以3余1的數字,那么整個數組之和減去這個數字后一定能被3整除,這應該是小學學過的某種定理吧,即若a除以c的余數和b除以c的余數相同,則 a-b 一定可以整除c。
回到本題,為了使剩余的數字之和最大,減去的那個除以3余1的數字應該越小越小,同理,若整個數組之和除以3余2,則減去一個最小的除3余2的數字。於是這道題就變成了分別尋找最小的除3余2,除3余1的數(也可以是若個數之和),用 leftOne 和 leftTwo 來表示,既然要找最小值,則初始化應該是最大數,但這里這不能直接初始化為整型最大值,因為后面還會做加法運算可能導致整型溢出。由於題目限定了數字的大小不超過 10^4
,就用這個當初始值就可以了。然后遍歷整個數組,先將遍歷到的數字 num 加到結果 res 中,然后就要更新 leftOne 和 leftTwo 了,判斷若 num 除以3余1的話,則可以用 leftOne+num 來更新 leftTwo,因為兩個除3余1的數相加就會除3余2,然后再用 num 來更新 leftOne。同理,若 num 除以3余2的話,則可以用 leftTwo+num 來更新 leftOne,因為兩個除3余2的數相加就會除3余1,然后再用 num 來更新 leftTwo。這樣最后只要看數組之和 res 是否能整除3,能的話直接返回 res,若余1的話,返回 res-leftOne,否則返回 res-leftTwo 即可,參見代碼如下:
解法一:
class Solution {
public:
int maxSumDivThree(vector<int>& nums) {
int res = 0, leftOne = 1e4, leftTwo = 1e4;
for (int num : nums) {
res += num;
if (num % 3 == 1) {
leftTwo = min(leftTwo, leftOne + num);
leftOne = min(leftOne, num);
} else if (num % 3 == 2) {
leftOne = min(leftOne, leftTwo + num);
leftTwo = min(leftTwo, num);
}
}
if (res % 3 == 0) return res;
if (res % 3 == 1) return res - leftOne;
return res - leftTwo;
}
};
上面的解法是比較巧妙,但估計比較難想,這道題給的 hint 是要用動態規划 Dynamic Programming 來做,這里我們定義的 dp 數組和題目中提示不太一樣,這里定義一個 n+1 by 3 的二維數組,其中 dp[i][j]
表示前i個數字中的最大的數字之和且其除以3余 3-j
,為什么要這么定義呢?這跟后面的狀態轉移方程有關,由於j只有三個狀態 0,1,和2,所以可以分別來更新。
對於 dp[i][0]
來說,至少可以用 dp[i-1][0]
來更新,即不選擇當前這個數字 num。若選擇當前的數字 num,其不一定會被3整除,但是 dp[i-1][num%3] + num
一定會被3整除,這是為啥呢?首先,若 num 能被3整除,則這個表達肯定也能被3整除,沒有問題。若 num 除3余1,這樣的話就是 dp[i-1][1] + num
了,根據上面說的 dp 的定義,dp[i-1][1]
表示前 i-1 個數字中的最大數字之和且其除3余2,而 num 正好是一個除3余1的數,加上之后就可以被3整除了,所以可以用來更新 dp[i][0]。同理,若 num 除3余2,就是用 dp[i-1][2] + num
跟 dp[i-1][0]
相比,取較大值更新 dp[i][0]
。
對於 dp[i][1]
來說,至少可以用 dp[i-1][1]
來更新,即不選擇當前這個數字 num。若選擇當前的數字 num,其不一定會是除3余2的,但是 dp[i-1][(num+1)%3] + num
一定會是除3余2的,這是為啥呢?首先,若 num 能被3整除,則 num+1 是除3余1的,代入得到 dp[i-1][1]
是除3余2的,加上 num 還是除3余2的。對於 num 除3余1的情況,代入可以得到 dp[i-1][2] + num
,根據上面說的 dp 的定義,dp[i-1][2]
表示前 i-1 個數字中的最大數字之和且除以3余1,而 num 正好也是一個除3余1的數,加上之后整個就是除3余2的了,所以可以用來更新 dp[i][1]。同理,若 num 除3余2,就是用 dp[i-1][1] + num
跟 dp[i-1][2]
相比,取較大值更新 dp[i][2]
。
對於 dp[i][2]
的更新這里就不講了,跟上面的情況大同小異,最終的結果保存在 dp[n][0]
中,若覺得上面的講解很繞,不容易理解的話,這里建議使用題目中的例子1來一步一步推,觀察 dp 數組的每個數字的更新,這樣就可以理解了,參見代碼如下:
解法二:
class Solution {
public:
int maxSumDivThree(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> dp(n + 1, vector<int>(3));
dp[0] = {0, INT_MIN, INT_MIN};
for (int i = 1; i <= n; ++i) {
int idx = i - 1;
dp[i][0] = max(dp[i - 1][0], dp[i - 1][nums[idx] % 3] + nums[idx]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][(nums[idx] + 1) % 3] + nums[idx]);
dp[i][2] = max(dp[i - 1][2], dp[i - 1][(nums[idx] + 2) % 3] + nums[idx]);
}
return dp[n][0];
}
};
再來看一種一維 dp 數組的解法,這個可不是上面的 dp 解法的省空間版本啊,二者的余數定義不同。這里的 dp[i] 表示整個數組的最大和且除3余i。下面來分析狀態轉移方程,遍歷數組的每一個數組 num,由於 dp 是一維數組,沒有多余的維度去保存上一輪的狀態值,所以之前的狀態值也都保存在 dp 中,為了不混淆上一輪和當前輪的 dp 值,在更新 dp 之前,把當前已有的 dp 值保存到一個臨時數組 t 中,遍歷t數組,當前遍歷到的數字i除以3余幾並不重要,其加上新數字 num 之后再對3取余,就是要更新的 dp 位置,用 i+num
去更新即可,參見代碼如下:
解法三:
class Solution {
public:
int maxSumDivThree(vector<int>& nums) {
vector<int> dp(3);
for (int num : nums) {
vector<int> t = dp;
for (int i : t) {
dp[(i + num) % 3] = max(dp[(i + num) % 3], i + num);
}
}
return dp[0];
}
};
Github 同步地址:
https://github.com/grandyang/leetcode/issues/1262
參考資料:
https://leetcode.com/problems/greatest-sum-divisible-by-three/