题意
可以修改一段区间的值 求两端不相交区间相同的最长长度。
思路
可以修改值的话 直接差分解决
求最长长度可以二分一个答案k,然后将height小于k的挖掉。 产生多个区间找到区间,那么区间内的LCP都符合要求,区间与区间不符合,因为两个后缀的LCP等于对应rank之间height的最小值。
怎么保证不相交呢?
找到区间内最大和最小的SA 判断Max-Min是否大于k 不能等于因为是差分数组如果等于k的话会重复。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn=40000+5;
int n,a[maxn],b[maxn];
int r[maxn],y[maxn],sa[maxn],rank[maxn],height[maxn],c[maxn];
//r表示排名 y表示第二关键字排序 sa为后缀数组 rank表示第i个后缀的排名 height表示排i的后缀和前一个的最大公共前缀
int pos[maxn];
void getsa(int *r,int *y,int m,int n)
{
for(int i=0; i<=m; i++) c[i]=0;//初始化桶
for(int i=1; i<=n; i++) c[r[i]=a[i]]++;//压入同中,记录初始r
for(int i=1; i<=m; i++) c[i]+=c[i-1];//记录比i小的有多少
for(int i=n; i>=1; i--) sa[c[r[i]]--]=i;//初始化sa,可能有一样的
for(int len=1; len<=n; len<<=1)
{
int p=0;
for(int i=n; i>=n-len+1; i--) y[++p]=i;//最后的几个长度不够有第二关键字,所以第二关键字最小
for(int i=1; i<=n; i++) if(sa[i]>len) y[++p]=sa[i]-len;//因为之前一次已经排好了一遍,所以直接按顺序找第二关键字对应的第一关键字
for(int i=0; i<=m; i++) c[i]=0;//初始化
for(int i=1; i<=n; i++) c[r[y[i]]]++;//将第一关键字压入桶中
for(int i=1; i<=m; i++) c[i]+=c[i-1];//同开始
for(int i=n; i>=1; i--) sa[c[r[y[i]]]--]=y[i];//算sa
swap(r,y);
p=2; r[sa[1]]=1;//重新求排名第一关键字
for(int i=2; i<=n; i++)
{
if(y[sa[i]]==y[sa[i-1]] && y[sa[i]+len]==y[sa[i-1]+len]) r[sa[i]]=p-1;
else r[sa[i]]=p++;
}
if(p>n) return;//所有的n个都有对应的sa了 不重复,完成。
m=p;
}
}
void getheight()
{
for(int i=1; i<=n; i++) rank[sa[i]]=i;
for(int i=1,k=0; i<=n; i++)
{
if(k>0) k--;
int t=sa[rank[i]-1];
while(a[i+k]==a[t+k]) k++;
height[rank[i]]=k;
}
}
void init()
{
memset(sa,0,sizeof(sa));
memset(height,0,sizeof(height));
memset(b,0,sizeof(b));
memset(a,0,sizeof(a));
memset(r,0,sizeof(r));
memset(y,0,sizeof(y));
for(int i=1; i<=n; i++)
{
scanf("%d",&b[i]); a[i]=b[i]-b[i-1];
}
for(int i=1; i<=n; i++) a[i]+=88;
}
bool can(int x)
{
pos[0]=0;
for(int i=1; i<=n; i++)
{
if(height[i]<x) pos[++pos[0]]=i;
}
if(pos[pos[0]]<n) pos[++pos[0]]=n+1;
for(int i=2; i<=pos[0]; i++)
{
int minn=0x7ffffff,maxx=0;
for(int j=pos[i-1]+1; j<pos[i]; j++)
{
minn=min(minn,min(sa[j],sa[j-1])); maxx=max(maxx,max(sa[j-1],sa[j]));
if(maxx-minn>x) return true;//因为是差分所以要用大于x
}
}
return false;
}
int solve()
{
int l=0,r=n;
while(l<r)
{
int mid=(l+r+1)>>1;
if(can(mid)) l=mid;
else r=mid-1;
} l++;
if(l<5) l=0;
return l;
}
int main()
{
freopen("test.in","r",stdin);
freopen("test.out","w",stdout);
scanf("%d",&n);
init();
getsa(r,y,177,n); getheight();
for(int i=1; i<=n; i++)
{
cout<<height[i]<<endl;
}
printf("%d\n",solve());
return 0;
}
学习笔记
SA的求解代码注释的比较易懂了 实在不懂可以到网上找点图看看
height数组的求解方法
height数组的求解需要用到一个性质
令h[i]=height[rank[i]]
那么就有 h[i]>=h[i−1]−1h[i]>=h[i−1]−1
证明 i前面的一个后缀是i-1,i比i-1少一个字符
令 k=sa[rank[i-1]-1] 即排在i-1前的一个后缀。
那么k+1就比k要少一个字符
设k与i-1的LCP为[l,r] 很显然的是因为k+1只比k少一个 i只比i-1少一个那么 k+1与i至少有[l+1,r]的是完全相同的
那么LCP(k+1,i)>=LCP(k,i−1)−1即h[i−1]−1LCP(k+1,i)>=LCP(k,i−1)−1即h[i−1]−1
而我们又知道两个后缀的LCP等于对应rank之间height的最小值。
所以h[i]>=LCP(k+1,i)>=LCP(k,i−1)−1即h[i−1]−1h[i]>=LCP(k+1,i)>=LCP(k,i−1)−1即h[i−1]−1
代码上边有
常用技巧
- 二分一个值把比这个值小的height去掉剩下一段段区间。
- 将height排序然后一个一个的将区间拆成两半。
- 求长度为L的字串出现几次的话找到L,2*L,3*L,4*L这些点最少两次的话最少要覆盖两个点,可以枚举每对点,比如说是L,2*L求以L为开头的后缀和以2*L为开头的后缀的LCP长度len1 和以2*L为开头前缀和L为开头的前缀的最长公共后缀(LCS?) 长度len2 len1+len2就是以L为循环节的最长长度。
有时间补上图