936. 戳印序列

困難 貪心 隊列 字串 拓撲排序

思路

  • 蓋章時,會把前面的東西覆蓋掉。
  • 如果蓋章可以得到target,那麼target必定包含stamp字串。
  • 先蓋的東西如果跟target有不相同之處,就需要用後面的蓋章去取消掉它。

用範例來說,"ababc"能找到"abc",那麼最後一步肯定是蓋2位置,上一步是"ab???",此時有一個字串"ab?"stamp只差一個字元,而這個字元會被後面覆蓋,因此可以蓋零位置。

有點像時光倒流

再舉個例子,target="aabcbc"stamp="abc"

蓋第零個位置
aababc××\begin{array}{} a & a & b \\ a & b & c \\ \checkmark & \times & \times \end{array}

蓋第一個位置
abcabc\begin{array}{} a & b & c \\ a & b & c \\ \checkmark & \checkmark & \checkmark \end{array}

蓋第二個位置
bcbabc×××\begin{array}{} b & c & b \\ a & b & c \\ \times & \times & \times \end{array}

蓋第三個位置
cbcabc×\begin{array}{} c & b & c \\ a & b & c \\ \times & \checkmark & \checkmark \end{array}

此時最佳位置是第一個,因此最後一步蓋一位置,target="a???bc"
因為會蓋位置一,蓋其他位置的錯誤狀態因此更新。

蓋第零個位置
a??abc\begin{array}{} a & ? & ? \\ a & b & c \\ \checkmark & \checkmark & \checkmark \end{array}

蓋第二個位置
??babc×\begin{array}{} ? & ? & b \\ a & b & c \\ \checkmark & \checkmark & \times \end{array}

蓋第三個位置
?bcabc\begin{array}{} ? & b & c \\ a & b & c \\ \checkmark & \checkmark & \checkmark \end{array}

這時可以蓋零或是三位置,這裡蓋零,target="????bc",再次更新其他位置的狀態

蓋第二個位置
??babc×\begin{array}{} ? & ? & b \\ a & b & c \\ \checkmark & \checkmark & \times \end{array}

蓋第三個位置
?bcabc\begin{array}{} ? & b & c \\ a & b & c \\ \checkmark & \checkmark & \checkmark \end{array}

再蓋第三個位置,target = "??????",更新狀態。

蓋第二個位置
?bcabc\begin{array}{} ? & b & c \\ a & b & c \\ \checkmark & \checkmark & \checkmark \end{array}

把蓋章的順序倒過來,就得到答案 [2,3,0,1][2,3,0,1] 了。 整個過程就是在做拓樸排序,入度就是打叉的個數,當入度為 0 時,這個位置就可以蓋章。 剩下的細節,在程式碼當中有註解。

程式碼

1. 拓樸排序 鄰接表

class Solution {
public:
    vector<int> movesToStamp(string stamp, string target) {
        // 如果 stamp.size() = 10, target.size() = 9, 那麼一共有兩個位置可以蓋章
        int m = stamp.length();
        int n = target.length();
        int len = n - m + 1; // 可蓋章的位置
        vector<vector<int>> graph(n); // 紀錄鄰居 
        vector<int> indegree(len);
        for(int i = 0; i < len; i++) {
            int cnt = 0;
            for(int j = 0; j < m; j++) {
                if(stamp[j] != target[i + j]) { // 計算不相同之處
                    cnt++;
                    // 當 i+j 的錯誤被修正時,蓋在位置 i 的印章入度會減一(解鎖)
                    graph[i + j].push_back(i);
                }
            }
            indegree[i] = cnt; // 紀錄每個位置蓋章的入度,也就是字串不同的個數
        }
        queue<int> q;
        for(int i = 0; i < len; i++) {
            if(indegree[i] == 0) {
                q.push(i);
            }
        }
        vector<int> res;
        vector<bool> visited(n, false); // 紀錄哪些位置被解鎖了(範例中的標為?),避免重複解鎖
        while(!q.empty()) {
            int top = q.front(); q.pop();
            res.push_back(top); // 印章蓋在 top 位置
            for(int i = 0; i < m; i++) { // 後續長度 m 的位置都會受到影響
                if(!visited[top + i]) {
                    visited[top + i] = true;
                    for(int& neighbor : graph[top + i]) { // 受到影響的地方,入度減一
                        if(--indegree[neighbor] == 0) {
                            q.push(neighbor);
                        }
                    }
                }
            }
        }
        if(res.size() == len && res.size() <= 10 * n) {
            reverse(res.begin(), res.end());
            return res;
        }
        else {
            return {};
        }
    }
};

2. 拓樸排序 鄰接表 簡化

class Solution {
public:
    vector<int> movesToStamp(string stamp, string target) {
        // 如果 stamp.size() = 10, target.size() = 9, 那麼一共有兩個位置可以蓋章
        int m = stamp.length();
        int n = target.length();
        int len = n - m + 1; // 可蓋章的位置
        vector<vector<int>> graph(n); // 紀錄鄰居 
        vector<int> indegree(len, m); // 預設最大入度
        queue<int> q;
        for(int i = 0; i < len; i++) {
            int cnt = 0;
            for(int j = 0; j < m; j++) {
                if(stamp[j] == target[i + j]) { // 計算不相同之處
                    if(--indegree[i] == 0) { // 入度為 0,可以當最後蓋印章的位置
                        q.push(i);
                    }
                }
                else {
                    // 當 i+j 的錯誤被修正時,蓋在位置 i 的印章入度會減一(解鎖)
                    graph[i + j].push_back(i);                    
                }
            }
        }
        vector<int> res;
        vector<bool> visited(n, false); // 紀錄哪些位置被解鎖了(範例中的標為?),避免重複解鎖
        while(!q.empty()) {
            int top = q.front(); q.pop();
            res.push_back(top); // 印章蓋在 top 位置
            for(int i = 0; i < m; i++) { // 後續長度 m 的位置都會受到影響
                if(!visited[top + i]) {
                    visited[top + i] = true;
                    for(int& neighbor : graph[top + i]) { // 受到影響的地方,入度減一
                        if(--indegree[neighbor] == 0) {
                            q.push(neighbor);
                        }
                    }
                }
            }
        }
        if(res.size() == len && res.size() <= 10 * n) {
            reverse(res.begin(), res.end());
            return res;
        }
        else {
            return {};
        }
    }
};

複雜度分析

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

顯示設定

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