字符串合集
基本概念
有一个字符串
子串:由一个字符串
前缀:由一个字符串
后缀:由一个字符串
border:对于一个字符串
单模式串匹配
暴力匹配
在每个位置都进行一次匹配即可。
string a,b;
cin>>a>>b;
int sa=a.size(),sb=b.size();
for(int i=0;i<sa;i++){
if(a[i]==b[0]){
int f=1;
for(int j=0;j<sb;j++){
if(a[i+j]!=b[j])f=0;
}
if(f==1){
cout<<i;
return 0;
}
}
}
字符串哈希
字符串匹配为什么慢?
因为需要一位一位匹配。
如果我们将一个字符串转化为一个整数,那么匹配会快很多。
考虑如何将字符串转化为一个整数。
容易想到,直接将每个字符编号,然后加和就可以了。但是这种方法会把 ab
和 ba
一类字符串识别成同一个字符串,正确性不高。
我们需要一个正确性更高的算法。
我们记字符集大小为
那么将一个字符串记为一个
结果需要取模(令模数为
然后我们在字符串匹配的时候,通常需要获取一段区间的哈希值。
令
那么
实际运用中需要乘
例1 【模板】字符串哈希
直接哈希。甚至不用取区间。
这里底数我用
#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int p=131;
int n,ans=1;
ull h[10001],Pow[10001];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
Pow[0]=1;
for(int i=1;i<=n;i++){
string s;
cin>>s;
int len=s.size();
ull hash=0;
s=" "+s;
for(int j=1;j<=len;j++){
hash*=p;
hash+=s[j];
}
h[i]=hash;
}
sort(h+1,h+n+1);
for(int i=2;i<=n;i++){
if(h[i]!=h[i-1]) ans++;
}
cout<<ans;
return 0;
}
KMP
想想暴力匹配为什么会慢?
因为每次失配之后我们都要从头匹配。
KMP 就很好地优化了这一点,失配时,可以不需要再次重新匹配,而是可以找到当前位置失配时模式串应该从何处开始匹配。举个例子:
s: abcacababcab
t: abcab
令
此时,我们在
s:abcacababcab
t: abcab
为什么可以这么做?
因为我们扫完了模式串的前四位,发现字符串 a
是其 abca
的最长 border。那么就意味着我们在
那么现在问题落在了怎么求解前缀的最长 border 上。(一般前
怎么计算 abacabab
的
考虑递推:
首先令
对于位置
对于位置
对于位置
同理可得
对于位置
显然不是,既然现在这个前缀不能与 aba
的最长 border 为 a
,于是这个 a
就有可能与
因为匹配时候的
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
int nxt[maxn],lena,lenb,j;
string a,b;
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>a>>b;
lena=a.size(),lenb=b.size();
a=" "+a,b=" "+b;
for(int i=2;i<=lenb;i++){
while(j>0&&b[i]!=b[j+1]){
j=nxt[j];
}
if(b[j+1]==b[i]){
j++;
}
nxt[i]=j;
}
j=0;
for(int i=1;i<=lena;i++){
while(j>0&&b[j+1]!=a[i]){
j=nxt[j];
}
if(b[j+1]==a[i]){
j++;
}
if(j==lenb){
cout<<i-lenb+1<<endl;
j=nxt[j];
}
}
for(int i=1;i<=lenb;i++){
cout<<nxt[i]<<" ";
}
return 0;
}
STL 大法
清峥不让写
失配树
我们把一个字符串的一个位置的下标和它的
不难发现一个节点的所有祖先节点代表其所有的 border。
P3435 [POI2006] OKR-Periods of Words
题意
求给定字符串所有前缀的最大周期长度之和。
思路
我们有一个性质:自证不难·)
对于一个字符串,我们不断跳
那么我们找到一个最小的
暴力跳 next (65pts)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e6+10;
string s;
int nxt[maxn],n,ans;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>s;
s=" "+s;
int x=0;
for(int i=2;i<=n;i++){
while(x>0&&s[i]!=s[x+1]){
x=nxt[x];
}
if(s[x+1]==s[i]){
x++;
}
nxt[i]=x;
}
for(int i=1;i<=n;i++){
int now=i;
while(nxt[now]){
now=nxt[now];
}
ans+=i-now;
}
cout<<ans;
return 0;
}
优化1
如果我们找到
记忆化 (100pts 3.82s)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e6+10;
string s;
int nxt[maxn],n,ans;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>s;
s=" "+s;
int x=0;
for(int i=2;i<=n;i++){
while(x>0&&s[i]!=s[x+1]){
x=nxt[x];
}
if(s[x+1]==s[i]){
x++;
}
nxt[i]=x;
}
for(int i=1;i<=n;i++){
int now=i;
while(nxt[now]){
now=nxt[now];
}
ans+=i-now;
if(nxt[i]) nxt[i]=now;
}
cout<<ans;
return 0;
}
优化2
不难发现这是失配树上找离根节点最近的祖先的问题(求最短 border)。
那么我们干脆不要根节点
路径压缩 (100pts 250ms)
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e6+10;
string s;
int nxt[maxn],n,ans,f[maxn];
inline int find(int x){
if(f[x]!=x) f[x]=find(f[x]);
return f[x];
}
inline int search(int x){
return x-f[x];
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>s;
s=" "+s;
int x=0;
for(int i=1;i<=n;i++){
f[i]=i;
}
for(int i=2;i<=n;i++){
while(x>0&&s[i]!=s[x+1]){
x=nxt[x];
}
if(s[x+1]==s[i]){
x++;
}
nxt[i]=x;
if(nxt[i]){
f[fa]=fb;
}
}
for(int i=1;i<=n;i++){
ans+=search(i);
}
cout<<ans;
return 0;
}
字典树(Trie 树)
普通字典树
对于一堆字符串
暴力扫显然是
假如我们的字符串是 i
,int
,integer
,shw
,shw666
,intern
和 internet
,那么我们可以建一颗树:
对于每个字符串,构建方法为:
首先,
若节点
否则直接跳到该儿子。
同时,我们记录一下哪些节点是一个字符串的结尾,那么对于每个询问,我们只要爬一遍树,看看路径上有哪些节点是被标记以该节点结尾的字符串,统计一下就是答案。
例如我们查找字符串 shw114514
,此时爬树时会遇到
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e6+10;
int t,n,q,trie[maxn][63],cnt,e[maxn];
int id(char c){
if('a'<=c&&c<='z') return c-'a';
else if('A'<=c&&c<='Z') return 26+c-'A';
else return 52+c-'0';
}
void insert(string s){
int len=s.size(),now=0;
s=" "+s;
for(int i=1;i<=len;i++){
if(!trie[now][id(s[i])]){
trie[now][id(s[i])]=++cnt;
}
now=trie[now][id(s[i])];
e[now]++;
}
}
int query(string s){
int len=s.size(),now=0;
s=" "+s;
for(int i=1;i<=len;i++){
if(trie[now][id(s[i])]){
now=trie[now][id(s[i])];
}
else{
return 0;
}
}
return e[now];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>t;
while(t--){
for(int i=0;i<=cnt;i++){
for(int j=0;j<=114;j++){
trie[i][j]=0;
}
}
for(int i=0;i<=cnt;i++){
e[i]=0;
}
cnt=0;
cin>>n>>q;
for(int i=1;i<=n;i++){
string s;
cin>>s;
insert(s);
}
for(int i=1;i<=q;i++){
string s;
cin>>s;
cout<<query(s)<<endl;
}
}
return 0;
}
01Trie
P4551 最长异或路径
给定一棵
首先是一个很简单的处理:我们与处理出每个节点到根节点的异或值,记为
那么问题就变成了:计算
我们对于每个
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e6+10;
struct edge{
int e,v;
};
int s[N],trie[N][2],n,cnt;
vector<edge> vec[N];
void dfs(int x,int fa){
for(int i=0;i<vec[x].size();i++){
int nx=vec[x][i].e;
if(nx!=fa){
s[nx]=s[x]^vec[x][i].v;
dfs(nx,x);
}
}
}
void insert(int v){
int now=0;
for(int i=(1<<30);i;i>>=1){
bool x=(v&i);
if(!trie[now][x]){
trie[now][x]=++cnt;
}
now=trie[now][x];
}
}
int getval(int v){
int ans=0,now=0;
for(int i=(1<<30);i;i>>=1){
bool x=(v&i);
if(trie[now][!x]){
ans+=i;
now=trie[now][!x];
}
else{
now=trie[now][x];
}
}
return ans;
}
int main(){
//freopen("data.txt","r",stdin);
//freopen("my.txt","w",stdout);
std::ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<n;i++){
int u,v,w;
cin>>u>>v>>w;
vec[u].push_back((edge){v,w});
vec[v].push_back((edge){u,w});
}
dfs(1,-1);
for(int i=1;i<=n;i++){
insert(s[i]);
}
int ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,getval(s[i]));
}
cout<<ans;
return 0;
}
多模式串匹配
下次一定
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话