【字符串算法】刷题总结

字符串

笔记参考《算法竞赛从入门到进阶》《算法竞赛进阶指南》

一、c++字符串基本操作

相关博客

输入与输出

char s1[100],s2[1001000];int l1,l2;
scanf("%s",s1);//输入 遇到回车结束
l1=strlen(s1);//获取长度
string s1;
cin>>s1;//遇到换行或者回车结束
cin.get();
//从指定的输入流中提取一个字符(包括空白字符,空格、换行、tab 等),函数的返回值就是读入的字符
getline(cin,s2);//获取一行,遇到回车结束

字符串函数(string)

函数名功能
str.length()字符串长度
str[i]存取第i个字符,从0开始
str.substr(开始位置,长度)截取子串,位置下标从0开始,长度(可选,无长度截取从开始位置到最后)
str.find("子串",开始位置)查找子串返回下标,开始位置(可选),查找一次,更新一次开始位置即可查找子串出现的全部位置。

还有一些删除、插入、替换之类的完全可以使用上述函数实现。

大部分题目基础题目都是考察输入输出、字符串操作、字符串char与int互转。

还有一下小的语法或者注意事项。

字符串赋值

题目超级玛丽,需要初始化一个多行字符串,一般方法是每行加双引号行末加回车,不过用R("")可以直接表示多行字符串。

string s1="第一行/n"+"第二行";
string s=R"(第一行
第二行)"

输出字符回车注意事项

cout<<endl;//输出转义字符'\n',并且刷新缓冲区,所以遇到大量换行输出的题目尽可能使用'/n',否则TLE。
cout<<'\n';

scanf相关

scanf无法直接输入string类型数据,因为string不是原生c语言数据类型,或者使用前预先分配空间。参考

	string a;
	a.resize(2); //需要预先分配空间
	scanf("%s", &a[0]);
	cout << a;

二、字符串hash

hash函数将一个任意长度的字符串映射成一个非负整数,对字符串的各种操作,都可以直接对该非负整数hash值进行。

十进制数,基数是10,权重为 10^n (n是第几位)

相同思想,字符串映射函数可以是:基数是P(取31、13331),a代表1,z代表26每位权重为 31^n。(这里是针对与字符串全为小写字母,如果其他字符扩大P取值)。

常用的字符串hash函数

unsigned long long BKDRHash(string s){
    unsigned long long P=31,key=0;
    for(int i=0;i<s.length();i++){
        key=key*P+s[i]-'a'+1;
    }
    return key;
}

追加字符串

H(S+c)=H(S)*P+c-'a'+1;

获取子串

H(T)=H(S+T)-H(S)*P^(T.length());

相当于 12345 获取 45=12345-123*10^2=12345-12300=45,通过对整数操作去前缀。

题目:兔子与兔子

最大回文串 hash+二分

三、字典树

字典数学习最好要对数据结构–有所了解,需要涉及一些数组存储树结构,遍历等知识。

通过树的结点保存一个字符(针对一个字符串而言),每个结点有26(或者更多)分支。第一层是根节点,第二层是第一个字母,第三层是第三个字母。如果能够通过先序遍历获得该单词,说明字典序中存在该单词。

将所有空节点去掉,举一个实例图。

在这里插入图片描述

通过先序遍历(将根节点视作空),可以得到单词 bee、bee、may、man、mom、he等单词

树的基本操作过程

初始化、插入、检索

/*初始化*/
int tot=1;//已有结点数量,初始化第一个节点为根节点
int trie[10000][26],end_[10000];//end_数组以分支结尾的单词数量
//trie数组(比较难理解)第一维是结点
//trie数组第二维是26个分支,分支0存在则表示有扩展至'a'的分支
//其内容指向第一维,即下一个结点

/*插入*/
void insertTrie(string s){
    int len=s.length(),p=1;//沿根节点出发向下搜索
    for(int k=0;k<len;k++){
        int ch=s[k]-'a';
        if(trie[p][ch]==0) trie[p][ch]=++tot;//遇到不存在结点,分配结点
        p=trie[p][ch];//不断向下搜索
    }
    end_[p]++;//记录结尾单词数量
}

