Remember the story of Little Match Girl? By now, you know exactly what matchsticks the little match girl has, please find out a way you can make one square by using up all those matchsticks. You should not break any stick, but you can link them up, and each matchstick must be used exactly one time.
Your input will be several matchsticks the girl has, represented with their stick length. Your output will either be true or false, to represent whether you could make one square using all the matchsticks the little match girl has.
Example 1:
Input: [1,1,2,2,2] Output: true Explanation: You can form a square with length 2, one side of the square came two sticks with length 1.
Example 2:
Input: [3,3,3,3,4] Output: false Explanation: You cannot find a way to form a square with all the matchsticks.
Note:
- The length sum of the given matchsticks is in the range of
0
to10^9
. - The length of the given matchstick array will not exceed
15
.
我已經服了LeetCode了,連賣火柴的小女孩也能改編成題目,還能不能愉快的玩耍了,坐等灰姑娘,丑小鴨的改編題了。好了,言歸正傳,這道題讓我們用數組中的數字來擺出一個正方形。跟之前有道題Partition Equal Subset Sum有點像,那道題問我們能不能將一個數組分成和相等的兩個子數組,而這道題實際上是讓我們將一個數組分成四個和相等的子數組。我一開始嘗試着用那題的解法來做,首先來判斷數組之和是否是4的倍數,然后還是找能否分成和相等的兩個子數組,但是在遍歷的時候加上判斷如果數組中某一個數字大於一條邊的長度時返回false。最后我們同時檢查dp數組中一條邊長度位置上的值跟兩倍多一條邊長度位置上的值是否為true,這種方法不幸TLE了。所以只能上論壇求助各路大神了,發現了可以用優化過的遞歸來解,遞歸的方法基本上等於brute force,但是C++版本的直接遞歸沒法通過OJ,而是要先給數組從大到小的順序排序,這樣大的數字先加,如果超過target了,就直接跳過了后面的再次調用遞歸的操作,效率會提高不少,所以會通過OJ。下面來看代碼,我們建立一個長度為4的數組sums來保存每個邊的長度和,我們希望每條邊都等於target,數組總和的四分之一。然后我們遍歷sums中的每條邊,我們判斷如果加上數組中的當前數字大於target,那么我們跳過,如果沒有,我們就加上這個數字,然后對數組中下一個位置調用遞歸,如果返回為真,我們返回true,否則我們再從sums中對應位置將這個數字減去繼續循環,參見代碼如下:
解法一:
class Solution { public: bool makesquare(vector<int>& nums) { if (nums.empty() || nums.size() < 4) return false; int sum = accumulate(nums.begin(), nums.end(), 0); if (sum % 4 != 0) return false; vector<int> sums(4, 0); sort(nums.rbegin(), nums.rend()); return helper(nums, sums, 0, sum / 4); } bool helper(vector<int>& nums, vector<int>& sums, int pos, int target) { if (pos >= nums.size()) { return sums[0] == target && sums[1] == target && sums[2] == target; } for (int i = 0; i < 4; ++i) { if (sums[i] + nums[pos] > target) continue; sums[i] += nums[pos]; if (helper(nums, sums, pos + 1, target)) return true; sums[i] -= nums[pos]; } return false; } };
其實這題還有迭代的方法,很巧妙的利用到了位操作的特性,前面的基本求和跟判斷還是一樣,然后建立一個變量all,初始化為(1 << n) - 1,這是什么意思呢,all其實是一個mask,數組中有多少個數字,all就有多少個1,表示全選所有的數字,然后變量target表示一條邊的長度。我們建立兩個一位向量masks和validHalf,其中masks保存和target相等的幾個數字位置的mask,validHalf保存某個mask是否是總和的一半。然后我們從0遍歷到all,實際上就是遍歷所有子數組,然后我們根據mask來計算出子數組的和,注意這里用了15,而不是32,因為題目中說了數組元素個數不會超過15個。我們算出的子數組之和如果等於一條邊的長度target,我們遍歷masks數組中其他等於target的子數組,如果兩個mask相與不為0,說明有公用的數字,直接跳過;否則將兩個mask或起來,說明我們當前選的數字之和為數組總和的一半,更新validHalf的對應位置,然后我們通過all取出所有剩下的數組,並在validHalf里查找,如果也為true,說明我們成功的找到了四條邊,參見代碼如下:
解法二:
class Solution { public: bool makesquare(vector<int>& nums) { if (nums.empty() || nums.size() < 4) return false; int sum = accumulate(nums.begin(), nums.end(), 0); if (sum % 4 != 0) return false; int n = nums.size(), all = (1 << n) - 1, target = sum / 4; vector<int> masks, validHalf(1 << n, false); for (int i = 0; i <= all; ++i) { int curSum = 0; for (int j = 0; j <= 15; ++j) { if ((i >> j) & 1) curSum += nums[j]; } if (curSum == target) { for (int mask : masks) { if ((mask & i) != 0) continue; int half = mask | i; validHalf[half] = true; if (validHalf[all ^ half]) return true; } masks.push_back(i); } } return false; } };
類似題目:
參考資料:
https://discuss.leetcode.com/topic/72107/java-dfs-solution-with-explanation
https://discuss.leetcode.com/topic/72232/c-bit-masking-dp-solution-with-detailed-comments