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.)html
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.git
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:github
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,參見代碼以下:函數
解法一:post
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] 便可,最後查找的時候,有點像二維數組的查找方式同樣,直接兩個中括號座標定位便可,參見代碼以下:優化
解法二:url
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解法還要快,叼,參見代碼以下:spa
解法三:
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
相似題目:
參考資料:
https://leetcode.com/problems/split-array-with-same-average/