hdu6704 后缀数组+主席树+ST +二分
Posted 6262369sss
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了hdu6704 后缀数组+主席树+ST +二分相关的知识,希望对你有一定的参考价值。
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6704
推荐博客:https://www.cnblogs.com/1625--H/p/11403199.html
题意:输入t,接下来t组数据,每组数据输入n和q,n代表字符串长度,q代表查询次数,接下来一行输入长度为n的字符串,最后再输入q行查询,每一个查询有l,r,k,查询字符串中在区间[l,r]里的这个子串第k次出现的位置,位置就是出现这个子串的首字母位置,如果没有第k次,则输出-1。
比赛时我也没写出来,补题。
思路:我讲的可能不是很清楚,可以看上面的推荐博客。
区间[l,r]的子串在位置为l的后缀中是一定出现了的,并且是这个后缀的一个前缀,而要找到其他出现这个子串的位置,我们可以用后缀数组,先用后缀数组先把原串所有的后缀进行排序,即求出sa数组,其中出现这个子串的后缀肯定都是连续的,因为它都是这些连续后缀的前缀,这里我们可以先求出后缀数组中的height[i]数组,i是排位,height[i]代表排位为i的后缀和排位为i-1的后缀之间的最长公共前缀(LCP),我们以位置为l的后缀的排位x[l]为中心,向左右两边分别找最后一个和位置为l的后缀之间LCP>=r-l+1的后缀,得到两个后缀的排位(假设分别是L和R),那么在排位区间[L,R]中出现的所有后缀的前缀都包含着我们要查询的子串,这个过程可以用二分来实现。我们要找其中出现的第k个,实际上就只需要寻找这个排位区间中的位置第k大,这个我们可以用静态主席树做到。
代码:
#include<iostream> #include<cstring> #include<algorithm> #include<queue> #include<map> #include<stack> #include<cmath> #include<vector> #include<set> #include<cstdio> #include<string> #include<deque> using namespace std; typedef long long LL; #define eps 1e-8 #define INF 0x3f3f3f3f #define maxn 100005 int sa[maxn],height[maxn],x[maxn],y[maxn],c[maxn]; //这里x数组对应于rank数组,但是我个人习惯写x了 int n,m,k,t,q,cnt; char s[maxn]; int dp[maxn][20],root[maxn]; struct node//静态主席树也是用结构体写的 int l,r,sum; tree[maxn<<6]; void getHeight()//height数字下标表示排位,值表示排位为i的后缀和排位为i-1的后缀的LCP //H数组下标表示位置,H[i]=height[x[i]],有H[i]>=H[i-1]-1 int k=0; for(int i=1;i<=n;i++) x[sa[i]]=i; for(int i=1;i<=n;i++) if(k) k--; int j=sa[x[i]-1];//找到位置在i的关键字的前一个排位的关键字 while(s[i+k]==s[j+k]&&(i+k)<=n&&(j+k)<=n) k++; height[x[i]]=k; void build_sa()//好久没写这个了,所以写的时候加了很多注释,就当梳理思路,复习了 m=256; for(int i=1;i<=m;i++) c[i]=0; for(int i=1;i<=n;i++) c[x[i]=s[i]]++;//x数组下标表示位置,值表示排位 for(int i=1;i<=m;i++) c[i]+=c[i-1]; for(int i=n;i>=1;i--) sa[c[x[i]]--]=i;//sa数组下标表示排位,值表示第一关键字的位置 for(int k=1;k<n;k<<=1) int num=0; for(int i=n-k+1;i<=n;i++) y[++num]=i;//y数组下标表示第二关键字排位,值表示对应的第一关键字的位置 // 有部分第一关键字没有第二关键字,这里把它们的第二关键字补‘0‘,按照出现顺序排序 for(int i=1;i<=n;i++)//按照关键字排位从小到大来遍历第一关键字 if(sa[i]>k)//排位为i的第一关键字位置如果大于k,则说明这个关键字可以作为另一个关键字的第二关键字 y[++num]=sa[i]-k;// 这个关键字作为第二关键字的话,它所对应的第一关键字的位置就是sa[i]-k //上面已经求出了排位为i的第一关键字和第二关键字的对应的位置,下面开始对第一关键字和对应的第二关键字进行合并 //并求出合并之后的关键字对应的sa数组,下标表示排位,值表示位置 for(int i=1;i<=m;i++) c[i]=0; for(int i=1;i<=n;i++) c[x[i]]++; for(int i=1;i<=m;i++) c[i]+=c[i-1]; //我们这里i从大到小,在第一关键字不同时,这里的y[i]没有什么影响,但是在第一关键字相同时,第二关键字 //更大的关键字会先出现,从而获得更大的位置,这就保证了先比较第一关键字,相同时比较第二关键字 for(int i=n;i>=1;i--) sa[c[x[y[i]]]--]=y[i]; swap(x,y);//接下来我们要求出合并之后关键字的x数组,下标表示位置,值表示排位,所以我们把x数组的值复制到y数组 num=1; x[sa[1]]=1; for(int i=2;i<=n;i++)//按照排位从小到大的顺序遍历y数组 x[sa[i]]=y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]?num:++num; m=num; if(m>=n) break; void ST() for(int i=1;i<=n;i++) dp[i][0]=height[i];//i代表的是排位 for(int j=1;(1<<j)<=n;j++) for(int i=1;i+(1<<j)-1<=n;i++) int a=dp[i][j-1]; int b=dp[i+(1<<(j-1))][j-1]; dp[i][j]=min(a,b); void init() memset(root,0,sizeof(root)); memset(height,0,sizeof(height)); memset(dp,0,sizeof(dp)); cnt=0; void build_0(int &root,int l,int r) root=++cnt; tree[root].sum=0;//在这个位置卡了一天,把他写在下面了,并且这个函数中没有向上更新 if(l==r) return; int mid=(l+r)/2; build_0(tree[root].l,l,mid); build_0(tree[root].r,mid+1,r); void update(int root) tree[root].sum=tree[tree[root].l].sum+tree[tree[root].r].sum; void build(int pre,int &root,int l,int r,int index) root=++cnt; tree[root]=tree[pre]; if(l==r) tree[root].sum=1; return; int mid=(l+r)/2; if(index<=mid) build(tree[pre].l,tree[root].l,l,mid,index); else build(tree[pre].r,tree[root].r,mid+1,r,index); update(root); int RMQ(int l,int r)//这里写的有点麻烦,这里的l和r已经是后缀的排位了 if(l>r) swap(l,r); if(l==r) return n-sa[l]+1;//l==r一定满足条件,因为就是位置为l的后缀,这里返回INF也是可以的 l++;//这里需要l++,因为考虑到height[i]的定义 //当我们要查询排位在区间[l,r]之间的后缀,它们两两相邻的LCP最小值,l需要++, //因为height[l]是排位为l的后缀和排位为l-1的后缀的LCP //这里我们是不需要height[l]的,只需要height[l+1]到height[r] int k=0; while((1<<(k+1))<=r-l+1) k++; int a=dp[l][k]; int b=dp[r-(1<<k)+1][k]; return min(a,b); int find_L(int l,int r,int len) int R=r; int pre=r;//其实还是不怎么会二分的,写成这个方式比较保险 while(l<=r) int mid=(l+r)/2; if(RMQ(mid,R)>=len) pre=mid; r=mid-1; else l=mid+1; return pre; int find_R(int l,int r,int len) int L=l; int pre=l; while(l<=r) int mid=(l+r)/2; if(RMQ(L,mid)>=len) pre=mid; l=mid+1; else r=mid-1; return pre; int ask(int pre,int root,int l,int r,int k) if(tree[root].sum-tree[pre].sum<k) return -1; if(l==r) return l; int mid=(l+r)/2; if(tree[tree[root].l].sum-tree[tree[pre].l].sum>=k) return ask(tree[pre].l,tree[root].l,l,mid,k); else return ask(tree[pre].r,tree[root].r,mid+1,r,k-(tree[tree[root].l].sum-tree[tree[pre].l].sum)); int main() scanf("%d",&t); while(t--) scanf("%d%d",&n,&q); init(); scanf("%s",s+1); build_sa(); getHeight(); build_0(root[0],1,n);//第0个版本的线段树,我这里直接写build_0了,个人习惯了 for(int i=1;i<=n;i++)//其他版本的线段树 build(root[i-1],root[i],1,n,sa[i]); ST(); int l,r; while(q--) scanf("%d%d%d",&l,&r,&k); int len=r-l+1;//相邻p排位两个后缀的LCP最小要是len int L=find_L(1,x[l],len);//二分找左边界 int R=find_R(x[l],n,len);//二分找右边界 LL ans; if(R-L+1<k) ans=-1; else ans=ask(root[L-1],root[R],1,n,k);//两个历史版本的线段树中查询第k大的位置 printf("%lld\\n",ans); return 0;
以上是关于hdu6704 后缀数组+主席树+ST +二分的主要内容,如果未能解决你的问题,请参考以下文章
HDU - 6704 K-th occurrence (后缀数组+主席树/后缀自动机+线段树合并+倍增)
[LuoguP4094] [HEOI2016] [TJOI2016]字符串(二分答案+后缀数组+ST表+主席树)
BZOJ 4556: [Tjoi2016&Heoi2016]字符串(后缀数组 + 二分答案 + 主席树 + ST表 or 后缀数组 + 暴力)