1. 程式人生 > >[LeetCode] Matchsticks to Square 火柴棍組成正方形

[LeetCode] Matchsticks to Square 火柴棍組成正方形

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:

  1. The length sum of the given matchsticks is in the range of 0
     to 10^9.
  2. 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;
    }
};

類似題目:

參考資料: