字符串——从入门到入门

最小表示

循环同构

当一个字符串 \(s\) 可以选定一个位置 \(i\) 满足 \(s[i\dots n]+s[1\dots i-1]=t\),则称 \(s\)\(t\) 循环同构。

最小表示

字符串 \(s\) 的最小表示为与 \(s\) 循环同构的所有字符串中字典序最小的字符串。

实现

每次比较 \(i\)\(j\) 开始的循环同构,把当前比较到的位置记作 \(k\),每次遇到不同的字符时跳过较大的,最后剩下的就是最优解。

int get_min_start(){
	int i=0,j=1,k=0;
	while(std::max(i,std::max(j,k))<n){
		if(s[(i+k)%n]==s[(j+k)%n])k++;
		else{
			if(s[(i+k)%n]>s[(j+k)%n])i++;
			else j++;
			k=0;
			if(i==j)i++;
		}
	}
	return std::min(i,j);
}

考虑时间复杂度,该实现方法在随机数据下表现良好,但当字符串中出现多个连续重复子串时,复杂度降低,可能达到 \(\Theta(n^2)\)

优化

暴力比较,如果 \(i\) 开头比 \(j\) 开头更优,且在第 \(k\) 位猜更优,那么对于 \(t\le k\)\(i+t\) 开头也一定比 \(j+k\) 开头更优。

跳过不优的即可,复杂度 \(\Theta(n)\)

int get_min_start(){
	int i=0,j=1;k=0;
	while(std::max(i,std::max(j,k))>n){
		if(s[(i+k)%n]==s[(j+k)%n])k++;
		else{
			if(s[(i+k)%n]>s[(j+k)%n])i+=k+1;
			else j+=k+1;
			if(i==j)i++;
			k=0;
		}
	}
	return std::min(i,j);
}

Manacher

给定一个长度为 \(n\) 的字符串 \(s\),请找到所有对 \((i,j)\) 使得子串 \(s[i\dots j]\) 为一个回文子串。

解释

暴力 \(\Theta(n^2)\),考虑线性。

\(d1_i\)\(d2_i\) 分别表示以 \(i\) 为中心的长度为奇数/偶数的回文串个数,同时也表示了以 \(i\) 为中心的最长回文串的半径长度(从 \(i\) 到最右端)。

发现,如果以某个位置 \(i\) 为中心,有一个长度为 \(l\) 的回文串,那么我们有以 \(i\) 为中心的长度为 \(l-2,l-4,\dots\) 的回文串,接下来考虑线性计算 \(d1,d2\) 两个数组。

解法

以计算 \(d1\) 为例,\(d2\) 的计算与此大致相同。

设我们当前要计算 \(d1_i\)\((l,r)\) 表示已找到的最靠右的子回文串的边界(初始值为 \(l=0,r=-1\)),考虑 \(i\) 的取值情况。

\(i\) 处于当前子回文串外时(即 \(i>r\)),暴力向外扩展。

\(i\le r\) 时,我们定义 \(j\)\(i\)\((l,r)\) 中的对称位置,所以 \(d1_i=d1_j\),当到达为计算的位置时,同样暴力向外扩展。

对于复杂度,每次暴力扩展均会使 \(r\) 增加 \(1\),并且 \(r\) 的取值使单调不递减的,所以共会进行 \(\Theta(n)\) 次迭代,而另一部分的复杂度也是线性的,所以总复杂度为 \(\Theta(n)\)

void get_d1(){
	int l=0,r=-1;
	for(int i=1;i<n;i++){
		int k=(i>r)?1:std::min(d1[l+r-i],r-i+1);
		while(i-k>=0&&i+k<n&&s[i-k]==s[i+k])k++;
		d1[i]=k--;
		if(i+k>r){
			l=i-k;
			r=i+k;
		}
	}
}

void get_d2(){
	int l=0,r=-1;
	for(int i=0;i<n;i++){
		int k=(i<r)?0:std::min(d2[l+r-i_1],r-i+1);
		while(i-k-1>=0&&i+k<n&&s[i-k-1]==s[i+k])k++;
		d2[i]=k--;
		if(i+k>r){
			l=i-k-1;
			r=i+k;
		}
	}
}
posted @ 2024-07-30 16:51  CheZiHe929  阅读(18)  评论(0编辑  收藏  举报