/*检索*/
int searchWord(string s){//检索的过程是判断是否有该位字符,有则向下搜索的先序遍历。
    int len=s.length(),p=1;
    for(int k=0;k<len;k++){
        p=trie[p][s[k]-'a'];
        if(p==0) return 0;//第k层并没有s[k]说明检索失败
    }
    return end_[p];//返回单词数量
}

例题统计前缀 模板end_[]数组、检索变形。

例题最大异或对 转化成二进制存储整数(插入),以及贪心检索最优结果。

#include<iostream>
using namespace std;
int n,m;
int tot=1;
int trie[3201000][2],a[100010],pow[32];

//1=1 2=10 3=11这种转换后保存不对
//因为需考虑到比较位数可能在检索中受限
//统一转换成31位二进制串,保留前缀0
//1=000 0000 0000 0000 0000 0000 0000 0001
void insertTrie(int x){
    int p=1;
    for(int i=30;i>=0;i--){
        int ch=x>>i&1;
        if(trie[p][ch]==0) trie[p][ch]=++tot;
        p=trie[p][ch];
    }
}
// 1^0=1 1^1=0 0^1=1 0^0=0 尽可能选择与本位不同的结点
int searchWord(int x){
    int p=1,ans=0;
    for(int i=30;i>=0;i--){//高位决定大小,所以必须从高位到低位搜索
        int ch=x>>i&1;
        if(trie[p][!ch]){
            p=trie[p][!ch];
            ans+=1<<i;
        }else{
            p=trie[p][ch];
        }
    }
    return ans;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        insertTrie(a[i]);
    }
    int maxx=0;
    for(int i=1;i<=n;i++){
        maxx=max(maxx,searchWord(a[i]));
    }
    cout<<maxx;
}

四、KMP算法

KMP洛谷模板题

在线性实现内判定字符串A[1~N] 是否是字符串B[1~M]的子串,并且可以求出字符串A在字符串B中最大匹配的数目(如果等于N,那么就是该子串出现的位置的末尾)。

可以实现c++ string 的find()函数 ,不过find()函数是简单的匹配算法。

朴素实现

	n=B.length(),m=A.length();
    for(int i=0;i<n;i++){//遍历B每一个字符
        bool f=true;
        for(int j=0;j<m;j++){//对齐判断是否有不对应的的字符
            if(B[i+j]!=A[j]) f=false;
        }
        if(f) cout<<i<<endl;
    }

KMP算法实现

在朴素算法基础上,减少每次对齐判断,无论匹配成功还是失败,并不是回退到开始处。

引入next数组,next[i]的值 l, 代表字符串A 前缀**A[ 1~I-1 ]** 与子串 A[i-I+1,i] 匹配。

例如,A=123123  下标从1开始
A[1]=0,A[2]=0,A[3]=0,
A[4]=1(因为1231 A[1~1]=A[4~4]="1"),
A[5]=2(因为12312 A[1~2]=A[4~5]="12"),
A[6]=3(因为123123 A[1~3]=A[4~6]="123")

​ 引入的原因是,比如 匹配到B[j+7]A[7],发现不等,未完整匹配A,匹配失败。但是我们知道 B[j+4~j+6]==A[4~6]A[1~3]=A[4~6],在朴素算法中B[j+1]开始与A[1]对齐后,可以省去 前三个字符的匹配,直接比较 B[j+7]A[4]

算法实现next数组

for(int i=2,j=0;i<=n;i++){//i遍历下标,j前缀长度
        while(j>0 && A[i]!=A[j+1]) j=next_[j];//直接比较i与j+1,不匹配j回退
        if(A[i]==A[j+1]) j++;//仍然匹配扩展
        next_[i]=j;
    }

B字符串最大匹配位置数组

for(int i=1,j=0;i<=m;i++){
        while(j>0 && (j==n||B[i]!=A[j+1])) j=next_[j];
        if(B[i]==A[j+1]) j++;
        f[i]=j;
        //if(f[i]==n) cout<<i-n+1<<'\n';//完全匹配的处理
 }

将这些内容刷题掌握住后,再去学习后缀树和后缀数组、AC自动机等较难知识。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值