1. 后缀数组

既然是水博客,那么就……\(\rm Link.\)

重要的就是基数排序的思想,先将最不重要关键字排序,再在排序后数组中按倒数第二不重要关键字排序……这样最重要的关键字能在最后 力挽狂澜

需要注意数的排序是末位对齐,而字符串是首位对齐。

1.1. 模板

\(\text{Update on 2022/5/27}\):今天模拟赛写暴力打了个 \(\rm sa\),然后 \(\color{red}{\rm wa}\) 穿了……检查了半天发现是重新划分数组 \(x\) 时,\(\text{sa}_i+k\)\(\text{sa}_{i-1}+k\) 发生了越界,但那道题是多组数据,大于 \(\rm cnt\) 部分的数值是未知的,所以可能引起错误。写的时候还是注意一下 qwq.

void Suffix() {
    
    /*
    	sa[i]:排名为 i 的后缀的开头
    	y[i]:按第二关键字排序,排名为 i 的后缀的开头
    	x[i]:开头为 i 的后缀所属的阶级(注意这里的值是会重合的,这里存的是上一轮的结果)
    */
    
	rep(i,1,n) ++c[x[i]=s[i]];
	rep(i,2,m) c[i]+=c[i-1];
	fep(i,n,1) sa[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1) { // 每次比较的长度为 2*k
		num=0;
		rep(i,n-k+1,n) y[++num]=i; // [n-k+1,n] 都没有第二关键字,所以可以乱插,按第一关键字排序
		rep(i,1,n) if(sa[i]>k) y[++num]=sa[i]-k; // 按第二关键字排名插入第一关键字的位置,当然这得满足能有第一关键字的开头
		rep(i,1,m) c[i]=0; // 有 m 个阶级
		rep(i,1,n) ++c[x[i]];
		rep(i,2,m) c[i]+=c[i-1]; // 求前缀和后就知道第 i 个阶级的最后一个数排在总数组的哪个位置
		fep(i,n,1) sa[c[x[y[i]]]--]=y[i],y[i]=0; // 倒着枚举排名,求出这个开头属于的阶级,再求出它的总数组的编号,再将总数组编号位赋值为这个开头
		swap(x,y);
		x[sa[1]]=1; num=1;
		rep(i,2,n)
			x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?num:++num;
		if(n==num) break;
		m=num;
	}
}


void LCP() {
    
    /*
    rk[i]:开头为 i 的后缀的 rank
    关于 height[i]>=height[i-1]-1:
		height[i] 指开头为 i 的后缀与排名为 rk[i]-1 的后缀的 lcp
    h[i]:排名为 i 的后缀与排名为 i-1 的后缀的 lcp
    
    关于原理:
    	既然 id_{i-1} 都与 "排名前面一位的后缀 p" 匹配了 h[rk[id_{i-1}]] 位,那么对于 i,p+1 的排名在 i 之前,它们的 lcp 就是 height[i-1]-1,可能中间还有其它后缀,但是 lcp 不会更小了
    */
    
	int k=0;
	rep(i,1,n) rk[sa[i]]=i;
	rep(i,1,n) {
		if(rk[i]==1) continue;
		if(k) --k; // height[i-1]-1
		int j=sa[rk[i]-1]; // j 是排名在 i 前面一位的后缀编号
		while(j+k<=n&&i+k<=n&&s[j+k]==s[i+k]) ++k;
		h[rk[i]]=k; // k 不超过 n,最多减 n 次,所以复杂度 O(n)
	}
}


/* Updated */

void _sort() {
    for(int i=1;i<=m;++i) c[i]=0;
    for(int i=1;i<=n;++i) ++ c[x[i]];
    for(int i=2;i<=m;++i) c[i] += c[i-1];
    for(int i=n;i;--i) sa[c[x[y[i]]]--] = y[i], y[i]=0;
}

void SA() {
    m=180;
    for(int i=1;i<=n;++i) y[i]=i,x[i]=s[i];
    _sort();
    for(int k=1;k<=n;k<<=1) {
        num=0;
        for(int i=n-k+1;i<=n;++i) y[++num]=i;
        for(int i=1;i<=n;++i) if(sa[i]>k) y[++num] = sa[i]-k;
        _sort(); swap(x,y); x[sa[1]] = num = 1;
        for(int i=2;i<=n;++i)
            x[sa[i]] = (y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k])?num:++num;
        if(n==num) return;
        m=num;
    }
}

void getLCP() {
    int j,k=0;
    for(int i=1;i<=n;++i) x[sa[i]] = i; // 这里不能直接用 x,因为它代表的 rank 不是严格的
    for(int i=1; i<=n; h[x[i++]]=k)
        for(k=k?k-1:k, j=sa[x[i]-1]; j && i+k<=n && j+k<=n && s[i+k]==s[j+k]; ++k);
}

1.2. 拓展

1.2.1. 求所有本质不同的子串

先求出后缀的所有前缀数,再减去与之前重复的。

\[ans=\sum_{i=1}^n n-sa[i]+1-h[i] \]

1.2.2. 不可重叠最长重复子串

给定一个字符串,求最长重复子串,这两个子串不能重叠。

