后缀数组da+dc3

    以前一看到字符串的题目就有些畏惧,感觉无从下手,只会个KMP和AC自动机,很多情况下都是力不从心,所以打算学各种高端的算法,来解决这类问题,后缀数组应该是性价比教高的吧,至少很简单,容易理解,算法实现是另一回事,毕竟ACM比赛是可以带模版的,所以它也就显得简单实用了,下面一起看看这个实用的数据结构吧!

    首先给定一些定义:

    字符串S,s[ i ]表示第i个字符,s[ i , j ]表示第i个字符到第j个字符组成的子串, len表示S的长度

    后缀, suffix( i )=s[ i, len-1] ,可见后缀是特殊的一类子串


    后缀数组sa[ i ]表示将所有后缀按字典序排序后,排在第 i 位的后缀

    名词数组rank[ i ],表示suffix(i)排在第几位,它是sa数组的逆运算,即rank[ sa[i] ]=i

    高度数组height[ i ]表示 sa[ i ]与sa[ i-1 ]的最长公共前缀

   后缀数组主要部分就是sa,rank 和 height这三个数组,整个结构相当简单,不过在构造后缀数组时,我们发现,最简单的方法往往是平方级的,很容易就超时,完全不可用,不过有两种方法来快速的构造后缀数组。


第一种方法:倍增思想 da

     我们在普通排序的过程中忽略了后缀之间的联系,导致太多的重复比较,倍增的思想充分利用已经得到的结果,从而降低了整体复杂度

     suffix(i)k表示长度为2^k的后缀,即s[ i, i+2^k ],我们可以很轻易的得到suffix(i)0的排序结果,而对于suffix(i)k的排序,我们可以通过suffix(i)k-1快速得到,suffix(i)k<suffix(j)k,当且仅当suffix(i)k-1<suffix(j)k-1||(suffix(i)k-1==suffix(j)k-1&&suffix(i+2^k-1)k-1<suffix(j+2^k-1)k-1),通过数学归纳法,我们可以知道这个过程是正确的,当2^k>len时,我们可以得到最终的排序结果,总排序次数为log len,当使用基数排序时,总复杂度是O(len*log len),基本上可以过掉大部分题


第二种方法: DC3

    这种方法构思比较巧妙,不过同样也是充分利用前面的比较结果,这次我们将原字符串分成两部分,一部分为i mod 3=0的后缀,另一部分i mod 3!=0的后缀,我们可以发现,第一部分的结果容易通过第二部分的结果得到,而第二部分可以通过形成一个新的串,长度为2/3len,然后用同样的方式得到新串的排序结果,可以证明,所有串的总和为3len,所以复杂度自然为k*len,k表示常数。最后结果就是两部分归并后的结果


本来想写详细的解释的,发现时间越来越紧迫,等做些专题后再来整理吧,学习这两种方法主要看 《2004 年国家集训队论文 许智磊——后缀数组》主要介绍了理论和da算法,

《2005年集训队作业 周源——线性后缀排序算法》还有《2009年 国家集训队论文 罗穗骞——后缀数组——处理字符串的有力工具》前两篇注重理论,后一篇给出实现和一些习题


下面贴下我自己实现的模版,实现后发现和罗穗骞大神的差不多,我太弱了= =

模版:

