CF533F Encoding 题解

在博客园观看本文

题目链接CF533F Encoding

提示1:

   O ( 2 6 2 ∗ n ) \mathcal O(26^2*n) O(262n) 的算法可通过。常用的几种字符串匹配算法kmp,AC自动机,哈希都可以解决该问题 (后两者可以优化到 O ( 26 ∗ n ) \mathcal O(26*n) O(26n) )。

提示2:

  将文本串 S S S 和模式串 T T T 中的 26 26 26 个字母分开考虑,各自匹配。

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

  枚举每一种转换,记为 ( x , y ) , x , y ∈ { ′ a ′ , ′ b ′ , ⋯   , ′ z ′ } (x,y),x,y\in\{'a','b',\cdots,'z'\} (x,y),x,y{a,b,,z} x x x 可以等于 y y y ,即不转换。如果该转换在某区间 S [ l , r ] S[l,r] S[l,r] 是与 T T T 匹配的,则说明在 S [ l , r ] S[l,r] S[l,r] 中每个 x x x 位置和 T T T y y y 位置一一对应,每个 y y y 位置和 T T T x x x 位置一一对应,而且不会有另一个关于 x x x 的转换使得该区间匹配。 若对于 S [ l , r ] S[l,r] S[l,r] 段上 x x x 的任何一种转换都不能与 T T T 匹配,则说明该段无法匹配;否则在该区间起点打标记,表示该转换匹配成功。对于 T T T 任何一个字符 x x x 都需要有转换在 S [ l , r ] S[l,r] S[l,r] 中匹配,才说明 S [ l , r ] S[l,r] S[l,r] 是与 T T T 匹配的

  具体实现时令 T T T x , y x,y x,y 互换,其他位置替换为 0 0 0 S S S 中除 x , y x,y x,y 以外的位置置为 0,其它保持不变,这样可以用kmp判断转换 ( x , y ) (x,y) (x,y) 是否与 S S S 上某一段匹配。例如:

原字符串转换 ( b , c ) (b,c) (b,c) ,变成只含 0 , b , c 0,b,c 0,b,c 的序列(只在 T T T 转换 ( b , c ) (b,c) (b,c)
S S S a b c a b abcab abcab 0   b   c   0   b 0~b~c~0~b 0 b c 0 b
T T T a c b a c acbac acbac 0   b   c   0   b 0~b~c~0~b 0 b c 0 b

  这里 a , b , c a,b,c a,b,c 都能找到转换与 S S S 匹配( a a a 不转换就可以匹配),而对于 S , T S,T S,T 都没出现的字母,任意转换都是匹配的,即会把两个序列都变成全 0 0 0 ,肯定是匹配的。

C o d e : Code: 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;
}

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

  在kmp匹配结束后,有一个 O ( 26 ∗ n ) \mathcal O(26*n) O(26n) 的判断循环,因为在kmp中已经求出 p i , j p_{i,j} pi,j 的值了,所以对于每个 i i i ,有 26 26 26 O ( 1 ) \mathcal O(1) O(1) 的判断 。我们考虑,哈希也可以 O ( 1 ) \mathcal O(1) O(1) 的判断两个串是否匹配,这里只要将 26 26 26 个字母分别求哈希值再求和匹配就可以了,所以哈希是可以将整体复杂度优化到 O ( 26 ∗ n ) \mathcal O(26*n) O(26n) 的。

  具体实现时需要求出一个特殊的数组,我们记为 n x t i , j nxt_{i,j} nxti,j ,表示 S [ i , ∣ s ∣ ] S[i,|s|] S[i,s] 上第一次出现字符 j j j 的下标。这样在我们判断 S [ i , i + ∣ t ∣ − 1 ] S[i,i+|t|-1] S[i,i+t1] 这段区间是否可以通过转换 j j j 使得与 T T T 匹配时,我们先通过 n x t i , j nxt_{i,j} nxti,j 获得 j j j 下标,如果它在 [ i , i + ∣ t ∣ − 1 ] [i,i+|t|-1] [i,i+t1] 这个区间上,说明它在 T T T 串对应位置有一个字符 y y y 需要与它匹配,如果我们把 [ i , i + ∣ t ∣ − 1 ] [i,i+|t|-1] [i,i+t1] 上的 j j j 全部替换为 y y y ,其他字符置为 0 0 0 。对所有的 26 26 26 个字符都这样处理,他们分别计算的哈希值加起来与 T T T 的哈希值比较,就可以知道 S [ i , i + ∣ t ∣ − 1 ] S[i,i+|t|-1] S[i,i+t1] 是否可以和 T T T 匹配了。这样对于每个 i i i 26 26 26 次计算都是 O ( 1 ) \mathcal O(1) O(1) 的。注意对于同一区间上的字符转换不能有交集,即两个关于 x x x 的转换 ( x , y ) , ( x , z ) , y ≠ z (x,y),(x,z),y\not=z (x,y),(x,z),y=z 不能同时存在,代码中用 v i s vis vis 记录每个字符的转换对象,每个字符的 v i s j vis_j visj 只能被赋值一次因为比较懒没写双哈希

C o d e : Code: 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;
}

O ( 26 ∗ n ) \mathcal O(26*n) O(26n) 的AC自动机匹配

  思路来自 【题解】Codeforces533F. Encoding 按字符分解,AC自动机 。其实与哈希匹配思想是一致的,都是想要找到一种方式可以做到对于每个起点 i i i ,进行 26 26 26 次复杂度只有 O ( 1 ) \mathcal O(1) O(1) 的判断。我们把 T T T 串分成 26 26 26 01 01 01 串(不计入全 0 0 0 串),构建AC自动机。再将 S S S 串分成 26 26 26 01 01 01 串,在AC自动机上作匹配,每次匹配到一个 T T T 串末尾,就在该位置标记匹配的字符,对于某个 i i i ,要求 T T T 26 26 26 个字符 j j j 都能被匹配到末尾(除 j j j 0 0 0 串外),才说明该段和 T T T 匹配。例如:

原字符串 a a a 01 01 01 b b b 01 01 01 c c c 01 01 01
S S S a b c a b abcab abcab 1   0   0   1   0 1~0~0~1~0 1 0 0 1 0 0   1   0   0   1 0~1~0~0~1 0 1 0 0 1 0   0   1   0   0 0~0~1~0~0 0 0 1 0 0
T T T a c b a c acbac acbac 1   0   0   1   0 1~0~0~1~0 1 0 0 1 0 0   0   1   0   0 0~0~1~0~0 0 0 1 0 0 0   1   0   0   1 0~1~0~0~1 0 1 0 0 1

  这里 S S S a a a 串会匹配到AC自动机上对应 T T T a a a 串末端, S S S b b b 串会匹配到AC自动机上对应 T T T c c c 串末端, c c c 串会匹配到AC自动机上对应 T T T b b b 串末端,其他全为 0 0 0 的串不插入AC自动机,所以 S S S T T T 是匹配的。

AC自动机构建复杂度是 O ( 26 ∗ n ) \mathcal O (26*n) O(26n) 的,匹配时无需暴力跳fail,对于 26 26 26 个串的匹配也是 O ( 26 ∗ n ) \mathcal O (26*n) O(26n)

C o d e : Code: 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;
}

  这道题可以用多种算法尝试解决,是一道很全面的题目。各算法运行结果如下(空间换时间了):

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ailanxier

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值