Description
給定
n種貼紙(每種無限供應),每張貼紙上有若干小寫字母。要拼出目標字串target,最少需要幾張貼紙?如果無法拼出,回傳 -1。
Example:
Input: stickers = [“with”,“example”,“science”], target = “thehat” Output: 3
Intuition#
用 bitmask 表示 target 中哪些字元已被覆蓋,BFS/DP 找最少貼紙數。
- target 最長 15 個字元,可以用 bitmask 表示收集狀態
dp[mask]= 覆蓋 mask 所代表的字元所需的最少貼紙數- 對每個狀態嘗試每張貼紙,看能覆蓋哪些尚未收集的字元
- 優化:每次只需嘗試能覆蓋當前 mask 中第一個未收集字元的貼紙
Approaches#
1: Bitmask DP (Top-Down)#
- 概念: 記憶化搜尋,每個 mask 狀態嘗試每張貼紙
- 時間複雜度:
O(2^n * m * n)其中 n = target 長度, m = 貼紙數 - 空間複雜度:
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)
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#
- Pattern: Bitmask DP — 用位元遮罩表示子集的收集狀態
- 關鍵技巧: 剪枝策略 — 只嘗試能覆蓋「第一個未收集字元」的貼紙,大幅減少搜索空間