Hard草稿★★★★★O(2^n * m * n) 時間 · O(2^n) 空間
Dynamic ProgrammingBitmaskStringBacktracking
Patterns🎭 狀態壓縮 DP🌿 回溯📈 線性 DP・Kadane
尚未複習過

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

691Stickers to Spell Word1-D DPHard1-D DP

給定 n 種貼紙(每種無限供應),每張貼紙上有若干小寫字母。要拼出目標字串 target,最少需要幾張貼紙?如果無法拼出,回傳 -1。

Example:

Input: stickers = [“with”,“example”,“science”], target = “thehat” Output: 3

Intuition

TIP

用 bitmask 表示 target 中哪些字元已被覆蓋,BFS/DP 找最少貼紙數。

Approaches

1. Bitmask DP (Top-Down) — O(2^n * m * n) / O(2^n)
  • Idea: 記憶化搜尋,每個 mask 狀態嘗試每張貼紙
  • Time: O(2^n * m * n) 其中 n = target 長度, m = 貼紙數
  • Space: O(2^n)
class Solution {
    fun minStickers(stickers: Array<String>, target: String): Int {
        val n = target.length
        val full = (1 shl n) - 1
        val memo = IntArray(1 shl n) { -2 }
        memo[0] = 0

        // 預處理每張貼紙的字元頻率
        val stickerCounts = stickers.map { s ->
            IntArray(26).also { cnt -> s.forEach { cnt[it - 'a']++ } }
        }

        fun dp(mask: Int): Int {
            if (mask == full) return 0
            if (memo[mask] != -2) return memo[mask]

            // 找第一個未覆蓋的字元
            var first = -1
            for (i in 0 until n) {
                if (mask and (1 shl i) == 0) { first = i; break }
            }

            var best = Int.MAX_VALUE
            for (cnt in stickerCounts) {
                // 只嘗試包含第一個未覆蓋字元的貼紙
                if (cnt[target[first] - 'a'] == 0) continue
                val avail = cnt.copyOf()
                var newMask = mask
                for (i in 0 until n) {
                    if (newMask and (1 shl i) == 0 && avail[target[i] - 'a'] > 0) {
                        newMask = newMask or (1 shl i)
                        avail[target[i] - 'a']--
                    }
                }
                val sub = dp(newMask)
                if (sub != -1) best = minOf(best, sub + 1)
            }
            memo[mask] = if (best == Int.MAX_VALUE) -1 else best
            return memo[mask]
        }
        return dp(0)
    }
}
⭐ 2. Bitmask DP (Bottom-Up BFS-style) — O(2^n * m * n) / O(2^n)
  • Idea: 從空集合出發,逐步嘗試加貼紙擴展狀態
  • Time: O(2^n * m * n)
  • Space: O(2^n)
class Solution {
    fun minStickers(stickers: Array<String>, target: String): Int {
        val n = target.length
        val full = (1 shl n) - 1
        val dp = IntArray(1 shl n) { Int.MAX_VALUE }
        dp[0] = 0

        val stickerCounts = stickers.map { s ->
            IntArray(26).also { cnt -> s.forEach { cnt[it - 'a']++ } }
        }

        for (mask in 0 until (1 shl n)) {
            if (dp[mask] == Int.MAX_VALUE) continue
            for (cnt in stickerCounts) {
                val avail = cnt.copyOf()
                var newMask = mask
                for (i in 0 until n) {
                    if (newMask and (1 shl i) == 0 && avail[target[i] - 'a'] > 0) {
                        newMask = newMask or (1 shl i)
                        avail[target[i] - 'a']--
                    }
                }
                dp[newMask] = minOf(dp[newMask], dp[mask] + 1)
            }
        }
        return if (dp[full] == Int.MAX_VALUE) -1 else dp[full]
    }
}

🔑 Takeaways