记录 1 道算法题
火柴拼正方形
要求:提供一组长度不等的火柴,将火柴拼成正方形,火柴不能折断拐弯。如果能拼出正方形返回 true,否则返回 false。
比如:[1,1,2,2,2],输出:true
无论是哪一种解法,我们都可以排除的就是不能被 4 整除的周长,以及火柴的数量不足 4 个的情况。
- 回溯法
我们可以用一个数组保存 4 条边的长度。通过循环分别尝试将火柴放到每一条边上,也就是说每一个火柴有 4 个可能性,再通过 dfs 递归维持一个当前方案的记录。通过检查火柴是否能正确的放完返回 true 或者 false。
为了避免栈溢出,我们先放置长的火柴,所以需要进行从大到小的排序
function makesquare(matchsticks) {
let sum = 0
for(let m of matchsticks) {
sum += m
}
// 周长不是4的倍数
if (sum % 4 !== 0 || matchsticks.length < 4) {
return false
}
const sideLen = sum / 4
const sides = Array(4).fill(0)
matchsticks.sort((a, b) => b - a)
const walk = (matchsticks, sides, index, sideLen) => {
// 放置到最后一条边,并符合边长
if (index === matchsticks) {
return true
}
const m = matchsticks[index]
// 每条边都放一次
for(let i = 0; i < sides.length; i++) {
sides[i] += m
// 如果能够放得下,就根据这个方案,进行深度遍历,去放下一个边
if (sides[i] <= sideLen && walk(matchsticks, sides, index + 1, sideLen)) {
return true
}
// 当前方案遍历完,将火柴移除,下一轮循环放到另一条边
sides[i] -= m
}
// 中途没有 return 所以是不符合条件
return false
}
return walk(matchsticks, sides, 0, sideLen)
}
- 动态规划
首先这里状态存储和检查采用了位掩码,这里就不赘述了。
假设火柴的数量为 len。我们用二进制的方式来记录状态,需要一个 len 位的数。1 << len
,假设第 2 位为 1,那么代表 matchsticks 中第 2 根火柴被使用了。
如果需要去除第 3 根火柴,那么就进行与运算记录的二进制数 & ~(1 << 3)
。
由于二进制的 1 和 0 的组合排列就可以涵盖所有情况,因此建立一个双循环,外循环是从 1 开始,遍历这个数。内循环则是使用火柴的数量。
function makesquare(matchsticks) {
let sum = 0
for(let m of matchsticks) {
sum += m
}
// 周长不是4的倍数
if (sum % 4 !== 0 || matchsticks.length < 4) {
return false
}
const sideLen = sum / 4
const len = matchsticks.length
const status = 1 << len
const dp = Array(status).fill(-1)
// 垫一下
dp[0] = 0
for(let i = 1; i < status; i++) {
for(let j = 0; j < len; j++) {
// 回到上次放置这根火柴前的状态
const withoutJ = i & ~(1 << j)
// 检查是否可以进行放置
// 必须是已处理的火柴,并且当前火柴放到边上不超过边长
if (dp[withoutJ] >= 0 && dp[withoutJ] + matchsticks[j] <= sideLen) {
// 这根火柴能放
// mod 边长的作用在于检查是否已经等于边长
dp[i] = (dp[widthoutJ] + matchsticks[j]) % sideLen
break
}
}
}
// 最后一个状态是否为 0,如果是就代表都放置完成
return dp[status - 1] === 0
}