Medium草稿★★★★★O(4^n) 時間 · O(n) 空間
BacktrackingBit ManipulationDynamic Programming
Patterns🌿 回溯💡 位元技巧
尚未複習過

解法已隱藏 — 先讀題目敘述、自己想想看,再點上方按鈕揭曉。

473Matchsticks to SquareBacktrackingMediumBacktracking

給定一個整數陣列 matchsticks,每個元素代表一根火柴的長度。判斷是否能用所有火柴拼成一個正方形(每根火柴恰好用一次,不能折斷)。

Example:

Input: matchsticks = [1,1,2,2,2] Output: true

Intuition

TIP

核心思路:將火柴分成 4 組,每組的總和等於周長 / 4,用回溯法嘗試分配。

Approaches

1. Bitmask DP — O(2^n · n) / O(2^n)
  • Idea: dp[mask] = 用掉 mask 這些火柴後、當前未填滿那條邊已放的長度(mod side);填滿一邊就歸 0 進下一邊
  • Time: O(2^n · n)
  • Space: O(2^n)
class Solution {
    fun makesquare(matchsticks: IntArray): Boolean {
        val total = matchsticks.sum()
        if (total % 4 != 0) return false
        val side = total / 4
        if (matchsticks.any { it > side }) return false
        val n = matchsticks.size
        val full = (1 shl n) - 1
        val dp = IntArray(1 shl n) { -1 }   // -1 = 不可達
        dp[0] = 0
        for (mask in 0..full) {
            if (dp[mask] == -1) continue
            for (i in 0 until n) {
                if (mask and (1 shl i) != 0) continue
                if (dp[mask] + matchsticks[i] > side) continue
                val next = mask or (1 shl i)
                if (dp[next] == -1) dp[next] = (dp[mask] + matchsticks[i]) % side
            }
        }
        return dp[full] == 0
    }
}
⭐ 2. Backtracking + Pruning — O(4^n) / O(n)
  • Idea: 嘗試將每根火柴放入 4 條邊中的一條,若某邊超過目標則回溯。降序排序大幅減少搜索
  • Time: O(4^n) 最差情況,但剪枝後遠小於此
  • Space: O(n) - 遞迴深度
class Solution {
    fun makesquare(matchsticks: IntArray): Boolean {
        val total = matchsticks.sum()
        if (total % 4 != 0) return false
        val side = total / 4

        // 降序排序,讓大的先嘗試,提早剪枝
        matchsticks.sortDescending()

        // 若最大的火柴超過邊長,不可能
        if (matchsticks[0] > side) return false

        val sides = IntArray(4)

        fun backtrack(idx: Int): Boolean {
            if (idx == matchsticks.size) {
                return sides.all { it == side }
            }

            val seen = mutableSetOf<Int>()
            for (i in 0 until 4) {
                // 剪枝:放入後超過邊長
                if (sides[i] + matchsticks[idx] > side) continue
                // 剪枝:相同長度的邊不重複嘗試
                if (sides[i] in seen) continue
                seen.add(sides[i])

                sides[i] += matchsticks[idx]
                if (backtrack(idx + 1)) return true
                sides[i] -= matchsticks[idx]
            }

            return false
        }

        return backtrack(0)
    }
}

🔑 Takeaways