【POJ1743】Musical Theme & SA的模板和学习笔记

2 篇文章 0 订阅
2 篇文章 0 订阅

题意

可以修改一段区间的值 求两端不相交区间相同的最长长度。

思路

可以修改值的话 直接差分解决
求最长长度可以二分一个答案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[i1]1 h [ 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,i1)1h[i1]1 L C P ( k + 1 , i ) >= L C P ( k , i − 1 ) − 1 即 h [ i − 1 ] − 1
而我们又知道两个后缀的LCP等于对应rank之间height的最小值。
所以 h[i]>=LCP(k+1,i)>=LCP(k,i1)1h[i1]1 h [ i ] >= L C P ( k + 1 , i ) >= L C P ( k , i − 1 ) − 1 即 h [ i − 1 ] − 1
代码上边有

常用技巧

  1. 二分一个值把比这个值小的height去掉剩下一段段区间。
  2. 将height排序然后一个一个的将区间拆成两半。
  3. 求长度为L的字串出现几次的话找到L,2*L,3*L,4*L这些点最少两次的话最少要覆盖两个点,可以枚举每对点,比如说是L,2*L求以L为开头的后缀和以2*L为开头的后缀的LCP长度len1 和以2*L为开头前缀和L为开头的前缀的最长公共后缀(LCS?) 长度len2 len1+len2就是以L为循环节的最长长度。
    有时间补上图
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值