[LeetCode] 473. Matchsticks to Square
grandyang opened this issue · comments
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.
You are given an integer array matchsticks
where matchsticks[i]
is the length of the ith
matchstick. You want to use all the matchsticks to make one square. You should not break any stick, but you can link them up, and each matchstick must be used exactly one time.
Return true
if you can make this square and false
otherwise.
Example 1:
Input: matchsticks = [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: matchsticks = [3,3,3,3,4]
Output: false
Explanation: You cannot find a way to form a square with all the matchsticks.
Constraints:
1 <= matchsticks.length <= 15
1 <= matchsticks[i] <= 108
博主已经服了 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;
}
};
Github 同步地址:
类似题目:
参考资料:
https://leetcode.com/problems/matchsticks-to-square/
https://leetcode.com/problems/matchsticks-to-square/discuss/95744/cpp-6ms-solution-with-DFS
https://leetcode.com/problems/matchsticks-to-square/discuss/95729/Java-DFS-Solution-with-Explanation