字串學習筆記

Crymx發表於2024-08-21

擴充套件kmp

令z[i]代表i之後的字串與原先字串的最長公共字首

r為目前get到的最大位置,l為對應的左端點

很明顯的狀態轉移

比如現在列舉到了i這個位置

i在[l,r]的範圍內,首先S[l,r]==S[1,r-l+1]

於是S[i,r]==S[i-l+1,r-l+1]

那麼顯然z[i]=min(z[i-l+1],r-i+1) 不能超過長度

假設z[i-l+1]+(i-l+1)-1=r-l+1,也就是z[i]+i-1=r

但是r+1處還可能匹配 因為Sr+1!=Sr-l+2 但不一定不等於字首的Sz[i]+1

然後暴力即可 由於z[i]++的話一定會使r變大

所以是不斷遞增的 while迴圈最多執行n次(執行while迴圈的前提是z[i]+i-1=r)

注意1不能進入迴圈 否則一直z[i]=z[i] 每個i都會執行while迴圈 使得演算法失效

void getz()
{
    z[1]=n;
    int l=0,r=0;
    for(int i=2;i<=n;i++){
        if(i<=r)z[i]=min(z[i-l+1],r-i+1);
        while(i+z[i]<=n&&b[i+z[i]]==b[z[i]+1])z[i]++;
        if(i+z[i]-1>r)r=i+z[i]-1,l=i;
    }
}

AC自動機

fail[i]代表i這個節點的失配指標,即最長公共字尾(不能是自己)

先建立trie樹,然後bfs建立自動機,注意為了防止自己匹配自己,第一層push進佇列即可

void insert(int i){
	string x;
	cin>>x;
	s[i]=x;
	int now=0;
	for(auto j:x){
		if(!tr[now][j-'a'])
			tr[now][j-'a']=++cnt;
		now=tr[now][j-'a'];
	}
	mp[i]=now;
}
void build(){
	queue<int>q;
	for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
	while(!q.empty()){
		int now=q.front();q.pop();
		for(int i=0;i<26;i++){
			if(tr[now][i]){
				fail[tr[now][i]]=tr[fail[now]][i];
				q.push(tr[now][i]);
			}
			else tr[now][i]=tr[fail[now]][i];
		}
	}
}

迴文自動機

由於以s[i]結尾的迴文串必定是由以s[i-1]結尾的迴文串轉移過來

所以建立迴文樹

getfail(x,i)代表找上一位在迴文樹中下標為x,並且開頭前一個與s[i]相等 的下標

即最長迴文字尾

為什麼點要插在最長迴文字尾的後面 :

​ 因為如果不是最長迴文字尾 那麼一定出現過 迴文串中迴文串的性質

0根代表偶數迴文 1根代表奇數迴文

len[1]=-1 --> 保證每次+=2結果正確性

fail指標的求解:

由於最長迴文字尾不能是自己 所以應用fail[pos]求解 而不是pos(因為會直接返回pos 導致return本身),直接根據getfail函式轉移即可

時間複雜度證明:

由於每次插入點都會使得深度+1,深度最多加n次,則while迴圈最多執行n次

ps:第二個有點問題,但是直接用即可,沒人證明

int getfail(int x,int i){
    while(s[i-len[x]-1]!=s[i])x=fail[x];
    return x;
}
void solve()
{
    string s;
    cin>>s;
    int n=s.size(),pre=0;
    s=" "+s;
    len[1]=-1,fail[0]=1;
    for(int i=1;i<=n;i++){
        int pos=getfail(pre,i);
        if(!tr[pos][s[i]-'a']){
            fail[++tot]=tr[getfail(fail[pos],i)][s[i]-'a'];
            tr[pos][s[i]-'a']=tot;
            len[tot]=len[pos]+2;
            cnt[tot]=cnt[fail[tot]]+1;
        }
        pre=tr[pos][s[i]-'a'];
    }
}

half[i]表示以i結尾的最長迴文字尾 且 長度小於等於len[i]/2

for(int i=1;i<=n;i++){
    int pos=getfail(pre,i);
    if(!tr[pos][s[i]-'a']){
        fail[++tot]=tr[getfail(fail[pos],i)][s[i]-'a'];
        tr[pos][s[i]-'a']=tot;
        len[tot]=len[pos]+2;
        if(len[tot]<=2)half[tot]=fail[tot];
        else {
        	int tmp=half[pos];
            while(s[i-1-len[tmp]]!=s[i]||2*len[tmp]+4>len[tot]){
                tmp=fail[tmp];
            }
            half[tot]=tr[tmp][s[i]-'a'];
        }
    }
    pre=tr[pos][s[i]-'a'];
}

字尾陣列

具體詳見程式碼

