CF533F Encoding 題解

題目鏈接CF533F Encoding

提示1:

  \(\mathcal O(26^2*n)\) 的演算法可通過。常用的幾種字元串匹配演算法kmp,AC自動機,哈希都可以解決該問題 (後兩者可以優化到 \(\mathcal O(26*n)\) )。

提示2:

  將文本串 \(S\) 和模式串 \(T\) 中的 \(26\) 個字母分開考慮,各自匹配。

\(\mathcal O(26^2*n)\) 的kmp匹配

  枚舉每一種轉換,記為 \((x,y),x,y\in\{‘a’,’b’,\cdots,’z’\}\)\(x\) 可以等於 \(y\) ,即不轉換。如果該轉換在某區間 \(S[l,r]\) 是與 \(T\) 匹配的,則說明在 \(S[l,r]\) 中每個 \(x\) 位置和 \(T\)\(y\) 位置一一對應,每個 \(y\) 位置和 \(T\)\(x\) 位置一一對應,而且不會有另一個關於 \(x\) 的轉換使得該區間匹配。 若對於 \(S[l,r]\) 段上 \(x\) 的任何一種轉換都不能與 \(T\) 匹配,則說明該段無法匹配;否則在該區間起點打標記,表示該轉換匹配成功。對於 \(T\) 任何一個字元 \(x\) 都需要有轉換在 \(S[l,r]\) 中匹配,才說明 \(S[l,r]\) 是與 \(T\) 匹配的

  具體實現時令 \(T\)\(x,y\) 互換,其他位置替換為 \(0\)\(S\) 中除 \(x,y\) 以外的位置置為 0,其它保持不變,這樣可以用kmp判斷轉換 \((x,y)\) 是否與 \(S\) 上某一段匹配。例如:

