题意:
给出一个字符串,求其第k大子串出现的位置,即其左右端点下标
思路:
首先构建后缀数组,因为后缀数组会对每个后缀排序,就相当于对所有子串进行了排序,因为子串就是后缀的前缀
对于第i个后缀,其含有不同的子串个数为n-sa[i]-height[i]
我们定义sum[i]=sum[i-1]+n-sa[i]-height[i],求出sum[1n],即求出第1n个后缀所含有不同子串个数的前缀和
如何找第k大子串,对于第一个sum[x]>=k,则第k大子串一定是以sa[x]开头的后缀的一个前缀
由此我们可以确定第k大子串的长度,len=k-sum[x-1]+height[x]
可以这样理解,k表示在整个串中第k大的子串,t=k-sum[x-1]即表示在以sa[x]开头的后缀的第t个前缀就是整个串中第k大子串
但这样算出的第t个前缀可能也在其他后缀中出现,就会产生重复,所以再加上height[x]用来去重,这样算出的len就是第k大子串的长度
我们求出第k大子串第一次出现在第x个后缀中,但并不表示它第一次出现在整个字符串中,因为后缀顺序与真实顺序并不一样
若第k大子串有多个,那它就会出现在很多个后缀中,也就是很多个后缀的公共前缀
我们已经确定了它第一次出现在第x个后缀中,以x为起点,n为终点,二分查找一个最小的ans,使得的区间[x~ans]中每个后缀的公共前缀都大于等于k
即表示sa[x]~sa[ans]这些后缀中都有第k大子串
我们还需在sa[x]~sa[ans]这些后缀中找出最小值,即第k大子串在原串中最早出现的位置就是左端点位置,然后加上len就得到右端点位置
为了实现两次区间查询,我们需要使用两个RMQ,第一个用来处理区间最小lcp,第二个用来处理区间最小sa值
关键点:
1.子串就是后缀的前缀
2.排名靠前的后缀,并不是在原串中位置靠前的后缀,这两者毫无关系
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+10;
typedef long long LL;
int s[MAXN]; // s 数组保存了字符串中的每个元素值,除最后一个元素外,每个元素的值在 1..m 之间,最后一个元素的值为 0
int wa[MAXN], wb[MAXN], wc[MAXN], wd[MAXN]; // 这 4 个数组是后缀数组计算时的临时变量,无实际意义
int sa[MAXN]; // sa[i] 保存第 i 小的后缀在字符串中的开始下标,i 取值范围为 0..n-1
int cmp(int *r, int a, int b, int l) {
return r[a] == r[b] && r[a + l] == r[b + l];
}
void getSA(int *r, int *sa, int n, int m) { // n 为字符串的长度,m 为字符最大值
int i, j, p, *x = wa, *y = wb;
for (i = 0; i < m; ++i) wd[i] = 0;
for (i = 0; i < n; ++i) wd[x[i] = r[i]]++;
for (i = 1; i < m; ++i) wd[i] += wd[i - 1];
for (i = n - 1; i >= 0; --i) sa[--wd[x[i]]] = i;
for (j = 1, p = 1; p < n; j *= 2, m = p) {
for (p = 0, i = n - j; i < n; ++i) y[p++] = i;
for (i = 0; i < n; ++i) if (sa[i] >= j) y[p++] = sa[i] - j;
for (i = 0; i < n; ++i) wc[i] = x[y[i]];
for (i = 0; i < m; ++i) wd[i] = 0;
for (i = 0; i < n; ++i) wd[wc[i]]++;
for (i = 1; i < m; ++i) wd[i] += wd[i - 1];
for (i = n - 1; i >= 0; --i) sa[--wd[wc[i]]] = y[i];
for (swap(x, y), p = 1, x[sa[0]] = 0, i = 1; i < n; ++i)
x[sa[i]] = cmp(y, sa[i - 1], sa[i], j) ? p - 1 : p++;
}
return;
}
int n; //字符串长度
int Rank[MAXN]; // Rank[i] 表示从下标 i 开始的后缀的排名,值为 1..n
int height[MAXN]; // 下标范围为 1..n,height[1] = 0,表示suffix(sa[i-1])和suffix(sa[i])的最长公共前缀,即排名相邻的两个后缀的最长公共前缀
void getHeight(int *r,int *sa,int n) {
int i, j, k = 0;
for (i = 1; i <= n; ++i) Rank[sa[i]] = i;
for (i = 0; i < n; i++) {
if (k) k--;
int j = sa[Rank[i] - 1];
while (r[i + k] == r[j + k]) k++;
height[Rank[i]] = k;
}
return;
}
int lcp[MAXN][30]; //存储lcp
void init_RMQ(int n) //初始化rmq
{
for(int i=1;i<n;i++) lcp[i][0]=height[i];
for(int j=1;(1<<j)<=n;j++)
for(int i=0;i+(1<<j)<=n;i++)
lcp[i][j]=min(lcp[i][j-1],lcp[i+(1<<(j-1))][j-1]);
}
int RMQ(int l,int r) //查询l~r的lcp
{
int k=0;
while((1<<(k+1))<=r-l+1) k++;
int ans=min(lcp[l][k],lcp[r-(1<<k)+1][k]);
return ans;
}
int Min[MAXN][30]; //存储区间最小sa
void init_RMQ2(int n)
{
for(int i=1;i<n;i++) Min[i][0]=sa[i]+1;
for(int j=1;(1<<j)<=n;j++)
for(int i=0;i+(1<<j)<=n;i++)
Min[i][j]=min(Min[i][j-1],Min[i+(1<<(j-1))][j-1]);
}
int RMQ2(int l,int r) //查询l~r的最小sa
{
int k=0;
while((1<<(k+1))<=r-l+1) k++;
int ans=min(Min[l][k],Min[r-(1<<k)+1][k]);
return ans;
}
LL sum[MAXN];
void getsum(int n) //计算前缀和,sum[i]表示到第i个后缀为止的不同子串个数
{
for(int i=1;i<=n;i++)
sum[i]=sum[i-1]+(LL)(n-sa[i]-height[i]);
}
int main()
{
string a;
cin>>a;
n=a.size();
for(int i=0;i<n;i++)
s[i]=a[i]-'a'+1;
s[n]=0; //必须要加!!,将s最后一位置为一个最小值
getSA(s,sa,n+1,30); //!!!必须是n+1!!!
getHeight(s,sa,n);
getsum(n);
init_RMQ(n+1); //注意是n+1!!!!
init_RMQ2(n+1);
int q;
long long k;
cin>>q;
LL lp=0,rp=0;
while(q--)
{
cin>>k;
k^=lp,k^=rp,k++;
if(k>sum[n]) //无解情况
{
lp=rp=0;
}
else
{
int pos=lower_bound(sum+1,sum+1+n,k)-sum; //找到第一个大于等于k的位置,即第k大子串所在后缀的sa
k-=sum[pos-1]; //此时k表示是当前后缀的第k个前缀
int L=sa[pos];
int R=L+k+height[pos]-1;
int len=R-L+1; //len=k+height[pos],表示第k大子串的长度
int ll=pos+1,rr=n;
int ans=pos;
while(ll<=rr) //二分找到一个mid,使得sa[pos]~sa[mid]这几段后缀中都有一个长度大于等于len的公共前缀,即表示存在可行解
{
int mid=(ll+rr)>>1;
if(RMQ(pos+1,mid)<len)
rr=mid-1;
else
{
ans=mid;
ll=mid+1;
}
}
int tmp=RMQ2(pos,ans); //在sa[pos]~sa[ans]这几段后缀中找一个最早出现的sa,即是左端点位置
lp=tmp;
rp=tmp+len-1; //左端点位置加上长度,就是右端点位置
}
cout<<lp<<" "<<rp<<endl;
}
return 0;
}