1. 程式人生 > >[LeetCode] Stickers to Spell Word 貼片拼單詞

[LeetCode] Stickers to Spell Word 貼片拼單詞

We are given N different types of stickers. Each sticker has a lowercase English word on it.

You would like to spell out the given target string by cutting individual letters from your collection of stickers and rearranging them.

You can use each sticker more than once if you want, and you have infinite quantities of each sticker.

What is the minimum number of stickers that you need to spell out the target? If the task is impossible, return -1.

Example 1:

Input:

["with", "example", "science"], "thehat"

Output:

3

Explanation:

We can use 2 "with" stickers, and 1 "example" sticker.
After cutting and rearrange the letters of those stickers, we can form the target "thehat".
Also, this is the minimum number of stickers necessary to form the target string.

Example 2:

Input:

["notice", "possible"], "basicbasic"

Output:

-1

Explanation:

We can't form the target "basicbasic" from cutting letters from the given stickers.

Note:

  • stickers has length in the range [1, 50].
  • stickers consists of lowercase English words (without apostrophes).
  • target has length in the range [1, 15], and consists of lowercase English letters.
  • In all test cases, all words were chosen randomly from the 1000 most common US English words, and the target was chosen as a concatenation of two random words.
  • The time limit may be more challenging than usual. It is expected that a 50 sticker test case can be solved within 35ms on average.

這道題給了我們N個貼片,每個貼片上有一個小寫字母的單詞,給了我們一個目標單詞target,讓我們通過剪下貼片單詞上的字母來拼出目標值,每個貼片都有無數個,問我們最少用幾個貼片能拼出目標值target,如果不能拼出來的話,就返回-1。這道題博主最開始嘗試用貪婪演算法,結果發現不行,有網友留言提示說是多重揹包問題,然後去論壇上看大神們的解法,果然都是用DP做的,之前曾有網友推薦過一個“揹包九講”的帖子,大概掃過幾眼,真是叼到飛起啊,博主希望有時間也能總結一下。先來看這道題吧,既然是用DP來做,那麼就需要用dp陣列了,我們使用一個一維的dp陣列,其中dp[i]表示組成第i個子集合所需要的最少的sticker的個數,注意這裡是子集合,而不是子串。長度為n的字串共有2的n次方個子集合,比如字串"ab",就有4個子集合,分別是 "", "a", "b", "ab"。字串"abc"就有8個子集合,如果我們用0到7來分別對應其子集合,就有:

     abc   subset 
0    000     ""
1    001     c
2    010     b
3    011     bc
4    100     a
5    101     ac
6    110     bb
7    111     abc

我們可以看到0到7的二進位制數的每一位上的0和1就相當於mask,是1的話就選上該位對應的字母,000就表示都不選,就是空集,111就表示都選,就是abc,那麼只要我們將所有子集合的dp值都算出來,最後返回dp陣列的最後一個位置上的數字,就是和目標子串相等的子集合啦。我們以下面這個簡單的例子來講解:

stickers = ["ab", "bc", "ac"], target = "abc"

之前說了abc的共有8個子集合,所以dp陣列的長度為8,除了dp[0]初始化為0之外,其餘的都初始化為INT_MAX,然後我們開始逐個更新dp陣列的值,我們的目標是從sticker中取出字元,來拼出子集合,所以如果當前遍歷到的dp值仍為INT_MAX的話,說明該子集合無法被拼出來,自然我們也無法再其基礎上去拼別打子集合了,直接跳過。否則我們就來遍歷所有的sticker,讓變數cur等於i,說明此時是在第i個子集合的基礎上去reach其他的子集合,我們遍歷當前sticker的每一個字母,對於sticker的每個字母,我們都掃描一遍target的所有字元,如果target裡有這個字元,且該字元未出現在當前cur位置的子集合中,則將該字元加入子集合中。什麼意思呢,比如當前我們的cur是3,二進位制為011,對應的子集合是"bc",此時如果我們遍歷到的sticker是"ab",那麼遇到"a"時,我們知道target中是有"a"的,然後我們看"bc"中包不包括"a",判斷方法是看 (cur >> k) & 1 是否為0,這可以乍看上去不太好理解,其實不難想,因為我們的子集合是跟二進位制對應的,"bc"就對應著011,第一個0就表示"a"缺失,所以我們想看哪個字元,就提取出該字元對應的二進位制位,提取方法就是 cur >> k,表示cur向右移動k位,懂位操作Bit Manipulation的童鞋應該不難理解,提出出來的值與上1就知道該位是0還是1了,如果是0,表示缺失,那麼把該位變為1,通過 cur |= 1 << k來實現,那麼此時我們的cur就變位7,二進位制為111,對應的子集合是"abc",更新此時的dp[cur]為 dp[cur] 和 dp[i] + 1 中的較大值即可,最後迴圈結束後,如果"abc"對應的dp值還是INT_MAX,就返回-1,否則返回其dp值,參見程式碼如下:

