一、题目描述
还记得童话《卖火柴的小女孩》吗?现在,你知道小女孩有多少根火柴,请找出一种能使用所有火柴拼成一个正方形的方法。不能折断火柴,可以把火柴连接起来,并且每根火柴都要用到。
输入为小女孩拥有火柴的数目,每根火柴用其长度表示。输出即为是否能用所有的火柴拼成正方形。
来源:力扣(LeetCode) 链接:火柴拼正方形
二、算法思路
此题为经典的DFS搜索剪枝题目,建议背过。观前提示,本题有较多证明,如果没有足够安静的环境或者没有多余的时间建议先收藏下来,日后再看。
剪枝内容:
- 从大到小枚举每根火柴(目的是使在凑齐一条边的时候,每次选择最长的可以令下一次可选择范围变小。有效的减少枚举次数)
- 每条边内部按编号从小到大排列(目的是避免形如1-2-3,1-3-2的组合形式出现,因为这样组合长度是相同的,没必要多次枚举)
- 若当前枚举到的这根火柴加到当前边的这个位置上后,无法凑齐四条边
- 跳过当前长度相等的火柴
- 若这根火柴是放在当前边的第一位失败了,那么直接剪枝。当前这种方案是不可能成功的
- 若这根火柴放在当前边的最后一个位置且能够使当前边刚好满足长度,但是最后四条边无法凑齐。那么这种方案也是不可能成功的,剪枝。
证明3-1:反证法,假设当前这根火柴为a,存在一种合法方案,有一根长度跟a相等的火柴b放在这个位置上。那么就代表a这根火柴应该是放在【当前或后面要匹配的边】的某个位置上。那么因为长度是相等的,所以我们可以让a,b两根火柴互换位置,可以发现这也是一种合法方案,故与a这根火柴加到当前边的这个位置上后,无法凑齐四条边这个事实矛盾
证明3-2:同反证法,假设当前这根火柴为a,若存在一种合法方案,使得不使用当前这根火柴但是能凑齐四条边,由于我们是按编号从小到大选择火柴的,那么因为火柴a是当前所有可选火柴的第一根,所以它必然会出现在未来某条边的第一个位置上。那么将这条边和当前边互换位置,就意味着火柴a加到当前边的这个位置上后是由合法方案的,和事实矛盾
证明3-3:同反证法,加上当前这根火柴为a,若存在一种合法方案,不使用这根火柴但是能让所有的边都凑到相等的长度的话。那么就意味着a这根火柴在后面某条边的某个位置,我们当前这条边【最后面的长度和a这根火柴相同的这部分火柴】与火柴a互换,就变成了存在一种合法方案,火柴a是可以放在当前这条边最后面的,与事实矛盾。
证毕。
AC代码
/**
* @param {number[]} nums
* @return {boolean}
*/
var makesquare = function(nums) {
const n = nums.length, st = {};
if(!n) return false; // 特判空数组
let sum = 0;
for(let x of nums) sum += x;
if(sum % 4 != 0) return false;
const target = sum /= 4;
nums.sort((a, b) => b - a); // 剪枝1: 让火柴长度递减
return dfs(0, 0, 0);
function dfs(start, cur, cnt) {
if(cnt == 3) return true;
if(cur == target) return dfs(0, 0, cnt + 1);
for(let i = start; i < n; ++ i) { // 剪枝2: 固定火柴编号顺序
if(st[i]) continue; // 若这根火柴使用过了, 则跳过
if(cur + nums[i] <= target) {
st[i] = true;
if(dfs(i + 1, cur + nums[i], cnt)) return true;
st[i] = false;
}
else if(!cur || cur + nums[i] == target) return false; // 剪枝b和剪枝c
while(i + 1 < nums.length && nums[i] == nums[i + 1]) ++ i; // 剪枝a
}
return false;
}
};
本文正在参与「掘金 3 月闯关活动」,点击查看活动详情