[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

這道題給了一個數組 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] + numdp[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] + numdp[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/

https://leetcode.com/problems/greatest-sum-divisible-by-three/discuss/431077/JavaC%2B%2BPython-One-Pass-O(1)-space

https://leetcode.com/problems/greatest-sum-divisible-by-three/discuss/431108/Java-O(N)-solution-Simple-Math-O(1)-space

https://leetcode.com/problems/greatest-sum-divisible-by-three/discuss/431785/C%2B%2B-Commented-DP-solution-that-actually-makes-sense.


LeetCode All in One 題目講解匯總(持續更新中...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM