字符串学习笔记集锦(未完成)
\(hash\)
这里的 hash 就是数位 hash ,注意在离线赛考虑多模数 hash。
这里讲一下树 hash。
\(h_x = f({h_i|i\in son(x)})\)
\(f\) 为集合对整数的映射。
\(f_S = (c+\sum_{j\in S} g(j))\)。
\(g\) 为一个随机构造整数对整数的映射。
额,好像树 hash 不是这个笔记应该讲的内容,算了。
\(KMP\)
\(border\) 表示一个字符串的一个子串使得这个子串同时是原串的前缀和后缀。
引理: 一个字符串 \(border\) 的 \(border\) 也是原串的 \(border\)
证明显然。
令 \(f_i\) 表示 \(s[1,i]\) 最大的 \(border = s[1,f_i]\)。
如何快速求 \(f_i\),现在已经知道了 \(f_{i-1}\),那么 \(s[1,f_{i-1}] = s[i-f_{i-1},i-1]\) 和所有的 \(s[1,f_{i-1}]\) 的 border 的长度 \(l\) 满足 \(s[1,l] = s[i-l,i-1]\)。所以我们只要 \(s[f_{i-1}+1] = s[i]\) ,那么 \(f_i = f_{i-1}+1\)。否则一直往最长的 \(border\) 跳,肯定可以得到 \(f_i\)
CODE
f[0] = -1;
for (int i = 2, j = 0; i <= n; i++) {
while (~j && s[i] != s[j + 1]) j = f[j];
f[i] = ++j;
}
应用
-
一个串在模式串内的出现次数和出现下标
将两个串拼起来,做一遍 \(KMP\),在 \(fail_i=len_1\) 的下标就是这个串在模式串中出现的最后的下标。
-
计算最小周期
这里的周期是可以不完全覆盖的,如 \(ab\) 就是 \(ababa\) 的一个周期,容易证明 \(n-f_{i}\) 是其的一个最小周期,可以根据 border 的定义去证明。
\(Exkmp\)
一个 Kmp 的延伸,虽然没发现是延伸,而是不同的路径解决同一种问题的方案而已。可以使用范围更加广一个点?
令 \(z_i\) 表示 \(s[i\sim n]\) 与 \(s\) 的 LCP 长度。
这里令 \(s[i\sim i+z_i-1]\) 为一个匹配区间 (Z-box)。
显然有 \(z_1=n\),这里令 \([l,r]\) 表示已经求出的 Z-box 中 \(i+z_i-1\) 最大的区间。我们现在需要充分利用好前面的内容来求出 \(z_i\)
-
当 \(i \le r\)
有 \(s[i,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)\)。
之后可以直接暴力拓展。 -
当 \(i>r\)
这个可以直接暴力求即可。
CODE
void exkmp() {
z[1]=n;
for(int i=2,l=0,r=0; 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) l = i, r = i + z[i] - 1;
}
}
CODE
for (int i = 1, l = 0, r = 0; i <= n; i++) {
if (i <= r) p[i] = min (z[i - l + 1], r - i + 1);
while (i + p[i] <= n && a[i + p[i]] == b[p[i] + 1]) p[i] ++;
if (i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
cnt[p[i]] ++;
}
待补充题目。
\(Trie\)
Trie 是一种数据结构,用来查找是否某一个字符串已经存在。
每一个节点表示一个前缀,\(ch\) 表示一个节点的多个儿子,在放入一个字符串的时候需要遍历每一位,如果没有这个节点需要新建。构建树关系后,查询只需要从前往后遍历即可。
CODE
struct Trie {
int ch[N][27],cnt[N],tot;
void init() {
memset(cnt,0,sizeof cnt);
for(int i=0; i<=tot; i++)for(int j=1; j<=62; j++)ch[i][j]=0;
tot=0;
}
void change(string &s) {
int len=s.size(),p=0;
for(int i=0; i<len; i++) {
if(!ch[p][hsh[s[i]]])
ch[p][hsh[s[i]]]=++tot;
p=ch[p][hsh[s[i]]];
}cnt[p]++;
}
int query(string &s) {
int len=s.size(),p=0;
for(int i=0; i<len; i++) {
if(!ch[p][hsh[s[i]]])return 0;
p=ch[p][hsh[s[i]]];
}
return cnt[p];
}
} trie;
01 Trie 是一种维护数字之间的异或和的数据结构,支持修改,删除,全局加一的操作。
主要讲一下全局加一的操作,将每一个节点交换左右儿子,如果有进位,那就将 \(1\) 这一位继续递归下去即可。
01 Trie 还可以支持合并,这类似于线段树合并,将另一个节点的数据全部加到另一个节点上即可,以下是代码:
CODE
struct Trie{
int ch[N*10][2],w[N*10],Xor[N*10],tot;
int New(){
++tot;
ch[tot][0]=ch[tot][1] =w[tot]=Xor[tot]=0;
return tot;
}
void pushup(int p){
w[p]=Xor[p]=0;
if(ch[p][0]) w[p]+= w[ch[p][0]] , Xor[p]^=Xor[ch[p][0]]<<1;
if(ch[p][1]) w[p]+= w[ch[p][1]] , Xor[p]^=(Xor[ch[p][1]]<<1) | (w[ch[p][1]]&1);
w[p]&=1;
}
void insert(int &rt, int x, int dep){
if(!rt) rt = New();
if(dep > H) return (void)(w[rt] ++);
insert(ch[rt][ x&1 ], x >> 1, dep + 1);
pushup(rt);
}
int merge(int p,int q){
if(!p || !q) return p|q;
w[p]+=w[q];
Xor[p]^=Xor[q];
ch[p][0] = merge(ch[p][0],ch[q][0]);
ch[p][1] = merge(ch[p][1],ch[q][1]);
return p;
}
void add(int x){
swap(ch[x][0],ch[x][1]);
if(ch[x][0]) add(ch[x][0]);
pushup(x);
}
}trie;
Trie 也可以可持久化,这里不做赘述。
\(SA\)
此算法的直接目的:比较以 \(i\) 为首位的后缀(后文称 \(suf_i\))的字典序。
\(rk_i\) 表示 \(suf_i\) 的字典序序号,\(sa_i\) 表示字典序为 \(i\) 的下标。
明显的一个性质 \(sa_{rk_i} = rk_{sa_i}=i\)。
容易发现可以用倍增+基数排序的方法来实现 \(O(n\log n)\),当然用快排也可以实现,但是常数较大。
具体实现方法就不讲了,相对简单。
这里提出一个概念来拓展 SA 的作用域,\(height_i\) 表示字典序相邻的两个后缀的 LCP,记为 \(height_i = lcp(sa_i,sa_{i-1})\) 。之后令 \(LCP(i,j)\) 表示这两个后缀的 LCP。
给出两个引理:
-
\(lcp(suf_i,suf_j)=min(lcp(suf_i,suf_k),lcp(suf_k,suf_j))(rk_i<rk_k<rk_j)\)
证明
显然任意的 \(s\),都有 \(lcp(s1,s2) \ge \min(lcp(s1,s3),lcp(s2,s3))\)。
现在只需证明 \(lcp(suf_i,suf_j)min(lcp(suf_i,suf_k),lcp(suf_k,suf_j))(rk_i<rk_k<rk_j)\)。不存在即可。
令 \(LCP(i,k) = a,LCP(j,k)=b\)。
-
\(a<b\) 可得 \(s_{i+a}\neq s_{k+a},s_{j+a} = s_{k+a}\),可得 \(s_{i+a}\neq s_{j+a}\)。显然成立。
-
\(a>b\) 同理。
-
\(a=b\) ,很显然 \(LCP(i,j)=a\),成立。
综上,命题成立。
-
-
\(height_{rk_i} \ge height_{rk_{i-1}}-1\) 。
证明
这里规定
\(suf_i = a\),\(suf_{sa_{rk_i-1}} = b\),\(suf_{i+1}=c\),\(suf_{sa_{rk_{i+1}-1}}=d\),\(height_{rk_i} = x\),\(height_{rk_{i+1}} = y\)。同时可以知道 \(height_{rk_i} = LCP(i,sa_{rk_i-1})\),由 \(height\) 定义易证。
\(x\le 1\) 明显正确。
因为 \(c\) 就是 \(a\) 去掉首字母,故 \(a_{1,j} = b_{1,j} ,a_{2,j} = c_{1,j-1}\)
有定义可知,\(d_i<c_i\),若 \(d_{1,x-1} \neq c_{1,x_1}\),则 \(d_{1,x-1}<c_{1,x-1}\)
而存在后缀 \(e = b_{2,len(b)}\),满足 \(e_{1,x-1} = c_{1,x-1}\) 由于 \(x\ge 2\) ,且 \(e<c\),故 \(d<e<c\) 说明后缀 \(d,c\) 不是 \(rk\) 相邻的后缀。
故命题得证。
通过这个引理 2,我们可以很容易地写出 \(height\) 的 \(O(n)\) 处理方法。
又因为引理 1,我们可以得到 \(LCP(i,j) = \min_{k=i+1}^{j}(height_k)\),显然可以 \(O(n\log n)\) 预处理 \(O(1)\) 查询。
CODE
struct SA {
int height[N],n,m,id[N],rk[N<<1],sa[N<<1],cnt[N],od[N],st[20][N],tmp;
void init(char *s) {
n = strlen(s+1);
m = 128;
for (int i = 1; i <= n; i++) cnt[rk[i] = s[i]]++;
for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--) sa[cnt[rk[i]]--] = i;
for (int i = 1;; i <<= 1, m = tmp) {
tmp = 0;
for (int j = n - i + 1; j <= n; j++) id[++tmp] = j;
for (int j = 1; j <= n; j++)
if (sa[j] > i)id[++tmp] = sa[j] - i;
memset(cnt, 0, sizeof cnt);
for (int j = 1; j <= n; j++) cnt[rk[j]]++;
for (int j = 1; j <= m; j++) cnt[j] += cnt[j - 1];
for (int j = n; j >= 1; j--) sa[cnt[rk[id[j]]]--] = id[j];
tmp = 0;
memcpy(od, rk, sizeof od);
for (int j = 1; j <= n; j++) {
if (od[sa[j]] == od[sa[j - 1]] && od[sa[j] + i] == od[sa[j - 1] + i])
rk[sa[j]] = tmp;
else rk[sa[j]] = ++tmp;
}
if (tmp == n)break;
}
for (int i = 1, K = 0; i <= n; i++) {
if (rk[i] == 1)continue;
if (K)--K;
while (s[i + K] == s[sa[rk[i] - 1] + K]) K++;
height[rk[i]] = K;
}
}
void ST() {
for (int i = 1; i <= n; i++) st[0][i] = height[i];
for(int i=2; i<=n; i++)lg[i] = lg[i>>1]+1;
int w = lg[n];
for (int k = 1; k <= w; k++)
for (int i = 1; i <= n+1-(1<<k); i++)
st[k][i] = min(st[k-1][i], st[k-1][i+(1<<(k-1))]);
}
int getlcp(int l,int r) {
if(l>r)swap(l,r);
l++;
int tmp = lg[r - l + 1];
return min(st[tmp][l], st[tmp][r-(1<<tmp)+1]);
}
};
SA 的应用:
-
查询一个串 \(s1\) 是否在另一个串 \(s2\) 中出现
两个串合并(注意中间需要加特殊字符)做一个 SA 看 \(s1\) 的首字符编号 \(i\),求 \(\max(LCP(sa_i,sa_{rk_i-1}),LCP(sa_i,sa_{rk_i+1}))\) 即可。
-
求最长公共子串长度
两个串合并,做 SA,之后把将后面那一个串的 \(rk_i\) 都记录下来,对前面的串枚举每一个 \(rk_j\) ,在排完序的序列里找到前驱和后驱,求 LCP 即可。
也可以通过 ST 来解决 一个串和另一个串的区间的最长公共子串长度。
-
求本质不同的子串数量
\(ans = n\times (n+1)/2 - \sum height_i\)
容斥的思想,两个 \(sa_i\) 相邻的后缀,重复的只有 \(height_{sa_i}\),累加即可。
\(SAM\)
定义:
\(endpos\) 表示一个子串的所有出现位置的结尾下标。
\(trans\)

浙公网安备 33010602011771号