691. 貼紙拼詞

困難 哈希表 動態規劃

思路

給定幾張貼紙,上面寫了一些英文單字,再給定目標字串 targettarget ,問至少要幾張貼紙,才能將目標字串給組合出來。
貼紙有無限張,並且可以隨機裁切,比如 abkabk 可以拿來拼 bcabca 中的兩個字元。


因為貼紙可以隨意裁切,因此順序並不重要,只有每個字元的數量才重要。 假如現在目標字串是 aaabbbcckaaabbbcck ,兩張貼紙: aabb,ccddkaabb,ccddk

  • aabbaabb ,它可以滿足四個字元。
  • 接下來的問題,變成 abcckabcck 需要幾張貼紙滿足條件,假設答案是 x1x_1
  • ccddkccddk ,它可以滿足三個字元。
  • 接下來的問題,變成 aabbaabb 需要幾張貼紙滿足條件,假設答案是 x2x_2

對於 abcckabcckaabbkaabbk 這兩個子問題,我們能用完全相同的方式去解,
因此 queue 當中擺放的,是當前要處理的字串,一直到處理的字串是空,就代表到達目標狀況了 ,此時在第幾次展開,就代表使用了幾張貼紙。


優化方面,在上面的例子當中,第一步的展開,我們能查看目標字串的第一個字元,這時是 aa
既然不論如何都需要消掉這個 aa ,在這步當中,我們就只要把包含 aa 的字串 aabbaabb 拿去配對就好,而 ccddkccddk 就不用配,來減少每次分支的數量。

程式碼

記憶化搜索

class Solution {
private:
    static const int MX = 401;
    string q[MX];
    string f(string& cur, string& s) { // 把 s 貼紙用在 cur 上
        // 兩個字串都已經排序,可利用這個特性
        string res;
        int i = 0, j = 0;
        while(i < cur.length()) {
            if(j == s.length()) { // 無法消除
                res += cur[i++];
            }
            else {
                if(cur[i] == s[j]) {
                    i++;
                    j++;
                }
                else if(cur[i] > s[j]) { // 兩個不同,若 cur[i] 比較大, 移動 j
                    j++;
                }
                else { // cur[i] 比較小,沒希望了,不可能配對
                    res += cur[i++];
                }
            }
        }
        return res;
    }
public:
    int minStickers(vector<string>& stickers, string target) {
        int n = stickers.size();
        ranges::sort(target);
        vector<vector<string>> graph(26); // 紀錄能處理特定字元的單字
        unordered_map<string, bool> visited; // 紀錄那些狀態有被走訪過,先被走訪的,一定要比後面被走訪的答案來的好
        visited[target] = true;
        for(auto& s : stickers) {
            ranges::sort(s);
            for(int i = 0; i < s.length(); i++) {
                if(i == 0 || s[i - 1] != s[i]) { // 避免掉重複字元            
                    graph[s[i] - 'a'].push_back(s); // s 包含字元 c, 可以用來消除字元 c 
                }
            }
        }
        int step = 0;
        int l = 0, r = 0;
        q[r++] = target;
        while(l < r) {
            int size = r - l;
            while(size--) {
                string cur = q[l++];
                for(string& s : graph[cur[0] - 'a']) {
                    // 兩個字串都經過排序
                    string next = f(cur, s);
                    if(next == "") { // 到達目標狀態,返回步數
                        return step + 1;
                    }
                    if(!visited[next]) { // 沒來到過這個狀態,需要後續計算
                        visited[next] = true;
                        q[r++] = next;
                    }
                }
            }
            step++;
        }
        return -1;
    }
};

複雜度分析

  • 時間複雜度:O(2m×n×m)O(2^m\times{n}\times{m})
  • 空間複雜度:O(2m×m)O(2^m\times{m})

顯示設定

背景線條
顯示背景網格線條
懸停發光
滑鼠懸停時顯示霓虹效果
聚光燈
跟隨滑鼠的聚光燈效果
背景透明度
開啟透明玻璃效果
主題顏色
自訂主要顏色