解法一:

class Solution {
public:
    int minStickers(vector<string>& stickers, string target) {
        int n = target.size(), m = 1 << n;
        vector<int> dp(m, INT_MAX);
        dp[0] = 0;
        for (int i = 0; i < m; ++i) {
            if (dp[i] == INT_MAX) continue;
            for (string &sticker : stickers) {
                int cur = i;
                for (char c : sticker) {
                    for (int k = 0; k < n; ++k) {
                        if (target[k] == c && !((cur >> k) & 1)) {
                            cur |= 1 << k;
                            break;
                        }
                    }
                }
                dp[cur] = min(dp[cur], dp[i] + 1);
            }
        }
        return dp[m - 1] == INT_MAX ? -1 : dp[m - 1];
    }
};

下面這種解法是帶記憶陣列memo的遞迴解法,可以看作是DP解法的遞迴形式,核心思想都一樣。只不過dp陣列換成了雜湊Map,建立子集合跟最小使用的sticker的個數之間的對映,初始化空集為0,我們首先統計每個sticker的各個字母出現的頻率,放到對應的二維陣列freq中,然後就是呼叫遞迴函式。在遞迴函式中,首先判斷,如果target已經在memo中,直接返回其值。否則我們開始計算,首先統計出此時的target字串的各個字母出現次數,然後我們遍歷統計所有sticker中各個字母出現次數的陣列freq,如果target字串的第一個字母不在當前sticker中,我們直接跳過,注意遞迴函式中的target字串不是原始的字串,我們心間一個臨時字串t,然後我們遍歷target字串中存在的字元,如果target中的某字元存在的個數多於sticker中對應的字元,那麼將多餘的部分存在字串t中,表示當前sticker無法拼出的字元,交給下一個遞迴函式來處理,我們看再次呼叫遞迴函式的結果ans,如果不為-1,說明可以拼出剩餘的那些字元,那麼此時我們的res更新為ans+1,迴圈退出後,此時我們的res就應該是組成當前遞迴函式中的target串的最少貼片數,更新dp[target]值,如果res是INT_MAX,說明無法拼出,更新為-1,否則更新為res,參見程式碼如下:

解法二:

class Solution {
public:
    int minStickers(vector<string>& stickers, string target) {
        int N = stickers.size();
        vector<vector<int>> freq(N, vector<int>(26, 0));
        unordered_map<string, int> memo;
        memo[""] = 0;
        for (int i = 0; i < N; ++i) {
            for (char c : stickers[i]) ++freq[i][c - 'a'];
        }
        return helper(freq, target, memo);
    }
    int helper(vector<vector<int>>& freq, string target, unordered_map<string, int>& memo) {
        if (memo.count(target)) return memo[target];
        int res = INT_MAX, N = freq.size();
        vector<int> cnt(26, 0);
        for (char c : target) ++cnt[c - 'a'];
        for (int i = 0; i < N; ++i) {
            if (freq[i][target[0] - 'a'] == 0) continue;
            string t = "";
            for (int j = 0; j < 26; ++j) {
                if (cnt[j] - freq[i][j] > 0) t += string(cnt[j] - freq[i][j], 'a' + j);
            }
            int ans = helper(freq, t, memo);
            if (ans != -1) res = min(res, ans + 1);
        }
        memo[target] = (res == INT_MAX) ? -1 : res;
        return memo[target];
    }
};

類似題目:

參考資料: