字符串学习笔记集锦(未完成)

\(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;
}

应用

  1. 一个串在模式串内的出现次数和出现下标

    将两个串拼起来,做一遍 \(KMP\),在 \(fail_i=len_1\) 的下标就是这个串在模式串中出现的最后的下标。

  2. 计算最小周期

    这里的周期是可以不完全覆盖的,如 \(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;
	}
}
当然可以有两个不同的字符串的 Z 函数。
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 的应用:

  1. 查询一个串 \(s1\) 是否在另一个串 \(s2\) 中出现

    两个串合并(注意中间需要加特殊字符)做一个 SA 看 \(s1\) 的首字符编号 \(i\),求 \(\max(LCP(sa_i,sa_{rk_i-1}),LCP(sa_i,sa_{rk_i+1}))\) 即可。

  2. 求最长公共子串长度

    两个串合并,做 SA,之后把将后面那一个串的 \(rk_i\) 都记录下来,对前面的串枚举每一个 \(rk_j\) ,在排完序的序列里找到前驱和后驱,求 LCP 即可。

    也可以通过 ST 来解决 一个串和另一个串的区间的最长公共子串长度。

  3. 求本质不同的子串数量

    \(ans = n\times (n+1)/2 - \sum height_i\)

    容斥的思想,两个 \(sa_i\) 相邻的后缀,重复的只有 \(height_{sa_i}\),累加即可。

\(SAM\)

定义:

\(endpos\) 表示一个子串的所有出现位置的结尾下标。

\(trans\)

posted @ 2026-02-05 15:12  hnczy  阅读(17)  评论(0)    收藏  举报