Gym-102411L
L - Lengths and Periods
tag: 后缀数组,启发式合并
给定字符串 \(s\) ,设 \(w\) 是 \(s\) 的某一子串, 若有 \(w = x^nx_0\) ,且 \(x_0\) 为 \(x\) 的前缀, 称 \(\dfrac {|w|} {|x|}\) 为 \(w\) 的 \(critical\ exponent\) 。 求 \(s\) 的所有子串的最大的 \(critical\ exponent\) 。 ( \(|s| \le 2e5\) )
构造答案
首先, 对于下标 \(i < j\) ,可以构造出答案
\[ans = \dfrac {lcp(rk[i],rk[j]) + j - i} {j - i}
\]
$lcp(rk[i],rk[j]) $ 表示第 i 个后缀与第 j 个后缀的 lcp
构造出的字符串就是 s.substr(i,j-i) + lcp(i,j)
求解最大值
有
\[lcp(x,y) = min_{i = x + 1}^y height_i
\]
可以降序扫描 height 数组,扫描到下标 \(idx\) 的时候合并 \(idx\) 和 \(idx-1\)
这样每次扫描到的都是一段连续区间的 height 最小值,就是 lcp ,所以我们需要寻找最小的 \(j - i\)
可以用集合来保存所有的后缀开始的下标,用并查集 + 启发式合并完成该步骤
/*
* @Author: zhl
* @LastEditTime: 2021-03-01 19:27:08
*/
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10;
typedef long long ll;
int n, m;
char s[N];
int sa[N], x[N], y[N], c[N], rk[N], height[N];
/*
sa[i] :
x[i] : 第一关键字
y[i] : 第二关键字
c[i] : 桶
rk[i] :
*/
void get_sa(){
/*
根据首字母进行基数排序
*/
for (int i = 1; i <= n; i++) c[x[i] = s[i]] ++;
for (int i = 2; i <= m; i++) c[i] += c[i - 1];
for (int i = n; i; i--) sa[c[x[i]] --] = i; // s[i] = k 表示 rank i 的串从 k 位置开始
/*
开始倍增
*/
for (int k = 1; k <= n; k <<= 1)
{
/*
此时已经根据前k个字母排好序,要根据2*k个字母排好序
先按照后 k 个字母(第二关键字)排序,再根据前 k 个字母排序(稳定排序不会改变相对位置)
*/
int num = 0;
for (int i = n - k + 1; i <= n; i++) y[++num] = i; // 这个区间第二关键字是 空串
for (int i = 1; i <= n; i++) //已经按前 k 个字母排序, 第 i 个后缀的第二关键字是 第 i + k的第一关键字
if (sa[i] > k)
y[++num] = sa[i] - k;
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;
swap(x, y); //把 x 暂时存到 y 中
//离散化
x[sa[1]] = 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 (num == n) break;
m = num;
}
}
/*
h[i] = height[rk[i]]
h[i] >= h[i-1] - 1
k 是当前的 h[i]
*/
void get_height()
{
for (int i = 1; i <= n; i++) rk[sa[i]] = i;
for (int i = 1, k = 0; i <= n; i++)
{
if (rk[i] == 1) continue;
if (k) k--; //只需要从 h[i-1] - 1 开始枚举就可以
int j = sa[rk[i] - 1];
while (i + k <= n && j + k <= n && s[i + k] == s[j + k]) k++;
height[rk[i]] = k;
}
}
set<int>st[N];
int fa[N],ans[N],r[N];
void UFS_init(){
for(int i = 1;i <= n;i++)fa[i] = i,st[i].insert(sa[i]),ans[i] = n;
}
int find(int a){
return a == fa[a] ? a : fa[a] = find(fa[a]);
}
int merge(int a,int b){
a = find(a);b = find(b);
if(st[a].size() > st[b].size())swap(a,b);
fa[a] = b;
ans[b] = min(ans[b], ans[a]);
for(int v : st[a]){
auto p = st[b].lower_bound(v);
if(p != st[b].end()) ans[b] = min(ans[b], *p - v);
if(p != st[b].begin()) ans[b] = min(ans[b], v - *prev(p));
st[b].insert(v);
}
st[a].clear();
return ans[b];
}
int main()
{
scanf("%s", s + 1);
n = strlen(s + 1), m = 122;
get_sa();
get_height();
UFS_init();
for(int i = 1;i <= n;i++) r[i] = i;
sort(r + 1,r + 1 + n,[&](int a,int b){
return height[a] > height[b];
});
int ansx = 1,ansy = 1;
for(int i = 1;i <= n;i++){
int idx = r[i];
int y = merge(idx,idx - 1);
int x = y + height[idx];
if(1ll * x * ansy > 1ll * y * ansx){
ansx = x;ansy = y;
}
}
int g = __gcd(ansx,ansy);
ansx /= g,ansy /= g;
printf("%d/%d\n",ansx, ansy);
}
解法二
这道题也可以用 SAM 求解, 反串建 SAM , 两个实点的 LCA 的 len 就是原串中的 LCP
然后搞一个 dsu on Tree