poj3693 Maximum repetition substring (后缀数组 字典序最小的 重复次数最多的子串)

题目

重复次数R定义为一个串中重复的最大次数,

例如,abababab可以看做是R=4的ab的重复,此时输出abababab

而abcdabcd可以看成是R=2的abcd的重复,

给定一个长为n(n<=1e5)的小写字母串,

输出其子串中满足R是最大的,

且该子串是所有R最大的串中字典序最小的这个子串

思路来源

罗穗骞《后缀数组——处理字符串的有力工具》

POJ3693:Maximum repetition substring(后缀数组+RMQ)_- 第1组 输入: ccabababc 输出: ababab - 第2组 输入: daabbccaa-CSDN博客

题解

图片引自罗穗骞论文《后缀数组——处理字符串的有力工具》,感觉很清楚了,但没讲往前匹配怎么弄

首先,构建后缀数组,构建sa、height、rank,预处理倍增rmq、lcp等等,

这个板子所有下标都是[0,n-1]的,且不用后面补一个空字符

枚举长度i,起点j,出现次数至少为2的,lcp(j-i,j)一定会大于等于i

j-i和j的lcp显然是往后接可行的最大长度,[j-i,j)[j,j+lcp),长为i+lcp,出现次数num为(i+lcp)/i

但lcp%i不为0即不能整除时,意味着把j-i往左挪有可能仍然是这个num,但可能字典序更小,

所以,此时保留这个长度,最后再去按sa增序遍历,

特别的,如果向左挪i-lcp%i,

即补全不能整除的剩下的长度部分,也能满足lcp的对应要求时,说明此时还可以多凑齐一个循环节,num++

更新最大次数,最大次数相同时,把所有可行长度都加到数组里,

然后按字典序sa增序枚举下标i,如果对于i来说存在一个长度lenj满足要求就输出答案

显然,对于i相同来说,越短的前缀字典序越小,重复次数相同即越短的越小,

而两个合法的串来说,sa[i]开头的长度为len*mx的串,是不会比sa[i+1]开头的长度为(len-1)*mx字典序更大的

反证法可以证明,这违反了sa[i]<sa[i+1],

然后就有先按字典序增序找,再按长度增序找,找到第一个输出即可的正确性

虽然代码统一了最大出现次数为1的情况,但可以采取一点小技巧使时间更短

先置最大出现次数为1,长度为1,这样可以省去对其他的出现次数为1的判断

复杂度有点迷,但很难卡掉就是了

还有一种做法是,后面的部分仍然用LCP,前面的部分由于有一段是出现次数相同的,

此时就从j-i开始往回跳,一个一个往前比较,如果j-i-1出现次数相同且rank更小就改变左端点为j-i-1

此题有弱化版本spoj687,没有字典序要求,直接输出mx值即可

复杂度就很稳了,O(n/1+n/2+...+n/n)=O(nlogn)

