[LeetCode] 805. Split Array With Same Average 分割數組成相同平均值的小數組


 

In a given integer array A, we must move every element of A to either list B or list C. (B and C initially start empty.)

Return true if and only if after such a move, it is possible that the average value of B is equal to the average value of C, and B and C are both non-empty.

Example :
Input: 
[1,2,3,4,5,6,7,8]
Output: true
Explanation: We can split the array into [1,4,5,8] and [2,3,6,7], and both of them have the average of 4.5.

Note:

  • The length of A will be in the range [1, 30].
  • A[i] will be in the range of [0, 10000].

 

這道題給了我們一個數組A,問是否可以把數組分割成兩個小數組,並且要求分成的這兩個數組的平均值相同。之前我們有做過分成和相同的兩個小數組 Split Array with Equal Sum,看了題目中的給的例子,你可能會有種錯覺,覺得這兩個問題是一樣的,因為題目中分成的兩個小數組的長度是一樣的,那么平均值相同的話,和一定也是相同的。但其實是不對的,很簡單的一個例子,比如數組 [2, 2, 2],可以分成平均值相同的兩個數組 [2, 2] 和 [2],但是無法分成和相同的兩個數組。 現在唯一知道的就是這兩個數組的平均值相等,這里有個隱含條件,就是整個數組的平均值也和這兩個數組的平均值相等,這個不用多說了吧,加法的結合律的應用啊。由於平均值是由數字總和除以個數得來的,那么假設整個數組有n個數組,數字總和為 sum,分成的其中一個數組有k個,假設其數字和為 sum1,那么另一個數組就有 n-k 個,假設其數組和為 sum2,就有如下等式:

sum / n = sum1 / k = sum2 / (n - k)

看前兩個等式,sum / n = sum1 / k,可以變個形,sum * k / n = sum1,那么由於數字都是整數,所以 sum * k 一定可以整除 n,這個可能當作一個快速的判斷條件。下面來考慮k的取值范圍,由於要分成兩個數組,可以始終將k當作其中較短的那個數組,則k的取值范圍就是 [1, n/2],就是說,如果在這個范圍內的k,沒有滿足的 sum * k % n == 0 的話,那么可以直接返回false,這是一個快速的剪枝過程。如果有的話,也不能立即說可以分成滿足題意的兩個小數組,最簡單的例子,比如數組 [1, 3],當k=1時,sum * k % n == 0 成立,但明顯不能分成兩個平均值相等的數組。所以還需要進一步檢測,當找到滿足的 sum * k % n == 0 的k了時候,其實可以直接算出 sum1,通過 sum * k / n,那么就知道較短的數組的數字之和,只要能在原數組中數組找到任意k個數字,使其和為 sum1,就可以 split 成功了。問題到這里就轉化為了如何在數組中找到任意k個數字,使其和為一個給定值。有點像 Combination Sum III 那道題,當然可以根據不同的k值,都分別找原數組中找一遍,但想更高效一點,因為畢竟k的范圍是固定的,可以事先任意選數組中k個數字,將其所有可能出現的數字和保存下來,最后再查找。那么為了去重復跟快速查找,可以使用 HashSet 來保存數字和,可以建立 n/2 + 1 個 HashSet,多加1是為了不做數組下標的轉換,並且防止越界,因為在累加的過程中,計算k的時候,需要用到 k-1 的情況。講到這里,你會不會一拍大腿,吼道,這尼瑪不就是動態規划 Dynamic Programming 么。恭喜你騷年,沒錯,這里的 dp 數組就是一個包含 HashSet 的數組,其中 dp[i] 表示數組中任選 i 個數字,所有可能的數字和。首先在 dp[0] 中加入一個0,這個是為了防止越界。更新 dp[i] 的思路是,對於 dp[i-1] 中的每個數字,都加上一個新的數字,所以最外層的 for 循環是遍歷原數組的中的每個數字的,中間的 for 循環是遍歷k的,從 n/2 遍歷到1,然后最內層的 for 循環是遍歷 dp[i-1] 中的所有數組,加上最外層遍歷到的數字,並存入 dp[i] 即可。整個 dp 數組更新好了之后,下面就是驗證的環節了,對於每個k值,驗證若 sum * k / n == 0 成立,並且 sum * i / n 在 dp[i] 中存在,則返回 true。最后都沒有成立的話,返回 false,參見代碼如下:

 

解法一:

class Solution {
public:
    bool splitArraySameAverage(vector<int>& A) {
        int n = A.size(), m = n / 2, sum = accumulate(A.begin(), A.end(), 0);
        bool possible = false;
        for (int i = 1; i <= m && !possible; ++i) {
            if (sum * i % n == 0) possible = true;
        }
        if (!possible) return false;
        vector<unordered_set<int>> dp(m + 1);
        dp[0].insert(0);
        for (int num : A) {
            for (int i = m; i >= 1; --i) {
                for (auto a : dp[i - 1]) {
                    dp[i].insert(a + num);
                }
            }
        }
        for (int i = 1; i <= m; ++i) {
            if (sum * i % n == 0 && dp[i].count(sum * i / n)) return true;
        }
        return false;
    }
};

 

