参考资料:victorique的博客(有一点锅无伤大雅,记得看评论区),$wzz$ 课件(快去$ftp$%%%),$oi-wiki$以及某个人的帮助(万分感谢!)
首先还是要说一句:我不知道为什么我这么菜让我讲这么大神的知识点,我理解不深刻,你们可以随时$Ha(n)ck$然后我可能就fix不了了你知道吧
(好吧我大概又理解了因为数学是NC讲的数据结构是skyh讲的我能讲的可能也就是这种东西了)
希望还会的大神帮我解场。。。(某$bx$和某牛都$A$穿了)
顺便「无图预警」(懒得画又懒得粘)
当时$wzz$讲课的时候大概也都多多少少听了点,也有一半左右的人做过题。(虽说忘的差不多了)
至少定义都还记得是啥吧?不记得就再来一遍:
$suffix(x)$表示从$x$开始的字符串后缀
$rk[x]$就是$suffix(x)$在字符串所有后缀中的字典序排名
所谓的后缀数组$sa[i]$其实就是$Suffix \ Array$,表示排名为$i$的后缀它的起始位置是哪里。
其实$sa$就是$rk$的逆数组,即$rk[sa[i]]=i$,求出其一就可以得到另一个。
别的忘了可以,数组定义一定要记住,不然绝对下面全程跟不上。。。(废话,讲的就是这个)
想要求出这个数组,最暴力的做法就是$sort(string)$,字符串比较的复杂度是$O(n)$的所以总复杂度是$O(n^2log n)$
这肯定接受不了。然后就需要$wzz$给我们讲的那个倍增算法将复杂度优化到$O(nlogn)$。
存在线性做法,但是极大多数题目肯定不会单纯问$sa$其余线性求解而故意卡倍增,往往是要结合点东西的,所以$O(nlog n)$也就够用了。
考虑一下,暴力求解的主要复杂度瓶颈其实是在于字符串的字典序比较,然而既然它们都是同一个串的后缀,一定有什么可以利用的地方。
我们肯定要上来比较一下每个后继的第一个字符,然后有一个大致的排名,有很多串依旧是并列的。
然后对于这些并列的玩意我们要比较第二个字符了,而长度为$1$的所有比较结果我们其实已经有了所以并不需要暴扫一遍去求解。
这么说着有点蠢,考虑更大的数据。
对于一个长串,我们已经比较了它的所有后缀的前$65536$个字符得到了一个排名,接下来要比较每个后缀的$131072$个字符进一步细化排名。
假如有两个后缀$suffix(i)$与$suffix(j)$,它们的前$65536$个字符都相同,我们尝试通过第$65537$到第$131072$个字符来把它们区分开来。
那么其实我们真正要比较的是$suffix(i+65536)$和$suffix(j+65536)$这两个后缀的前$65536$个字符.
因为你已经知道$suffix(i)$和$suffix(j)$的前$65536$个字符相同了,差别只能在后边。
然而其实在上一次倍增的时候你已经有$suffix(i+65536)$和$suffix(j+65536)$的前$65536$个字符的排名了.
你现在可以直接利用上一轮的结果来$O(1)$进行比较而不需要$O(65536)$暴扫。
倍增就是这样利用了上一轮的结果来优化这一轮的比较。
如果真的能做到$O(1)$比较,那么这样倍增至多$log$层,然后普通排序是$O(nlogn)$的,所以总复杂度就是$O(nlog^2n)$
但是这个东西其实值域很小不超过$n$,我们用类似于桶排序的方法看看能不能把排序优化到$O(n)$,这就是基数排序的事了
其实主要不好懂的就在这个基数排序。。。以前好像没见过。
但是桶排序你还是会的吧?不会桶排序也没关系,你好歹知道桶是啥吧?(不知道桶是啥也没关系,但是你为什么没联赛退役啊)
因为这里的基数排序只有$2$维(就是上面所说的$suffix(i)$和$suffix(i+65536)$的前$65536$个字符),所以其实可以当成桶排。
(基数排序就是多次桶排,从高位到低位多次逐渐锁定排名,看完代码实现就明白它在干啥了)
但是这东西还是有点抽象不好口胡,于是向$wzz$学习,直接丢代码走人。
1 void Suffix_Array(char*a,int n,int m=27){ 2 //变量含义:m是字符集大小,n是字符串长度,c是一个桶数组,a[i]是字符串(下标从1开始) 3 //rk[i]就是suffix(i)的字典序排名,sa[i]就是要求的排名为i的后缀的起始位置,即rk[sa[i]]=i 4 for(int i=1;i<=m;++i) c[i]=0; 5 for(int i=1;i<=n;++i) x[i]=a[i]-'a'+2; 6 //x[i]用于存储suffix(i)的第一关键字(的排名),在刚开始长度为1的时候就是a[i] 7 //也就是,第i个后缀的第一关键字在所有后缀里的排名,记住定义!!! 8 //如果出于某些原因想加入一个空字符(字典序最小),那么在全局把它设置为'a'-1就好了,这也是为什么m=27而非26 9 for(int i=1;i<=n;++i) c[x[i]]++; 10 //看起来十分草率的一个扔进桶里的过程 11 for(int i=1;i<=m;++i) c[i]+=c[i-1]; 12 //把桶做一遍前缀和。这样操作后就有如果是s[i]=x,则c[x-1]13 //也就是对于一个c[x-1] 14 for(int i=1;i<=n;++i) sa[c[x[i]]--]=i; 15 //这句话就是对第12~13行的那句话的一种实现方式 16 for(int len=1;len<=n;len<<=1){ 17 //len表示的是第一关键字与第二关键字分别的长度,所以len<<1就是本轮真正所要比较的长度 18 //定义y数组,表示第二关键字排名为i的后缀是suffix(y[i]) 19 int num=0; 20 //num就是用来存储已经排好序的第二关键字的数量 21 for(int i=n-len+1;i<=n;++i) y[++num]=i; 22 //suffix(i)的第二关键字现在是a[i+len...i+2len-1]。而对于suffix(n-len+1)...suffix(n)已经没有第二关键字了。 23 //而空字符的字典序最小,所以它们第二关键字的排名最靠前 24 for(int i=1;i<=n;++i) if(sa[i]>len) y[++num]=sa[i]-len; 25 //对于所有p>len,a[p...p+len-1]这一截都会成为suffix(p-len)的第二关键字 26 //而你枚举的是sa[i],i从小到大也就是代表你从小到大枚举的所有可能出现的第二关键字 27 //现在y数组里面已经按从小到大的顺序存储好了所有可能出现的第二关键字,包括空的 28 //注意y数组的含义,和x是不一样的,x是位置i的第一关键字的排名,y是排名为i的第二关键字的位置 29 for(int i=1;i<=m;++i) c[i]=0; 30 for(int i=1;i<=n;++i) c[x[i]]++; 31 for(int i=1;i<=m;++i) c[i]+=c[i-1]; 32 //都与倍增外面的部分同理。还是按照第一关键字扔桶。下面要基数排序加入第二关键字了。 33 for(int i=n;i;--i) sa[c[x[y[i]]]--]=y[i]; 34 //这句话是最麻烦的一句,x[y[i]]表示第二关键字排名为i的后缀的第一关键字 35 //注意这里的循环需要倒序,因为你是从大到小枚举第二关键字,而由于你的c[]--,所以得到的排名也是从大到小 36 //这里和第12~13行的注释是同理的,注意每个按关键字排序后的后缀所在的排名区间 37 //所以也可以改写成for(int i=1;i<=n;++i) sa[++c[x[y[i]]-1]]=y[i];这样全文就都是正序枚举了不易混,但是需要清空c[0] 38 //我倾向于这个写法,实测可以AC。但是这里还是抄的wzz的原版板子,你们自行选择 39 for(int i=1;i<=n;++i) y[i]=x[i],x[i]=0; 40 //数组的回收利用,只不过是换个数组存了一下第一关键字,清空一个数组备用 41 x[sa[1]]=m=1; 42 //排名最小的串,它在2len长度比较下也最小,所以在下次len<<=1后,它对应的len第一关键字就是最小的第一关键字 43 //这里把m置为1.也就是现在要重新规定字符集大小了,这个1是是x[sa[1]],m以后表示的就是目前已知的本质不同后缀数 44 for(int i=2;i<=n;++i) x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&y[sa[i]+len]==y[sa[i-1]+len])?m:++m; 45 //这时候sa在2len长度的比较下已经排好序了,可以直接按照排名枚举。只要你和你前一名的第一/二关键字不完全相同 46 //那么你们就是不同的,所以你就是一个新串,加入字符集m++,然后重新给它在下一轮的第一关键字编号为m 47 if(m==n)break; 48 //如果你的字符集个数等于后缀个数,那么就是所有的后缀都被两两区分开了,可以提前跳出了 49 } 50 for(int i=1;i<=n;++i)rk[sa[i]]=i; 51 //rk是suffix(i)的排名,从含义上就知道它是sa的逆数组 52 }
顺带附luogu模板题链接,讲完之后可以自行试验。
然后假装我们已经会求$sa$和$rk$数组了好吧?
板子反正就放在博客里,几十行的注释等会也可以慢慢去看。先假装你会求出它们了,然后才好进行下一步。
理论上现在该讲$height$数组了,但是我个人认为直接换知识点最后听下去就啥也不会了。
于是先上几个不用$height$直接$sa/rk$爆干的例题,让你们切一切。(反正我是没切掉)
不用考虑代码实现,毕竟口胡一遍基本不可能直接记住板子。
对于接下来的例题,你可以认为$rk$和$sa$是题目的读入。只要想明白怎么用就好不用考虑代码。
可能有人没跟上上面讲代码的部分,但是这个可以补。不要连例题也丢了。
Problem 1:[JSOI2007]字符加密
$Description$:
喜欢钻研问题的JS 同学,最近又迷上了对加密方法的思考。一天,他突然想出了一种他认为是终极的加密办法:
把需要加密的信息排成一圈,显然,它们有很多种不同的读法。
例如‘JSOI07’,可以读作: JSOI07 SOI07J OI07JS I07JSO 07JSOI 7JSOI0。把它们按照字符串的大小排序:
07JSOI
7JSOI0
I07JSO
JSOI07
OI07JS
SOI07J
读出最后一列字符:I0O7SJ,就是加密后的字符串(其实这个加密手段实在很容易破解,鉴于这是突然想出来的,那就^^)。
但是,如果想加密的字符串实在太长,你能写一个程序完成这个任务吗?
$n \le 10^5$
难度不是很高,考虑怎么利用你求出来的那两个数组。
伟大的哲人Paris曾经说过:断环成链是常识。 然后就断开,把字符串拷贝一份直接接在后面,然后跑sa。 得到rk了,然后把所有以区间[1,n]以内为起始的长度为n的子串的最后一个字母按照rk的大小关系顺序输出。 AC。 我猜mikufun会说这道题是傻逼题。
Problem2:谁是垃圾话之王
$Description$:
为了弄这个知识点而编的题,难不成还要让我写一个题目背景?
那就写吧。
$kx$考了$HEOI-rank2$之后非常$kx$,然后继续保持了说话极有素质的习惯,开始大声$BB$,说出了一句长度为$n$的字符串。
然后正在扩脸的$skyh$就非常烦,觉得他说的是垃圾话.
为了有充足的理由来证明$kx$没素质,他想知道某些敏感词汇(例如$face$)在$kx$的话里出现了多少次。
然而他在正式开始批判$kx$之前并不想让$kx$知道他在进行这个统计,否则他会被$kx$暴打。
所以他加密了他的询问,只有在你正确回答出他的上一个询问之后,他才会继续问下去(也就是强制在线),否则他会说你是垃圾而给你$0$分。
因为$kx$说的垃圾话不是很多,也就几百万个字,所以保证$n \le 10^6$
因为$skyh$脑子里的词汇太少(不像我的$blog$标题),所以保证所有询问的字符串长度之和$\le 10^6$
如果你不会做这道题,那么请把$skyh$暴打一顿。所以为了$skyh$的身心健康,请大家切掉这道题。
一句话题意:在线查询一个特定串中某子串的出现次数。
最优做法肯定不是$SA$(被$SAM$暴切了),但是$SA$可做,大家尽量往这个方向上想。
其实这个东西用SA做真的挺蠢的,但是只是为了体现知识点。
子串都是某个后缀的前缀。
你已经得到了所有后缀的排名,在所有后缀上二分,找到模式串是后缀的前缀的rk区间。
检查大小关系的方法就是暴力匹配。
总复杂度是 n log n + skyh log n的
Problem3:「USACO07DEC」Best Cow Line
$Description$:
$USACO$的英文题,不给你们翻译背景了。
给定一个长度为n的字符串,你每次可以从首或尾取出一个字符放到新的字符串末尾,要求新字符串的字典序尽量小。
$n \le 10^6$。不要想简单,怎么处理两端字符都相同的情况?例如XXXXXCBXXXXX
这个稍微难一些。但就是去优化那个暴力的方法。想一会?
其实是个大套路,$cbxxx$早就会了。
其实就是在字符相同的时候比较左端点的后缀与右端点的前缀哪个小就选哪个。 暴力比较是O(n)的。 因为串是确定的,所以你可以预处理出它们的排名,就可以O(1)比较了。 我上来的思路是把字符串正反都SA一下就好了。 但是怎么把前缀的排名和后缀的排名放在一起。 于是正解就是把正反字符串接起来,中间加上一个空字符,然后跑后缀排名就可以得到相对rank了。
三道例题应该够了。所以你已经熟练掌握SA了。那就没我的事了再见
接下来就是我咕掉的$height$。
定义$lcp(string1,string2)$的含义为两个字符串的最长公共前缀。(下面有些地方省略了$suffix(x)$而直接写$x$,不然公式太长)
(如果你要从英文含义来理解,$lcp$就是$longest \ common \ prefix$)
注意定义的$lcp(a,b)$是$lcp(suffix(sa[a]),suffix(sa[b]))$而不是$lcp(suffix(a),suffix(b))$
那么定义$height(i)=lcp(suffix(sa[i-1]),suffix(sa[i]))$。就是排名相邻的两个串到底有多少位相同。
再进一步的说,其实$height[i]$就表示排名为$i$的串和排名为$i-1$的串的最长公共前缀。
怎么求啊?然后就需要大量的证明了。。。
其实$SA$的过程中,你已经把所有的后缀都排序好了,所以你可以想象一下。。。一本英文词典。。。(字典序。。。)
前几个字符相同的当然都在词典上聚在一起,和某个串前缀匹配最多的串应该就在这个串的前后。。。
多想想词典,也许有利于你对下面这些东西的理解。
首先比较显然的是(感性理解一下,大概就是所谓的短板理论):
$LCP Lemma:lcp(i,k)=min(lcp(i,j),lcp(j,k))$。对于任意$i \le j \le k$
(Lemma是引理的意思)
非要解释一下含义的话,那就是对于排名递增的三个后缀,两端的后缀的lcp就是两端后缀分别与中间的那个后缀的lcp的较小的那个。
还是简要证明一下吧:设$min(lcp(i,j),lcp(j,k))=p,A=suffix(i),B=suffix(j),C=suffix(k)$
则若$lcp(i,j) \geq p$则$lcp(j,k)=p$,则分别有$A[p+1]=B[p+1],B[p+1] \neq C[p+1]$,得到$A[p+1] \neq C[p+1]$,所以$lcp(i,k)=p$
反过来同理。
还有一种情况是$lcp(i,j)=lcp(j,k)=p$,这样的话得到$A[p+1] \neq B[p+1],B[p+1] \neq C[p+1]$,这样的话如果$A[p+1] \neq C[p+1]$就和上面一样。
否则,$A[p+1]=C[p+1] \neq B[p+1]$,这样$B$的$rk$不会夹在$A,C$之间,与题设矛盾,不存在这种情况。
得证。
进而得到:
$LCP \ Theorem:lcp(i,k)=min(lcp(j,j-1))$对于任意$1 \le i < j \le k \le n$
(Theorem是定理的意思)
比较好说的是$height[1]=0$。然后就有$height[i]=lcp(suffix(sa[i]),suffix(sa[i-1]))$。
接下来就是最烦人而重要的结论:
$height[rank[i]] ≥ height[rank[i − 1]] − 1$
设字典序排名比$suffix(i-1)$小$1$的那个字符串是$suffix(x)$,$lcp(suffix(x),suffix(i-1))=height[rk[i-1]]$
如果$suffix(x)$与$suffix(i-1)$首字母都不同,那么$height[rk[i-1]]=0$,$height[rk[i]] \geq 0$,所以一定满足上面的结论。
如果相同,那就考虑,$suffix(x)$去掉首字母会得到$suffix(x+1)$,$suffix(i-1)$去掉首字母得到$suffix(i)$。
去掉首字母大小关系不变,所以因为$rk[x] < rk[i-1]$,就有$rk[x+1] < rk[i]$。
这样的话,$x+1$和$i$的$lcp$也只是相较于$x$与$i-1$的$lcp$去掉了首字母。
即$lcp(x+1,i)=lcp(x,i-1)-1=height[rk[i-1]]-1$。
我们已经知道对于任何$rank[j] < rank[i]$,$height[rk[i]]=lcp(i,sa[rk[i]-1]) \geq lcp(i,j)$。
所以大小关系传递一下,就得到$height[rk[i]] \geq height[rk[i-1]]-1$。
所以我们每次都$-1$再让它尽量去匹配,就可以线性推出$height$数组了。
(注意下面这个板子是$wzz$的,数组下标从0开始的,和上面我的板子不是很适配,稍改一下就能用了)
1 for(int i=0,k=0;i1:0) 2 while(a[i+k]==a[sa[rk[i]-1]+k])++k;
然后好像是讲完了的样子。懒得上例题了我在咱OJ上做的题那么少啥也不会啊。。。要不让NC来讲?
然后还是为了防止你们说我给你们颓题解(因为我自己也没做我也还不想颓题解),我自己去找几道题吧。。。
真的是水题,都是可能用上的套路。随便切不要多想。
Problem4:谁是垃圾话之王2
$Description$:
为什么没有这个知识点的例题啊!写题目背景真累。
书接上回,$skyh$总算凑齐了证据,于是去找$kx$理论。
但是$kx$表示不想跟弱智($skyh$自己写在博客副标题的)讲道理,然后捶了$skyh$一下。
$skyh$觉得很委屈,他就$kuku$了。
然后$kx$抓住机会,说:“你才说垃圾话呢,你看你说的$kuku$里$ku$出现了两次,你重复罗嗦才是垃圾话”
$skyh$说:“不,我说的话重复的不多啊,你个$gp$是$gp$啊”
然后他俩掐起来了,你在看戏的同时,裁决一下$skyh$说的垃圾话多不多。
给定$skyh$说的一长段话,进行q次裁决,每次摘出两段只言片语(文字狱?其实就是子串),问它们从头开始匹配了多少个字。
$ n ,q \le 10^5$
一句话题意:给定一个字符串,每次询问求两个子串的$lcp$。
是$wzz$讲过的。我稍微扩句两下就行了。
根据LCP Lemma和LCP theorem可以得到推论 lcp(sa[i],sa[j])=min(lcp(sa[i],sa[i+1]),lcp(sa[i+1],sa[i+2]),...)=min{height[i+1..j]}。 然后就只剩下$RMQ$了。随便弄个数据结构就好了。
Problem5:谁是垃圾话之王3
$Descroption$:
啊。。。还是没有例题。口胡挺累的。况且我文采不足,你们将就看看吧。
话说这两个人掐起来之后没完没了,到了后来画风越来越诡异。
“我说的话字典序都比你大,怎么能是垃圾话呢?”
“你脸大就脸大,你好好看看谁的字典序大!”
“嘿,你看什么呢?还看戏呢?你如果不把我俩说的话比出字典序大小,$nmsl$。”
然后你就背锅了。给定他俩说的一大段话,每次摘出两段,比较字典序大小。
$n,q \le 10^5$
一句话题意:比较两子串字典序大小。
这个$wzz$都懒得讲了。。。我估计你们想到正解比看完题目背景用的时间还短。
你都会lcp了这还有什么好说的?
设要求解的子串为A[a..b],B[c..d]
那么你先算出lcp(a,c),如果发现它们匹配的长度大于A或B的长度,那么就表示一个是另一个的子串,直接根据长度判断。
否则就证明并没有子串关系,直接根据rk数组就可以比较了。
Problem6:谁是垃圾话之王4
$Description$:
抱歉哈还是没有找到这么裸的题。
这俩人吵得正欢,这时候$LNC$来了。看见$kx$想都不用想直接来一句:
“$HE-rank2$,巨~~~~~”
然后$skyh$开心了,问$LNC$:“你说这个人,他说我说垃圾话,他说我罗嗦重复$asas$”
$LNC$瞥了他一眼说:“这$as$就是重复。你除了那几句垃圾话还说过啥?”
然后$skyh$就自闭了,跑到一边去数自己说过哪几种话了。
为了体恤一下$skyh$,对于他说的一大段话,任意长度选其中一段,只要有一个字不同就算两种。
问题就是,他一共说过几种话。他这么可怜你一定会告诉他的对吧。
在你知道$n \le 10^6$之前,你一定会的。
一句话题意:本质不同的子串数。
这个也不难,稍微想一会。好像$wzz$也讲过。。?
算不同的麻烦,但是算完全相同的稍微简单一些。 先把所有的n(n-1)/2个子串都算上,再减去重复的。 重复的有多少?考虑到子串是前缀的后缀。 既然后缀可以用SA排好序,那么就可以发现: 排名相邻的两个串,只有高于height的部分才会形成新的子串。 所以答案就是n(n-1)/2-sum(height)
Problem7:「USACO06DEC」Milk Patterns
$Description$:
总算有题面了,但还是$USACO$的。这是$wzz$课件上的原题,直接粘翻译了。
John 的牛奶按质量可以被赋予一个 $0$ −$ 10^6$ 之间的数。并且$John$记录了 $N$($1 ≤ N ≤ 20000$) 天的牛奶质量值。
他想知道最长的出现了至少 K(2 ≤ K ≤ N) 次的模式的长度。
比如 $1 \ 2 \ 3 \ 2 \ 3 \ 2 \ 3 \ 1 $中$ 2 \ 3 \ 2 \ 3 $出现了两次。当 $K=2$ 时,这个长度为 $4$。
一句话题意:求出现次数超过$K$的最长子串。
这个东西在$wzz$课件里就有啊。
一个比较显然的结论是:当连续选择的串数一定时,排名连续的子串,它们的lcp最大
如果你选的不是连续的一段,那么根据problem4的结论,你取它们的lcp还是会对中间的所有height取min
所以,二分答案,问题转化为判断height数组上是否有长度为K的连续区间,它们height的min大于等于mid。
Problem8:谁是垃圾话之王5
$Description$:
这次是懒得找题了。
$skyh$很不服气,跟那两个人说:
“我感觉你们对重复的定义不对,假如我说一句:‘我的脸是不是不是很大’这句话,你们就认为我说了两次‘是不是’,这样有重叠部分的不算啊,所以我说话不重复”
然后$LNC$就稍微动用了一下教$GK$做人的本事:“第一点,回答你的问题,你脸就是大。第二点,就算不算重叠部分,你也重复”
然后$kx$说:“这我瞎写个代码就能判了,不就是检查是否存在无重叠部分的相同子串么?$O(nlogn)$瞎写就完了。顺便算一下最长的长度也不是事。$DeepinC$出的真是大水题”
我:“???串帮了???我还是不出题了”
一句话题意:求完全不相交的相同子串的最长长度。
其实还不是特别难,但是$wzz$好像没有讲的样子。
预处理一下对于每个height它对应的sa。 然后二分答案。 对于mid,我们提取出height数组里所有大于等于mid的位置。 这会构成若干个不连续的区间,对于每个区间,我们求出它们sa值的最大最小值,RMQ解决。 如果最大最小值的差值大于mid,就证明它们已经没有交集了,就是一个合法串,return true。
到这里为止,差不多能遇到的裸题型已经说得差不多了。
再之就是要与线段树,并查集,单调队列之类的数据结构结合了。(上面你已经看到它多次和二分答案结合了)
然而这部分的难度比较高(我不会),而且好多都是咱们OJ里的题目了(我也没做),我就不爆题解了。
让我讲数据结构是不可能哒!
(其实题解都在$wzz$的课件里面,想颓可以去拿啊,但是大部分套路我在上面都说完了,如果不自己想一些的话就没什么意义了吧)
附:谁是垃圾话之王大结局
过了两天,这几个去$WC$的人回来了。
他们发现了这篇博客,一人拿着一个一本约跟$DeepinC$说:
“你个垃圾$WC$都去不了还在这里$yy$我们,出了一大堆大水题凑字数,你才是垃圾话之王”
全剧终。