先二分答案 \(k\),把题目变成判定性问题。经典的解决方案是把排序后的后缀分成若干组,其中每组的后缀之间的 \(\text{height}\) 值都不小于 \(k\).
容易看出,有希望成为最长公共前缀不小于 \(k\) 的两个后缀一定在同一组。对于每组后缀,只须判断每个后缀的 \(\rm sa\) 值的最大值和最小值之差是否不小于 \(k\).

1.2.3. 可重叠的 \(k\) 次最长重复子串

给定一个字符串,求至少出现 \(k\) 次的最长重复子串,这 \(k\) 个子串可以重叠。

可以先二分答案,然后将后缀分成若干组。然后判断有没有一个组的后缀个数不小于 \(k\).

或者这道题也可以正向求解。出现至少 \(k\) 次意味着后缀排序后有至少连续 \(k\) 个后缀的 \(\rm lcp\) 是这个子串。用单调队列维护即可。

1.2.4. 重复次数最多的连续重复子串

给定一个字符串,求重复次数最多的连续重复子串。

首先枚举最小串长度 \(i\)(最小串定义为被重复串),那么如果出现了最小串长度为 \(i\) 的重复(不妨设重复次数大于一),从 \(1\) 开始枚举 \(j,j+i,\dots ,j+ki\),那么一定有 连续 的两个下标存在在这个重复内,且它们的 地位是等价 的。

于是我们 \(\mathcal O(1)\) 计算相邻下标的 \(L=\rm lcp\),不难发现,若 \(L\)\(i\) 的倍数,那么就是一个快速判断循环子串的模型 —— 此时循环次数为 \(L/i+1\).

如果不是呢?此时还有可能是我们的起始位置多加了 \(i-L\bmod i\),把它减回去,再判断能不能多形成一个循环串。

如果要求字典序最小呢?可以按排名枚举串,再枚举合法的 \(i\),遇到匹配的情况即可直接输出。复杂度不会证。

另外这里再提一嘴用 \(\rm rmq\) 预处理 \(\rm lcp\) 需要注意若查询 \(x\)\(y\)\(\rm lcp\),就是查询 \(\min_{i=rk_x+1}^{rk_y} h_i\). 所以函数应当这样实现:

int lcp(int l,int r) {
    l=x[l],r=x[r];
    if(l>r) swap(l,r);
    int d = lg[r-l]; 
    return min(st[d][l+1],st[d][r-(1<<d)+1]);
}

1.2.5. 最长公共子串

\(A\)\(B\) 的最长公共子串等价于求 \(A\) 的后缀和 \(B\) 的后缀的最长公共前缀的最大值。解决方法是将第二个字符串写在第一个字符串后面,中间用一个没有出现过的字符隔开,再求这个新的字符串的后缀数组。
接下来只用求排名相邻但原来不在同一个字符串中的两个后缀之间 \(\rm height\) 值的最大值。

1.2.6. 长度不小于 \(k\) 的公共子串的个数

A="xx",B="xx"
ans=5

还是先将两个字符串连起来,中间用一个没有出现过的字符隔开。按大小顺序枚举后缀,先计算 \(B\) 的后缀与在此之前 \(A\) 的后缀之间产生的贡献,再计算 \(A\) 的后缀与在此之前 \(B\) 的后缀之间产生的贡献。

考虑用单调栈维护(以第一种情况为例)。考虑加入 \(A\) 后缀 \(i\),那么就是将答案加上 \(\delta=h_{i+1}-k+1\)(特意提醒一下这和 \(i+1\) 是什么类型的后缀无关). 注意这里 "答案" 的意义是:如果之后有 \(B\) 的后缀,那么这个后缀增加的答案就是 "答案"。

那什么时候 \(\delta\) 会受影响呢?如果 \(h\) 是不降的,那么 \(\delta\) 就不受影响,那么不妨用单调栈维护不降的 \(h\),同时还要维护与这个后缀的 \(h\) 共存亡的 \(A\) 前缀的数量。听上去很奇怪?一个例子就是加入 \(A\) 后缀 \(i\),那么与后缀 \(i+1\)\(h\) 共存亡的 \(A\) 前缀就有 \(i\),这是因为如果后面有 \(h_u<h_{i+1}\),此时 \(\delta\) 就会发生改变,减少 \(h_{i+1}-h_u\). 为啥会有很多个与后缀的 \(h\) 共存亡的 \(A\) 前缀呢?其实是在单调栈更新过程中,后缀 \(u\) 将一些后缀踢出,就继承了与那些后缀共存亡的 \(A\) 前缀。

然后贴贴代码吧:

void findAns(int opt) {
    long long inc=0; int tp=0,cnt=0;
    for(int i=2;i<=n;++i) {
        if(h[i]<K) {
            tp=inc=0;
            continue;
        }
        cnt=0;
        if((sa[i-1]-bou-1)*opt<0) {
            ++ cnt;
            inc += h[i]-K+1;
        }
        while(tp && h[i]<=stk[0][tp]) {
            inc -= 1ll*(stk[0][tp]-h[i])*stk[1][tp];
            cnt += stk[1][tp]; -- tp;
        }
        stk[1][++tp] = cnt;
        stk[0][tp] = h[i];
        if((sa[i]-bou-1)*opt>0)
            ans += inc;
    }
}
posted on 2020-02-28 15:22  Oxide  阅读(24)  评论(0编辑  收藏  举报