代码

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e5+5;
int ans[N],cnt,ca;
namespace SA{
	char s[N];
	int n,m;
	int *x,*y,X[N],Y[N],c[N],sa[N],height[N],Rank[N];
	int dp[N][18],lg[N];
	void get_sa(int _m=30){
		m=_m;
	    x=X,y=Y;
	    for (int i=0;i<m;++i) c[i]=0;
	    for (int i=0;i<n;++i) x[i]=s[i]-'a',++c[x[i]];
	    for (int i=0;i<m;++i) c[i]+=c[i-1];
	    for (int i=n-1;i>=0;--i) sa[--c[x[i]]]=i;
	
	    for (int k=1;k<=n;k<<=1){
	        int p=0;
	        for (int i=n-k;i<n;++i) y[p++]=i;
	        for (int i=0;i<n;++i) if (sa[i]>=k) y[p++]=sa[i]-k;
	
	        for (int i=0;i<m;++i) c[i]=0;
	        for (int i=0;i<n;++i) ++c[x[y[i]]];
	        for (int i=0;i<m;++i) c[i]+=c[i-1];
	        for (int i=n-1;i>=0;--i) sa[--c[x[y[i]]]]=y[i];
	
	        swap(x,y);
	        p=1;x[sa[0]]=0;
	        for (int i=1;i<n;++i)
	            x[sa[i]]=y[sa[i-1]]==y[sa[i]]&&((sa[i-1]+k<n?y[sa[i-1]+k]:-1)==(sa[i]+k<n?y[sa[i]+k]:-1))?p-1:p++;
	        if (p>n) break;
	        m=p;
	    }
	}
	void get_height(){
	    for (int i=0;i<n;++i) Rank[sa[i]]=i;
	    int k=0;height[0]=0;
	    for (int i=0;i<n;++i){
	        if (!Rank[i]) continue;
	        if (k) --k;
	        int j=sa[Rank[i]-1];
	        while (i+k<n&&j+k<n&&s[i+k]==s[j+k]) ++k;
	        height[Rank[i]]=k;
	    }
	}
	void get_Log(){
		lg[0]=-1;
		for(int i=1;i<N;++i){
			lg[i]=lg[i>>1]+1;
		}
	}
	void get_ST(){
		for(int i=0;i<n;++i){
			dp[i][0]=height[i];
		}
		for(int len=1;(1<<len)<=n;++len){
			for(int l=0;l+(1<<len)-1<n;++l){
				dp[l][len]=min(dp[l][len-1],dp[l+(1<<(len-1))][len-1]);
			}
		}
	}
	int rmq(int l,int r){
		int k=lg[r-l+1];
		return min(dp[l][k],dp[r-(1<<k)+1][k]);
	} 
	int lcp(int l,int r){
		l=Rank[l],r=Rank[r];
		if(l==r)return n-sa[l];
		if(l>r)swap(l,r);
		return rmq(l+1,r);
	}
	//Rank[i]:下标位置在i的后缀的排名 
	//sa[i]:后缀排名第i的下标位置  
	//Rank和sa互为反函数 范围均在[0,n-1] 
	//height[i]:排名第i和排名第i-1的LCP长度 
	void PR(){
		string p(s);
		for(int i=0;i<n;++i)
		printf("Rank[%d]:%d\n",i,Rank[i]);
		for(int i=0;i<n;++i){
		 	printf("sa[%d]:%d ",i,sa[i]);
		 	cout<<p.substr(sa[i])<<endl;
		}
		for(int i=0;i<n;++i)
		printf("height[%d]:%d\n",i,height[i]);
	}
};

using namespace SA;
int main(){
	get_Log();
	while(~scanf("%s",s) && strcmp(s,"#")){
        n=strlen(s);
        get_sa();
        get_height();
        get_ST();
        //PR();
        int mx=1;
        ans[cnt=1]=1;
        for(int i=1;i<=n/2;++i){//枚举长度 
        	for(int j=i;j<n;j+=i){
        		int x=lcp(j-i,j);
        		if(x<i)continue;//说明只出现了一次 
        		int num=(i+x)/i;
        		if(x%i){
        			int off=i-x%i;//凑齐一个循环节 
        			if(j-i-off>=0 && lcp(j-i-off,j-off)>=off){
        				num++;
        			}
        		}
        		if(num>mx){
        			mx=num;
        			ans[cnt=1]=i;//记录可行的长度 
        		}
        		else if(num==mx){
        			if(i!=ans[cnt-1]){//长度去重 
        				ans[++cnt]=i;
        			} 
        		} 
        	} 
        }
        bool f=1;
        for(int i=0;i<n && f;++i){//增序枚举rank 字典序最小 
        	for(int j=1;j<=cnt && f;++j){
        		int len=ans[j];
        		if(lcp(sa[i],sa[i]+len)>=(mx-1)*len){
        			printf("Case %d: ",++ca);
        			for(int k=1;k<=mx;++k){
        				for(int l=sa[i];l<sa[i]+len;++l){
        					putchar(s[l]);
        				}
        			}
        			puts("");
					f=0;
        		}
        	} 
        }
    }
    return 0; 
} 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code92007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值