字符串
笔记参考《算法竞赛从入门到进阶》《算法竞赛进阶指南》
一、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算法
在线性实现内判定字符串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自动机等较难知识。