原字元串 轉換 \((b,c)\) ,變成只含 \(0,b,c\) 的序列(只在 \(T\) 轉換 \((b,c)\)
\(S\) \(abcab\) \(0~b~c~0~b\)
\(T\) \(acbac\) \(0~b~c~0~b\)

  這裡 \(a,b,c\) 都能找到轉換與 \(S\) 匹配(\(a\) 不轉換就可以匹配),而對於 \(S,T\) 都沒出現的字母,任意轉換都是匹配的,即會把兩個序列都變成全 \(0\) ,肯定是匹配的。

\(Code:\)

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int sl,tl,nxt[N],ss[N],tt[N];
bool p[N][27];//p[i][j]標記S[i,i+tl-1]這段上是否有關於x的轉換使得與T串匹配
char s[N],t[N];
vector<int>ans;

int main() {
    scanf("%d %d %s %s",&sl,&tl,s+1,t+1);
    for(int x = 'a'; x <= 'z'; x++){
        for(int y = x; y <= 'z'; y++){
            //t中x,y互換
            for(int i = 1;i <= tl;i++) {
                if(t[i] == x) tt[i] = y;
                else if(t[i] == y) tt[i] = x;
                else tt[i] = 0;
            }
            for(int i = 1; i <= sl; i++) ss[i] = ( (s[i] == y)||(s[i] == x) )? s[i] : 0;

            //kmp匹配
            for(int i = 2,j = 0; i <= tl; i++){
                while(j && tt[i] != tt[j + 1]) j = nxt[j];
                if(tt[i] == tt[j + 1]) nxt[i] = ++j;
                else nxt[i] = 0;
            }
            for(int i = 1,j = 0; i <= sl; i++){
                while(j && ss[i] != tt[j + 1]) j = nxt[j];
                if(ss[i] == tt[j + 1]) ++j;
                if(j == tl) {
                    p[i-tl+1][x-'a'] = 1;
                    p[i-tl+1][y-'a'] = 1;
                    j = nxt[j];
                }
            }
        }
    }
    //O(26*n) 的判斷:S[i,i+tl-1]是否可以和轉換後的T匹配,要滿足t的每個字元j都有轉換與S[i,i+tl-1]匹配
    for(int i = 1;i <= sl-tl+1;i++) {
        int flag = 1;
        for(int j = 0;j < 26;j++) if(!p[i][j]) { flag = 0;break;}
        if(flag) ans.push_back(i);
    }
    printf("%d\n",ans.size());
    for(auto it : ans) printf("%d ",it);
    return 0;
}

\(\mathcal O(26*n)\) 的哈希匹配

  在kmp匹配結束後,有一個 \(\mathcal O(26*n)\) 的判斷循環,因為在kmp中已經求出 \(p_{i,j}\) 的值了,所以對於每個 \(i\) ,有 \(26\)\(\mathcal O(1)\) 的判斷 。我們考慮,哈希也可以 \(\mathcal O(1)\) 的判斷兩個串是否匹配,這裡只要將 \(26\) 個字母分別求哈希值再求和匹配就可以了,所以哈希是可以將整體複雜度優化到 \(\mathcal O(26*n)\) 的。

  具體實現時需要求出一個特殊的數組,我們記為 \(nxt_{i,j}\) ,表示 \(S[i,|s|]\) 上第一次出現字元 \(j\) 的下標。這樣在我們判斷 \(S[i,i+|t|-1]\) 這段區間是否可以通過轉換 \(j\) 使得與 \(T\) 匹配時,我們先通過 \(nxt_{i,j}\) 獲得 \(j\) 下標,如果它在 \([i,i+|t|-1]\) 這個區間上,說明它在 \(T\) 串對應位置有一個字元 \(y\) 需要與它匹配,如果我們把 \([i,i+|t|-1]\) 上的 \(j\) 全部替換為 \(y\) ,其他字元置為 \(0\) 。對所有的 \(26\) 個字元都這樣處理,他們分別計算的哈希值加起來與 \(T\) 的哈希值比較,就可以知道 \(S[i,i+|t|-1]\) 是否可以和 \(T\) 匹配了。這樣對於每個 \(i\)\(26\) 次計算都是 \(\mathcal O(1)\) 的。注意對於同一區間上的字元轉換不能有交集,即兩個關於 \(x\) 的轉換 \((x,y),(x,z),y\not=z\) 不能同時存在,程式碼中用 \(vis\) 記錄每個字元的轉換對象,每個字元的 \(vis_j\) 只能被賦值一次因為比較懶沒寫雙哈希

\(Code:\)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 5;
const int base = 131;
const int mod = 19260817;
int sl,tl,nxt[N][26];
ll T_hash,s_hash[N][26],fac[N];
int vis[27];
vector<int>ans;
char s[N],t[N];

int main() {
    scanf("%d %d %s %s",&sl,&tl,s+1,t+1);
    fac[0] = 1;
    for(int i = 1;i <= sl;i++) fac[i] = fac[i-1] * base % mod;

    //預處理出nxt數組,記錄i後第一個字元j的下標
    for(int j = 0;j < 26;j++) nxt[sl+1][j] = sl+1;
    for(int i = sl;i >= 1;i--)
        for(int j = 0;j < 26;j++){
            if(s[i] == j+'a') nxt[i][j] = i;
            else nxt[i][j] = nxt[i+1][j];
        }

    //求出T串hash值
    for(int i = 1;i <= tl;i++) T_hash = (T_hash * base + (t[i]-'a') ) % mod;

    //求出S串26個字元分開計算的hash值,注意這裡是將為j的字元置為1,其他置為0作哈希,之後有(j,y)的轉換乘上y即可(哈希滿足乘法分配律)
    for(int i = 1;i <= sl;i++)
        for(int j = 0;j < 26;j++){
            if(s[i] == j+'a') s_hash[i][j] = (s_hash[i-1][j] * base + 1) % mod;
            else s_hash[i][j] = s_hash[i-1][j] * base % mod;
        }

    for(int i = 1;i <= sl-tl+1;i++) {
        ll tot_hash = 0;
        memset(vis,-1,sizeof vis); //-1表示未被賦值過
        for(int j = 0;j < 26;j++){
            int p = nxt[i][j];
            if(p > i + tl - 1) continue;
            int y = t[p-i+1]-'a';     //找到在T串對應位置的字元

            //只有之前x,y都沒有被轉換過才能賦值
            if(vis[j] == -1 && vis[y] == -1) vis[y] = j,vis[j] = y;
            tot_hash = (tot_hash + vis[j] * ((s_hash[i+tl-1][j] - s_hash[i-1][j] * fac[tl]) % mod + mod) ) % mod;
        }
        if(tot_hash == T_hash) ans.push_back(i);
    }
    printf("%d\n",ans.size());
    for(auto it :ans) printf("%d ",it);
    return 0;
}

\(\mathcal O(26*n)\) 的AC自動機匹配

  思路來自 【題解】Codeforces533F. Encoding 按字元分解,AC自動機 。其實與哈希匹配思想是一致的,都是想要找到一種方式可以做到對於每個起點 \(i\) ,進行 \(26\) 次複雜度只有 \(\mathcal O(1)\) 的判斷。我們把 \(T\) 串分成 \(26\)\(01\) 串(不計入全 \(0\) 串),構建AC自動機。再將 \(S\) 串分成 \(26\)\(01\) 串,在AC自動機上作匹配,每次匹配到一個 \(T\) 串末尾,就在該位置標記匹配的字元,對於某個 \(i\) ,要求 \(T\)\(26\) 個字元 \(j\) 都能被匹配到末尾(除 \(j\)\(0\) 串外),才說明該段和 \(T\) 匹配。例如:

原字元串 \(a\)\(01\) \(b\)\(01\) \(c\)\(01\)
\(S\) \(abcab\) \(1~0~0~1~0\) \(0~1~0~0~1\) \(0~0~1~0~0\)
\(T\) \(acbac\) \(1~0~0~1~0\) \(0~0~1~0~0\) \(0~1~0~0~1\)

  這裡 \(S\)\(a\) 串會匹配到AC自動機上對應 \(T\)\(a\) 串末端, \(S\)\(b\) 串會匹配到AC自動機上對應 \(T\)\(c\) 串末端, \(c\) 串會匹配到AC自動機上對應 \(T\)\(b\) 串末端,其他全為 \(0\) 的串不插入AC自動機,所以 \(S\)\(T\) 是匹配的。

AC自動機構建複雜度是 \(\mathcal O (26*n)\) 的,匹配時無需暴力跳fail,對於 \(26\) 個串的匹配也是 \(\mathcal O (26*n)\)

\(Code:\)

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int sl,tl,tt[N],en[N*26], match[27];
bool has[27];
char s[N],t[N];
vector<int>ans;
int trie[N*26][2],cnt,fail[N*26],now[26];

void insert(int o){
    int now = 0;
    for (int i = 1,c; i <= tl;i++){
        c = tt[i];
        if(trie[now][c] == 0) trie[now][c] = ++cnt;
        now = trie[now][c];
    }
    en[now] = o;
}

void build(){
    queue<int> q;
    for (int i = 0; i < 2;i++) if(trie[0][i]) q.push(trie[0][i]);
    while(!q.empty()){
        int now = q.front();q.pop();
        for (int i = 0; i < 2;i++){
            if(trie[now][i]){
                q.push(trie[now][i]);
                fail[trie[now][i]] = trie[fail[now]][i];
            }else
                trie[now][i] = trie[fail[now]][i];
        }
    }
}

int main() {
    scanf("%d %d %s %s",&sl,&tl,s+1,t+1);
    memset(en,-1,sizeof en);
    for(int j = 0;j < 26;j++){
        for(int i = 1; i <= tl; i++)
            has[j] |= tt[i] = (t[i] == j + 'a');
        if(has[j]) insert(j); //不是全0串才插入AC自動機
    }
    build();
    for(int i = 1;i <= sl;i++){
        int flag = 0;
        memset(match,-1,sizeof match);
        for(int j = 0;j < 26;j++){
            int c = s[i]-'a';
            now[j] = trie[now[j]][c == j];
            if(en[now[j]] != -1) match[en[now[j]]] = j;//記錄T末端字元en[now[j]]匹配到S的字元j
        }
        for(int j = 0;j < 26;j++)
            if(has[j]){
                if(match[j] == -1) {flag = 1;break;}
                int y = match[j];
                //在T串同時含j,y的情況下,match[j]=y,match[y]=j才滿足變換後匹配
                if(match[y] != j && has[y]) {flag = 1;break;}  
            }
        if(!flag) ans.push_back(i-tl+1);
    }
    printf("%d\n",ans.size());
    for(auto it : ans) printf("%d ",it);
    return 0;
}

  這道題可以用多種演算法嘗試解決,是一道很全面的題目。各演算法運行結果如下(空間換時間了):

Tags: