[路飞]火柴拼正方形

105 阅读1分钟

记录 1 道算法题

火柴拼正方形

leetcode.cn/problems/ma…


要求:提供一组长度不等的火柴,将火柴拼成正方形,火柴不能折断拐弯。如果能拼出正方形返回 true,否则返回 false。

比如:[1,1,2,2,2],输出:true

无论是哪一种解法,我们都可以排除的就是不能被 4 整除的周长,以及火柴的数量不足 4 个的情况。

  1. 回溯法

我们可以用一个数组保存 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)
    }
  1. 动态规划

首先这里状态存储和检查采用了位掩码,这里就不赘述了。

假设火柴的数量为 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
    }