void get_sa(){
    int m=130;//m是桶最大編號
    vector<int>sa(n+1,0),sa2(2*n+1,0),rk(2*n+1,0),c(max(n,m)+1,0);
    /*
        sa[i]代表排名第i的字尾的起始編號
        sa2[i]代表第二關鍵字排序後排名第i的字尾起始編號
        rk[i]代表以i為起始字尾的排名
        c[i]即為桶 用來存第一關鍵字的個數
    */
    for(int i=1;i<=n;i++)c[rk[i]=s[i]]++;
    //一開始第一關鍵字為s[i] 存進桶即可
    for(int i=1;i<=m;i++)c[i]+=c[i-1];
    //做字首和 這樣即可求出每個第一關鍵字的最大排名
    for(int i=n;i>=1;i--)sa[c[rk[i]]--]=i;
    //按照第一關鍵字排序
    for(int k=1;k<=n;k<<=1){
        int num=0;
        for(int i=n-k+1;i<=n;i++)sa2[++num]=i;
        //由於n-k+1~n的無第二關鍵字 所以排名最靠前
        for(int i=1;i<=n;i++)if(sa[i]>k)sa2[++num]=sa[i]-k;
        //按照sa[i]從小到大遍歷 保證排名從前往後
        //如果sa[i]>k 說明這個可以當作第二關鍵字 則存入sa2中
        for(int i=1;i<=m;i++)c[i]=0;
        //初始化桶
        for(int i=1;i<=n;i++)c[rk[i]]++;
        for(int i=1;i<=m;i++)c[i]+=c[i-1];
        //存第一關鍵字並做字首和
        for(int i=n;i>=1;i--)sa[c[rk[sa2[i]]]--]=sa2[i];
        //倒著遍歷保證排序從後往前
        //rk[sa2[i]]是第二關鍵字排名為i的字尾的第一關鍵字
        //就可以像上面那樣 由於桶字首和已經對第一關鍵字排序
        //所以實質上就是倒著遍歷 每個第一關鍵字不影響 都在一個區間內
        //然後倒著排序第二關鍵字
        swap(rk,sa2);//下面要更新rk陣列,所以做個臨時交換
        rk[sa[1]]=1;//顯然排名為1的字尾的rk是1
        num=1;
        for(int i=2;i<=n;i++){
            rk[sa[i]]=(sa2[sa[i]]==sa2[sa[i-1]]&&sa2[sa[i]+k]==sa2[sa[i-1]+k])?num:++num;
            //如果第一關鍵字和第二關鍵字和上一個均相同,則排名一樣,num無需++
        }
        if(num==n)break;
        //num==n說明所有字尾排序均不相同,排序結束
        m=num;//更新容器最大值
    }
    for(int i=1;i<=n;i++)cout<<sa[i]<<' ';
}

height陣列的求解

void get_height(){
    //height[i]代表LCP(sa[i],sa[i-1])
    height[1]=0;
    int k=0;
    //k代表現在匹配到的長度
    for(int i=1;i<=n;i++){
      	//按字尾進行遍歷 很容易知道height[rk[i]]>=height[rk[i-1]]-1
        int j=sa[rk[i]-1];//上一個排名的字尾起始下標
        if(k)k--;//-1是因為第一個字元失去
        while(s[i+k]==s[j+k])k++;//暴力匹配 k最大是n 總時間複雜度最大為O(n)
        height[rk[i]]=k;//k即為最長公共字首
    }
}

字尾陣列模板

struct SA{
    vector<int>sa,sa2,c,rk,height;
    vector<vector<int>>dp;
    void init(string s,int n){
        int m=150;
        sa=vector<int>(max(n,m)+1,0);
        c=height=sa;
        sa2=rk=vector<int>(n*2+1,0);
        //可能re的點
        dp=vector<vector<int>>(n+1,vector<int>(31,1e9+10));
        for(int i=1;i<=n;i++)c[rk[i]=s[i]]++;
        for(int i=1;i<=m;i++)c[i]+=c[i-1];
        for(int i=n;i>=1;i--)sa[c[rk[i]]--]=i;
        for(int k=1;k<=n;k<<=1){
            int num=0;
            for(int i=n-k+1;i<=n;i++)sa2[++num]=i;
            for(int i=1;i<=n;i++)if(sa[i]>k)sa2[++num]=sa[i]-k;
            for(int i=1;i<=m;i++)c[i]=0;
            for(int i=1;i<=n;i++)c[rk[i]]++;
            for(int i=1;i<=m;i++)c[i]+=c[i-1];
            for(int i=n;i>=1;i--)sa[c[rk[sa2[i]]]--]=sa2[i];
            num=1;swap(rk,sa2);
            rk[sa[1]]=1;
            for(int i=2;i<=n;i++){
                rk[sa[i]]=(sa2[sa[i]]==sa2[sa[i-1]]&&sa2[sa[i]+k]==sa2[sa[i-1]+k])?num:++num;
            }
            m=num;
            if(m==n)break;
        }
        int k=0;
        for(int i=1;i<=n;i++){
            int j=sa[rk[i]-1];
            if(k)k--;
            while(max(i,j)+k<=n&&s[i+k]==s[j+k])k++;
            height[rk[i]]=k;
        }
        for(int i=1;i<=n;i++)dp[i][0]=height[i];
        for(int j=1;(1<<j)<=n;j++){
            for(int i=1;i+(1<<j)-1<=n;i++){
                dp[i][j]=min(dp[i][j-1],dp[i+(1<<j-1)][j-1]);
            }
        }
    }
    int lca(int l,int r){
        l=rk[l],r=rk[r];
        if(l>r)swap(l,r);
        l++;
        int len=lg[r-l+1];
        return min(dp[l][len],dp[r-(1<<len)+1][len]);
    }
};

字尾自動機模板

struct sam{
    vector<vector<int>>tr;
    vector<int>len,fa;
    int tot=1,now=0,lst=1;
    void init(int n){
        tr=vector<vector<int>>(n*2+10,vector<int>(30,0));
        len=vector<int>(n*2+10,0);
        fa=len;
    }
    void insert(char c){
        c-='a';
        now=++tot;
        len[now]=len[lst]+1;
        while(!tr[lst][c]&&lst){
            tr[lst][c]=now;
            lst=fa[lst];
        }
        if(lst==0)fa[now]=1;
        else {
            int x=tr[lst][c];
            if(len[x]==len[lst]+1){
                fa[now]=x;
            }
            else {
                int y=++tot;
                tr[y]=tr[x];
                fa[y]=fa[x];
                len[y]=len[lst]+1;
                fa[x]=fa[now]=y;
                while(tr[lst][c]==x&&lst){
                    tr[lst][c]=y;
                    lst=fa[lst];
                }
            }
        }
        lst=now;
        cnt[now]=1;
    }
};

相關文章