字符串——从入门到入门
最小表示
循环同构
当一个字符串 \(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;
}
}
}