下面這種解法跟上面的解法十分的相似,唯一的不同就是使用了 bitset 這個數據結構,在之前那道 Partition Equal Subset Sum 的解法二中,也使用了 biset,了解了其使用方法后,就會發現使用這里使用它只是單純的為了炫技而已。由於 biset 不能動態變換大小,所以初始化的時候就要確定,題目中限定了數組中最多 30 個數字,每個數字最大 10000,那么就初始化 n/2+1 個 biset,每個大小為 300001 即可。然后每個都初始化個1進去,之后更新的操作,就是把 bits[i-1] 左移 num 個,然后或到 bits[i] 即可,最后查找的時候,有點像二維數組的查找方式一樣,直接兩個中括號坐標定位即可,參見代碼如下:

 

解法二:

class Solution {
public:
    bool splitArraySameAverage(vector<int>& A) {
        int n = A.size(), m = n / 2, sum = accumulate(A.begin(), A.end(), 0);
        bool possible = false;
        for (int i = 1; i <= m && !possible; ++i) {
            if (sum * i % n == 0) possible = true;
        }
        if (!possible) return false;
        bitset<300001> bits[m + 1] = {1};
        for (int num : A) {
            for (int i = m; i >= 1; --i) {
                bits[i] |= bits[i - 1] << num;
            }
        }
        for (int i = 1; i <= m; ++i) {
            if (sum * i % n == 0 && bits[i][sum * i / n]) return true;
        }
        return false;
    }
};

 

再來看一種遞歸的寫法,說實話在博主看來,一般不使用記憶數組的遞歸解法,等同於暴力破解,基本很難通過 OJ,除非你進行了大量的剪枝優化處理。這里就是這種情況,首先還是常規的k值快速掃描一遍,確保可能存在解。然后給數組排了序,然后對於滿足 sum * k % n == 0 的k值,進行了遞歸函數的進一步檢測。需要傳入當前剩余數字和,剩余個數,以及在原數組中的遍歷位置,如果當前數字剩余個數為0了,說明已經取完了k個數字了,那么如果剩余數字和為0了,則說明成功的找到了k個和為 sum * k / n 的數字,返回 ture,否則 false。然后看若當前要加入的數字大於當前的平均值,則直接返回 false,因為已經給原數組排過序了,之后的數字只會越來越大,一旦超過了平均值,就不可能再降下來了,這是一個相當重要的剪枝,估計能過 OJ 全靠它。之后開始從 start 開始遍歷,當前遍歷的結束位置是原數組長度n減去當前剩余的數字,再加1,因為確保給 curNum 留夠足夠的位置來遍歷。之后就是跳過重復,對於重復的數字,只檢查一遍就好了。調用遞歸函數,此時的 curSum 要減去當前數字 A[i],curNum 要減1,start 為 i+1,若遞歸函數返回 true,則整個返回 true。for 循環退出后返回 false。令博主感到驚訝的是,這個代碼的運行速度比之前的DP解法還要快,叼,參見代碼如下:

 

解法三:

class Solution {
public:
    bool splitArraySameAverage(vector<int>& A) {
        int n = A.size(), m = n / 2, sum = accumulate(A.begin(), A.end(), 0);
        bool possible = false;
        for (int i = 1; i <= m && !possible; ++i) {
            if (sum * i % n == 0) possible = true;
        }
        if (!possible) return false;
        sort(A.begin(), A.end());
        for (int i = 1; i <= m; ++i) {
            if (sum * i % n == 0 && helper(A, sum * i / n, i, 0)) return true;
        }
        return false;
    }
    bool helper(vector<int>& A, int curSum, int curNum, int start) {
        if (curNum == 0) return curSum == 0;
        if (A[start] > curSum / curNum) return false;
        for (int i = start; i < A.size() - curNum + 1; ++i) {
            if (i > start && A[i] == A[i - 1]) continue;
            if(helper(A, curSum - A[i], curNum - 1, i + 1)) return true;
        }
        return false;
    }
};

 

Github 同步地址:

https://github.com/grandyang/leetcode/issues/805

 

類似題目:

Combination Sum III

Partition Equal Subset Sum

Split Array with Equal Sum 

 

參考資料:

https://leetcode.com/problems/split-array-with-same-average/

https://leetcode.com/problems/split-array-with-same-average/discuss/120660/Java-accepted-recursive-solution-with-explanation

https://leetcode.com/problems/split-array-with-same-average/discuss/120842/DP-with-bitset-over-*sum*-(fast-PythonRuby-decent-C%2B%2B)

https://leetcode.com/problems/split-array-with-same-average/discuss/120667/C%2B%2B-Solution-with-explanation-early-termination-(Updated-for-new-test-case)

 

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


免責聲明!

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



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