首页 > 其他分享 >字符串学习笔记

字符串学习笔记

时间:2024-08-21 18:15:19浏览次数:10  
标签:int tr 笔记 学习 lst 字符串 sa now rk

扩展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;
    }
};

标签:int,tr,笔记,学习,lst,字符串,sa,now,rk
From: https://www.cnblogs.com/xqys/p/18372299

相关文章

  • 珂朵莉树/颜色段均摊学习笔记
    珂朵莉树是基于set的数据结构,与线段树、平衡树等树形结构类似,珂朵莉树是用来解决区间问题的很暴力的树形结构该数据结构支持区间推平,即将区间\([l,r]\)赋值为同一个值1.前置知识setset本质是平衡树,所以不会出现重复的元素且会自动排序,部分函数:set<Node>t;//定......
  • 迁移学习是什么
    1.迁移学习定义与原理  1.1迁移学习概念  迁移学习是一种机器学习技术,它允许一个模型将在一个任务上学到的知识应用到另一个相关任务上。这种技术特别适用于目标任务数据不足的情况,通过迁移已有的知识来提高学习效率和性能。  在迁移学习的框架中,通常有两个不同......
  • Linux学习之进程
    进程进程process是指正在执行的程序;是程序正在运行的一个实例。它由程序指令,和从文件、其它程序中读取的数据或系统用户的输入组成。进程状态在进程的生命周期内,进程总会从一个状态转变到另一个状态。Linux中,一个进程有下面的可能状态:Running:正在运行(它是系统中的当前进程)或......
  • 《Prometheus监控实战》读书笔记
    监控简介Google服务层次结构图,监控是底座一些监控反模式:事后监控机械式监控不(够)准确的监控静态监控:不是说超过某个绝对阈值系统就一定出现问题,更有意义的监控是对比(环比)动态监控。数据库性能分析供应商VividCortex的首席执行官BaronSchwartz对此评论道[插图]:它们比一个停......
  • 七个合法学习黑客技术的平台,让你从萌新成为大佬
     1、HackThisSite提供在线IRC聊天和论坛,让用户交流更加方便。网站涵盖多种主题,包括密码破解、网络侦察、漏洞利用、社会工程学等。非常适用于个人提高网络安全技能2、HackaDay涵盖多个领域,包括黑客技术、科技、工程和DIY等内容,站内提供大量有趣的文章、视频、教程和......
  • 七个合法学习黑客技术的平台,让你从萌新成为大佬
     1、HackThisSite提供在线IRC聊天和论坛,让用户交流更加方便。网站涵盖多种主题,包括密码破解、网络侦察、漏洞利用、社会工程学等。非常适用于个人提高网络安全技能2、HackaDay涵盖多个领域,包括黑客技术、科技、工程和DIY等内容,站内提供大量有趣的文章、视频、教程和......
  • 二叉树入门学习 优势对比 以及 完全二叉树c++代码的实现
    二叉树介绍文档一、概述二叉树是一种常见的数据结构,它是一种树形结构,每个节点最多有两个子节点,通常称为左子节点和右子节点。二叉树的基本概念如下:节点(Node):二叉树的基本单元,包含一个值以及指向左右子节点的引用。根节点(Root):树的顶端节点,没有父节点。叶子节点(Leaf):没有子节......
  • Python自动化脚本学习整理
     10个常用Python自动化脚本https://blog.csdn.net/csdn1561168266/article/details/135757528?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522172422930716800184162692%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=1724229307168......
  • 一门多范式的编程语言Scala学习的第二天-函数的使用
    2.12scala中的函数式编程*scala中的函数式编程**面向对象编程:将对象当作参数一样传来传去*1、对象可以当作方法参数传递*2、对象也可以当作方法的返回值返回*当看到类,抽象类,接口的时候,今后无论是参数类型还是返回值类型,都需要提供对应的实现类对象**面向函数式编程......
  • CAN学习笔记(一)CAN入门
    CAN学习笔记(一)CAN入门参考链接:https://blog.csdn.net/2301_77952570/article/details/131114941CAN收发器的作用发:将TTL电平转换为CAN专用电压的差分信号收:将CAN的差分信号转换为TTL电平高低电平的定义CAN_High-CAN_Low<0.5V时候为隐性的,逻辑信号表现为"逻辑1",即高......