template <typename T, int LEN>
struct suffixarray
{
	int str[LEN*3],sa[LEN*3];
	int rank[LEN],height[LEN];
	int id[LEN];
	int best[LEN][20];
	int len;
	bool equal(int *str, int a, int b)
	{
		return str[a]==str[b]&&str[a+1]==str[b+1]&&str[a+2]==str[b+2];
	}
	bool cmp3(int *str, int *nstr, int a, int b)
	{
		if(str[a]!=str[b])return str[a]<str[b];
		if(str[a+1]!=str[b+1])return str[a+1]<str[b+1];
		return nstr[a+b%3]<nstr[b+b%3];
	}
	void radixsort(int *str, int *sa, int *res, int n, int m)
	{
		int i;
		REP(i,m)id[i]=0;
		REP(i,n)++id[str[sa[i]]];
		REP(i,m)id[i+1]+=id[i];
		DOWN(i,n-1,0)res[--id[str[sa[i]]]]=sa[i];
	}
	void dc3(int *str, int *sa, int n, int m)
	{
		#define F(x) ((x)/3+((x)%3==1?0:one))
		#define G(x) ((x)<one?(x)*3+1:((x)-one)*3+2)
		int *nstr=str+n, *nsa=sa+n, *tmpa=rank, *tmpb=height;
		int i,j,k,len=0,num=0,zero=0,one=(n+1)/3;
		REP(i,n)if(i%3)tmpa[len++]=i;
		str[n]=str[n+1]=0;
		radixsort(str+2, tmpa, tmpb, len, m);
		radixsort(str+1, tmpb, tmpa, len, m);
		radixsort(str+0, tmpa, tmpb, len, m);
		nstr[F(tmpb[0])]=num++;
		UPTO(i,1,len-1)
			nstr[F(tmpb[i])]=equal(str,tmpb[i-1],tmpb[i])?num-1:num++;
		if(num<len)dc3(nstr,nsa,len,num);
		else REP(i,len)nsa[nstr[i]]=i;
		if(n%3==1)tmpa[zero++]=n-1;
		REP(i,len)if(nsa[i]<one)tmpa[zero++]=nsa[i]*3;
		radixsort(str, tmpa, tmpb, zero, m);
		REP(i,len)tmpa[nsa[i]=G(nsa[i])]=i;
		i=j=0;
		REP(k,n)
		if(j>=len||(i<zero&&cmp3(str,tmpa,tmpb[i],nsa[j])))sa[k]=tmpb[i++];
		else sa[k]=nsa[j++];
	}
	bool cmp(int *str, int a, int b, int k)
	{
	    return str[a]==str[b]&&str[a+k]==str[b+k];
	}
	void da(int *str, int *sa, int n, int m)
	{
	    int *x=rank, *y=height;
	    int i,j,k,num=1;
	    /** 获取长度为1的结果*/
	    REP(i,n)y[i]=i;
	    radixsort(str,y,sa,n,m);
	    REP(i,n)x[i]=str[i];
	    REP(i,n)x[i+n]=y[i+n]=-1;
	    /** 循环排序直到结果唯一或2^k大于n*/
	    for(k=1;num<n;k<<=1,m=num)
	    {
	        /** 通过k-1的结果或得当前结果*/
	        num=j=0;
	        UPTO(i,max(0,n-k),n-1)y[j++]=i;
	        REP(i,n)if(sa[i]>=k)y[j++]=sa[i]-k;
	        radixsort(x,y,sa,n,m);
	        swap(x,y);
	        /** 通过当前结果得到新字符串*/
	        x[sa[0]]=num++;
	        UPTO(i,1,n-1)
                x[sa[i]]=cmp(y,sa[i-1],sa[i],k)?num-1:num++;
	    }
	}
	void initSA(T *s, int n,int m)
	{
		int i,j,k=0;
		str[len=n]=0;//末尾增加一个0,这样就省去一些特殊情况的讨论,也就是最后一个mod 3刚好等于0
		REP(i,n)str[i]=s[i];
		dc3(str,sa,n+1,m);
		REP(i,n)sa[i]=sa[i+1];//第0小的默认为最后一个字符0,所以答案向前移动一位,da算法不用

		//da(str,sa,n,m);
		REP(i,n)rank[sa[i]]=i;
		REP(i,n)//计算height数组
		{
			if(k)--k;
			if(rank[i])for(j=sa[rank[i]-1];str[i+k]==str[j+k];++k);
			else k=0;
			height[rank[i]]=k;
		}
	}
	void initRMQ()
	{
		int i,j;
		REP(i,len)best[i][0]=height[i];
		for(j=1;(1<<j)-1<len;++j)
			for(i=0;i+(1<<j)-1<len;++i)
				best[i][j]=min(best[i][j-1],best[i+(1<<(j-1))][j-1]);
	}
	int RMQ(int l, int r)
	{
		int k=0;
		while(l+(1<<k)-1+1<r-(1<<k)+1)++k;
		return min(best[l][k],best[r-(1<<k)+1][k]);
	}
	int LCPSA(int a, int b)
	{
		if(a==b)return len-sa[a];
		if(++a>b)swap(a,b);
		return RMQ(a,b);
	}
};

相关习题:

poj 2774 Long Long Message 难度:1

这题求两个字符串的最长公共子串,算是模版题了

POJ 3261 Milk Patterns 难度:1

这题用到最长公共前缀,参考题解

poj 3294 Life Forms 难度:1

这题是上题的加强版吧,参考题解

poj 1226 Substrings 难度:2

这题跟3294差不多,不过这回加上回文,题解同上

poj 1743 Musical Theme 难度:2

楼教主男人八题,最长不重叠重复子串,参考题解

spoj 220 Relevant Phrases of Annihilation 难度:2

n个串的最长公共重复2次子串,参考题解

poj 3415 Common Substrings  难度:4

长度大于k的相同子串对数xian 后缀数组+单调桟统计,参考题解

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值