字符串基础知识
定义
字符串
对于一个字符串
子串
从一个字符串
子序列
从一个字符串
前缀
从
后缀
以
回文串
正着和倒着都一样的字符串。
周期
我们定义,如果一个字符串是以一个或者一个以上的长度为
标准库
s.c_str()/s.data()
返回一个指针,内容就是原来的字符串。s.size()
返回字符个数。s.find(c, begin)
查找并返回从begin
开始的c
的位置。s.substr(begin,len)
从begin
开始截取一段长度为len
的字符。s.append(s',pos,len)
将s'
中从pos
开始的len
个字符接到s
的末尾。s.replace(pos,n,s')
将s
中从pos
开始的长为n
的子串替换成s'
。s.erase(pos,len)
删除从pos
开始的len
个字符。s.insert(pos,s')
在pos
处插入s'
。
哈希
定义
哈希其实就是一个映射,它通过一个函数将一些抽象的东西变成直观的数字,方便我们去比较信息。
字符串哈希
就是把字符串通过映射变成一个数,方便比较。一般把字符串看成一个多位数,进制取一个质数,然后选择一个大模数。
性质
- 哈希值不同的原来一定不同。
- 哈希值相同的原来不一定相同。
如果原来不同但是哈希值相同,我们称此为哈希冲突。
哈希冲突
我们设哈希后字符串取值范围为
证明:
先算哈希不冲突的概率为:
然后根据泰勒公式:
当
所以之前的式子就可以写成:
所以哈希冲突概率为:
那么,我们在实际写题的时候又应该如何避免呢?
双哈希
顾名思义,就是对于一个字符串分别设置两个函数,判断是否相等就看两个哈希值是否都相等即可。
例题
P3370 【模板】字符串哈希
板子题没啥好说的。
P2757 [国家集训队] 等差子序列 Problem - 452F - Codeforces 双倍经验
题解
转化题意,就是在序列中找到一个位置
如何优化呢?假设现在有一个数组
到这里我们就用线段树做就行了。维护正序倒序两种哈希值,单点修改就做完了。
(由于没写所以代码就不放啦喵)
P3449 [POI2006] PAL-Palindromes
题解
先去寻找题目性质。首先能发现输入的都是回文串!然后想一想如果两个串要组成大的回文串需要什么条件?
我们先假设有两个回文串
我们就开一个 map
记一下每个最小周期相同的个数然后就做完了。时间复杂度小于调和级数所以是
代码
int n, m;
const int b1 = 133, b2 = 233, m1 = 1e9 + 7, m2 = 1e9 + 9;
ll h1[N], h2[N], s1[N], s2[N], ans;
char s[N];
map < pll , ll > mp;
pll aim;
pll geth(int l, int r){
ll x1 = (h1[r] - h1[l - 1] * s1[r - l + 1] % m1) + m1; x1 %= m1;
ll x2 = (h2[r] - h2[l - 1] * s2[r - l + 1] % m2) + m2; x2 %= m2;
return mkp(x1, x2);
}
bool eq(pll x, pll y){
return x.first == y.first and x.second == y.second;
}
bool chk(int len){
for(int i = len + 1; i <= m; i += len)if(! eq(geth(i, i + len - 1), aim))return false;
return true;
}
signed main(){
n = rd(); s1[0] = s2[0] = 1;
for(int i = 1; i < N; ++i)s1[i] = s1[i - 1] * b1 % m1, s2[i] = s2[i - 1] * b2 % m2;
while(n--){
m = rd(); scanf("%s", s + 1);
for(int i = 1; i <= m; ++i)h1[i] = (h1[i - 1] * b1 % m1 + s[i]) % m1, h2[i] = (h2[i - 1] * b2 % m2 + s[i]) % m2;
for(int i = 1; i <= m; ++i)if(m % i == 0){
aim = geth(1, i);
if(chk(i)){ans += (mp[aim]++ << 1) + 1; break;}
}
}
printf("%lld", ans);
return 0;
}
KMP
介绍
KMP 算法是解决字符串中的匹配问题的高效算法。最基本的问题是给你一个文本串和模式串,让你求出模式串在匹配串中出现的位置、次数等。
匹配问题
关于这个问题其实有很多的做法。
- 暴力做法:枚举文本串每个位置作为起点开始按位匹配,如果不行就换起点。
- 哈希做法:预处理出文本串与模式串的哈希值然后
比较。 - KMP!
前言
虽然就一个模式串和一个匹配串的题看不出哈希和 KMP 的差距,但 KMP 的思想可以用于解决多模式串匹配串的题,这也就是学习 KMP 的意义。
字符串的 border
前言
border 是一个非常重要的概念,所以在介绍所有匹配的东西之前我们需要先了解 border 以及它的一些性质。
概念
如果字符串的一个真前缀和真后缀相等,那么我们称此为这个字符串的 border。
性质
周期有关
- 如果一个前缀
是 的 border,那么 是 的周期。 - 若
都是 的周期,且 ,则 也是 的周期。
性质 1 很简单可以画图自证;
性质 2 证明:钦定
- 对于
,我们能发现 ; - 对于
,我们同样能发现 。
border 本身性质
- 对于一个字符串
,它的所有长度 的 border 的长度是等差序列。 - 字符串的所有 border 组成
个等差序列。
性质 3 证明:假设我们知道最大的 border 是 d,那么字符串的最小周期为
性质 4 证明:首先对于长度 不清晰了。
所以又可以根据性质 3 又分出一些长度
例题:WC2016 论战捆竹竿
题解
考虑一个串不同的贡献就是这个串的长度减去 border,然后可以将题意转化为给你一些数,问你可以凑出多少不超过
因为 border 的性质是
转移时可以单调队列维护
最后考虑两个等差序列之间如何转换。对于一个点
因为作者太菜了写不来所以就不放半成品了
前缀函数
在正式讲 KMP 前,我们需要学习前缀函数。
对于一个位置
举一个例子:若我现在有一个字符串
如何去求前缀函数?
对于一个字符串,假设我们已经求出
我们知道前缀函数是算的最长相等真前缀真后缀,所以我们可以先去比较
那如果它们不同呢?我们可以记录一个
具体的可以参考上面我画的图(好丑)。如果两个大的部分(
我们已经求出前缀函数,那么如何用前缀函数求解匹配问题呢?
KMP 算法
先放图qwq。
看完图我们能发现:这个流程与前面求前缀函数的过程非常类似。我们可以先求出模式串
代码(我远古时期写的板子)
int n, m, j, k[N];
char a[N], b[N];
void solve(){
cin >> a + 1;
cin >> b + 1;
n = strlen(a + 1); m = strlen(b + 1);
FL(i, 2, m){
while(j and b[i] != b[j + 1])j = k[j];
if(b[i] == b[j + 1])j++;
k[i] = j;
}
j = 0;
FL(i, 1, n){
while(j > 0 and a[i] != b[j + 1])j = k[j];
if(a[i] == b[j + 1])j++;
if(j == m){
cout << i - m + 1 << '\n';
j = k[j];
}
}
FL(i, 1, m)cout << k[i] << ' ';
}
例题
USACO15FEB Censoring S
题解:
疑似板子?就是在匹配文本串时用栈代替数组,当找到一个匹配时就把这一段弹出去,然后维护一下前缀函数即可。
代码
int n, m, fail[N], j, top, cur[N];
char a[N], b[N], st[N];
signed main(){
// fileio(fil);
scanf("%s %s", a + 1, b + 1); n = strlen(a + 1), m = strlen(b + 1);
for(int i = 2; i <= m; ++i){
while(j and b[i] ^ b[j + 1])j = fail[j];
if(b[i] == b[j + 1])++j; fail[i] = j;
}
j = 0;
for(int i = 1; i <= n; ++i){
st[++top] = a[i];
while(j and st[top] ^ b[j + 1])j = fail[j];
if(st[top] == b[j + 1])++j;
if(j == m)top -= m, j = cur[top];
cur[top] = j;
}
for(int i = 1; i <= top; ++i)putchar(st[i]);
return 0;
}
P4391 BOI2009 Radio Transmission 无线传输
题解
找周期就直接用长度减去
trie 树
介绍
顾名思义就是一颗朴实无华的树。有一个根,从根往下有一些子树,从根到某个点的路径就是一个字符串。
这里引用了 OI-wiki 上的一张图,以便读者能更直观地理解 trie 树。比如 12 号点就代表了一个字符串
这里先给出 trie 的板子。
int T, n, m;
char s[N];
map < char, int > mp;
struct trie{
int cnt, nex[N][63], ext[N];
void ins(char *s, int l){
int p = 0;
FL(i, 1, l){
int ch = mp[s[i]];
if(! nex[p][ch])nex[p][ch] = ++cnt;
p = nex[p][ch]; ext[p]++;
}
}
int query(char *s, int l){
int p = 0;
FL(i, 1, l){
int ch = mp[s[i]];
if(! nex[p][ch])return 0;
p = nex[p][ch];
}
return ext[p];
}
void clear(){
FL(i, 0, cnt){
ext[i] = 0;
FL(j, 0, 62)nex[i][j] = 0;
}cnt = 0;
}
}t;
void init(){
int tot = 0;
for(char i = 'a'; i <= 'z'; i++)mp[i] = ++tot;
for(char i = 'A'; i <= 'Z'; i++)mp[i] = ++tot;
for(char i = '0'; i <= '9'; i++)mp[i] = ++tot;
}
那么 trie 树又有什么用呢?
应用
- 查询字符串。直接沿着树边走就行,如果走到空节点就说明不存在这个字符串。
- 与 KMP 合体变成 ACAM!这个会在其他博客中讲。
- 01 trie
这里稍微讲一下 01 trie。
01 trie
01 trie 是一种特殊的 trie,它维护了一些由 0 和 1 组成的字符串。然后对于任意整数我们都可以把它写成二进制,这样我们就能够用 01 trie 维护一些数了,而且把这些数丢进 01 trie 中它会自动排好序,然后你也许会意识到它(01 trie)就类似一颗平衡树。然而作者数据结构学得西撇,所以不在此讲有关维护 01 trie 的内容,对于字符串只要知道 trie 能够查询字符串也许就行了。(逃
例题
P2580 于是他错误的点名开始了
题解
就是普通的在 trie 上做插入操作,然后每次查询后维护一下字符串是否被第一次询问即可。
P4551 最长异或路径
题解
首先要知道
代码
int n, hd[N], cnt, dis[N], trie[N << 4][2], cnt_t = 1, ans;
bool ed[N << 4];
struct edge{int nxt, to, d;}e[N << 1];
void add(int x, int y, int z){e[++cnt] = (edge){hd[x], y, z}; hd[x] = cnt;}
void dfs(int u, int fa)
{
for(int i = hd[u]; i; i = e[i].nxt)
{
int v = e[i].to, val = e[i].d;
if(v == fa)continue;
dis[v] = dis[u] xor val; dfs(v, u);
}
}
void ins(int x)
{
int k = 1;
for(int i = 31; i > ~ i; i--)
{
int ret = (x >> i) & 1;
if(!trie[k][ret])trie[k][ret] = ++cnt_t;
k = trie[k][ret];
}
ed[k] = true;
}
int find(int x)
{
int k = 1, ans = 0;
for(int i = 31; i > ~ i; i--)
{
int ret = (x >> i) & 1;
if(trie[k][ret xor 1])k = trie[k][ret xor 1], ans += 1 << i;
else k = trie[k][ret];
}
return ans;
}
signed main()
{
cin >> n;
for(int i = 1, x, y, z; i < n; i++)
{
scanf("%d %d %d", &x, &y, &z);
add(x, y, z); add(y, x, z);
}
dfs(1, 0);
for(int i = 1; i <= n; i++)ins(dis[i]);
for(int i = 1; i <= n; i++)ans = max(ans, find(dis[i]));
cout << ans; return 0;
}
最后
以上就是这篇博客的全部内容了,这篇博客以讲基础为主(hash、KMP、trie),加了一点稍难的题目。后面还会写三篇字符串的博客然后结束字符串所有内容,请敬请期待。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)