zoukankan      html  css  js  c++  java
  • POJ 3261 Milk Patterns(后缀数组+二分答案)

    题意

    给定一个长度为 (n​) 的由正整数构成的串和一个整数 (K​),求至少出现 (K​) 次的串的最长长度(可以重叠)。

    (1 leq nleq 20000​)

    (2leq Kleq n)

    思路

    变量及其含义

    后缀数组(SA)是一个比较灵活的处理字符串有关问题的算法,它能对一个串的所有后缀进行排序。我们用串 abaaab 作例子,来分析这个算法。

    sa(suffix array)

    算法名由来的数组,可见该数组是核心。(sa[i]) 的定义是排第 (i) 的后缀开头在哪个位置。

    sa suffix
    3 aaab
    4 aab
    5 ab
    1 abaaab
    6 b
    2 baaab

    比如例串的 (sa) 数组如上表

    rk(rank)

    (rk) 数组是 (sa) 数组的逆运算,(sa) 是排名映射到位置,而 (rk) 则是位置映射到排名,即 (rk[sa[i]]=i)

    H(height)

    (H) 数组也就是常说的高度数组,它是在求解后缀数组问题时一个重要的数组,满足 (H[i]= ext{lcp}(sa[i],sa[i-1])) ,其中 ( ext{lcp}) 表示最长公共前缀。高度数组的含义也就是排名相邻的两个后缀的最长公共前缀长度。

    这么说可能不太形象,那么我们把上面的表格再拓展一列。

    sa H suffix
    3 / aaab
    4 2 aab
    5 1 ab
    1 1 abaaab
    6 0 b
    2 1 baaab

    具体流程

    一般后缀数组使用 (O(nlog n)) 的倍增算法进行构造,当然有一个更加优秀的叫做 ( ext{DC3}​) 的算法(我不会)是线性的,听说较难实现。

    基数排序

    为了能实现较快的排序,有必要实现一个线性的排序算法,而基排就是一个相当灵活的线形排序算法。基数排序这里不在详细介绍,这里仅仅把它挖的更深一点,方便理解后缀数组中的基排。我们以它的一次排序(其实就是计数排序)来演示。

    比如直接把长度为 (n​) ,数值属于 ([1,n]​)(a​) 数组排序,放入 (b​) 数组中:

    FOR(i,1,m)c[i]=0;
    FOR(i,1,n)c[a[i]]++;
    FOR(i,2,m)c[i]+=c[i-1];
    DOR(i,n,1)b[c[a[i]]--]=a[i];
    

    而直接按照下标放置(即把 (a_i​) 当作比较关键字,对一个(1​)(n​) 的全排列进行排序),只用在第 (4​) 行把 (a[]​) 去掉即可。

    FOR(i,1,m)c[i]=0;
    FOR(i,1,n)c[a[i]]++;
    FOR(i,2,m)c[i]+=c[i-1];
    DOR(i,n,1)b[c[a[i]]--]=i;
    

    再拓展一下,把一个长度为 (n​)(a​) 数组,按比较关键字 (f​) 数组进行排序,放入 (b​) 数组中,其中 (f_iin[1,m]​)

    FOR(i,1,m)c[i]=0;
    FOR(i,1,n)c[f[a[i]]]++;
    FOR(i,2,m)c[i]+=c[i-1];
    DOR(i,n,1)b[c[f[a[i]]]--]=a[i];
    

    利用基排属于稳定排序的顺序,可以在此基础上继续优先度更高的排序。

    现在对基排的理解有没有更深?九种排序什么的要不仅仅会打板子,还要知道灵活运用才行。

    倍增过程

    回忆一下我们学过的倍增。如果可以依次求出 (1,2,3,4) 直到 (k) 的答案,那么有时稍作修改,通过依次求 (2^0,2^1,2^2,2^3) 的答案,就可以一直到 (2^k) 的答案。

    那么构造后缀数组的倍增也是如此,我们想得到后缀的排序,把问题抽象一下,就是求从每个字符开始长度为 (k​) 的串的顺序((k>=n​) ,越界则该位置无穷小),我们把问题转化成给 (n​) 个长为 (k​) 的串排序( (k​)(2​) 的正整数次幂),通过长为 (k​) 的串的顺序合并出长为 (2k​) 的串的顺序。而长为 (1​) 则是直接对每个字符排序。

    每次合并的过程就是给每第 (i) 个位置一个 ((rk[i],rk[i+k])) 的二元组,按这个排序得到一个新的顺序,就是长为 (2k) 的串的顺序。

    更加具体的流程只能通过代码+注释讲解了。

    int *x=tmp[0],*y=tmp[1],*c=tmp[2];	//c是基数排序的计数数组
    x[n+1]=y[n+1]=0;		//监视哨防止越界
    FOR(i,1,m)c[i]=0;
    FOR(i,1,n)c[x[i]=s[i]]++;
    FOR(i,2,m)c[i]+=c[i-1];
    DOR(i,n,1)sa[c[x[i]]--]=i;	//先解决2^0的排序
    for(int k=1;k<=n;k<<=1)
    {			//x在这里表示长为k的串的rk,y在这里表示二元组的后面那一维的顺序
    	int p=0;
    	FOR(i,n-k+1,n)y[++p]=i;		//i+k>n,故rk[i+k]为极小值
    	FOR(i,1,n)if(sa[i]>k)y[++p]=sa[i]-k;
    	FOR(i,1,m)c[i]=0;				//对y数组以x为关键字排序,放在sa中
    	FOR(i,1,n)c[x[y[i]]]++;
    	FOR(i,2,m)c[i]+=c[i-1];
    	DOR(i,n,1)sa[c[x[y[i]]]--]=y[i];
    	std::swap(x,y);		//其实这里就是把x数组赋给y,并丢掉原来的x
    	p=x[sa[1]]=1;
    	FOR(i,2,n)x[sa[i]]=y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]?p:++p;	//x在这里表示长为2k的串的rk,通过已经得到的顺序sa算出
    	if(p==n)break;	//如果rk已经两两不同
    	m=p;
    }
    FOR(i,1,n)rk[sa[i]]=i;	//逆运算,得到rk
    

    高度数组

    • 定理:(H[rk[i+1]]>=H[rk[i]]-1)

    用人类的语言阐述一下,就是在原字符串 (i+1) 位置在后缀数组对应位置中的 (H) 最少只可能比 (i) 的少 (1)

    这个定理还是比较显然的,设原字符串的 (i) 位置 ( ext{lcp})(l) ,那么 (i+1) 位置保底也有一个 ( ext{lcp}) 长度为 (l-1) (就是与 (i) 位置形成 ( ext{lcp}) 的那个后缀去掉开头一位),至于有没有更长就不知道了。

    那我们只用按原串的顺序,求出后缀数组对应位置的高度即可,通过 ( ext{k- -})( ext{k++}) 的次数分析,可得复杂度 (O(n))

    int k=0;
    FOR(i,1,n)
    {
    	if(k)k--;
    	if(rk[i]==1)continue;
    	int j=sa[rk[i]-1];
    	while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])k++;
    	H[rk[i]]=k;
    }
    

    如何用SA解题

    利用 (sa,rk,H) 三个关键的数组,将字符串问题转化成这三个数组上的问题,是后缀数组解题的一般套路。

    后缀的前缀就是子串,是后缀结构的核心原理。

    我们此题题,求至少出现 (K) 次的串的最长长度为例进行分析。

    首先求出后缀数组,至少出现 (K) 次,就意味着一段后缀数组上的一段长度大于 (K) 的区间,它们相邻的 ( ext{lcp})(H) 值)最大。那么很自然的想到二分这个最长长度,检查后缀数组上是否存在一个长度为 (K) 的区间满足 (H) 的最小值大于等于这个长度即可。

    代码

    #include<iostream>
    #include<cmath>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<algorithm>
    #define FOR(i,x,y) for(int i=(x),i##END=(y);i<=i##END;++i)
    #define DOR(i,x,y) for(int i=(x),i##END=(y);i>=i##END;--i)
    template<typename T,typename _T>inline bool chk_min(T &x,const _T y){return y<x?x=y,1:0;}
    template<typename T,typename _T>inline bool chk_max(T &x,const _T y){return x<y?x=y,1:0;}
    typedef long long ll;
    const int N=2e4+5;
    int sa[N],rk[N],H[N],tmp[3][N];
    int disc[N],D;
    int s[N];
    int n,K;
    
    void get_SA(int *s,int n,int m)
    {
    	int *x=tmp[0],*y=tmp[1],*c=tmp[2];
    	x[n+1]=y[n+1]=0;
    	FOR(i,1,m)c[i]=0;
    	FOR(i,1,n)c[x[i]=s[i]]++;
    	FOR(i,2,m)c[i]+=c[i-1];
    	DOR(i,n,1)sa[c[x[i]]--]=i;
    	for(int k=1;k<=n;k<<=1)
    	{
    		int p=0;
    		FOR(i,n-k+1,n)y[++p]=i;
    		FOR(i,1,n)if(sa[i]>k)y[++p]=sa[i]-k;
    		FOR(i,1,m)c[i]=0;
    		FOR(i,1,n)c[x[y[i]]]++;
    		FOR(i,2,m)c[i]+=c[i-1];
    		DOR(i,n,1)sa[c[x[y[i]]]--]=y[i];
    		std::swap(x,y);
    		p=x[sa[1]]=1;
    		FOR(i,2,n)x[sa[i]]=y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]?p:++p;
    		if(p==n)break;
    		m=p;
    	}
    	FOR(i,1,n)rk[sa[i]]=i;
    	int k=0;
    	FOR(i,1,n)
    	{
    		if(k)k--;
    		if(rk[i]==1)continue;
    		int j=sa[rk[i]-1];
    		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])k++;
    		H[rk[i]]=k;
    	}
    }
    
    bool check(int len)
    {
    	int cnt=0;
    	FOR(i,1,n)
    	{
    		cnt++;
    		if(cnt>=K)return true;
    		if(H[i+1]<len)cnt=0;
    	}
    	return false;
    }
    
    int main()
    {
    	scanf("%d%d",&n,&K);
    	FOR(i,1,n)scanf("%d",&s[i]);
    	FOR(i,1,n)disc[++D]=s[i];
    	std::sort(disc+1,disc+D+1);
    	D=std::unique(disc+1,disc+D+1)-disc-1;
    	FOR(i,1,n)s[i]=std::lower_bound(disc+1,disc+D+1,s[i])-disc;
    	get_SA(s,n,n);
    	int l=0,r=n;
    	while(l<r)
    	{
    		int mid=(l+r+1)>>1;
    		if(check(mid))
    			l=mid;
    		else r=mid-1;
    	}
    	printf("%d
    ",l);
    	return 0;
    }
    
  • 相关阅读:
    原码、反码、补码,计算机中负数的表示
    [转]Vue 2.0——渐进式前端解决方案
    关于MySQL的tinyint(3)问题
    js对象的深拷贝及其的几种方法
    深入 js 深拷贝对象
    JS 数组克隆方法总结
    Undefined class constant 'MYSQL_ATTR_INIT_COMMAND'
    邮件措辞小计
    Forbidden You don't have permission to access / on this server PHP
    正则表达式
  • 原文地址:https://www.cnblogs.com/Paulliant/p/10573103.html
Copyright © 2011-2022 走看看