610 likes | 780 Vues
字符串. 李子星. 字符串相关算法和数据结构. 字符串相关算法 KMP 算法 RK 算法 *有穷状态自动机( DFA ) 字符串相关数据结构 后缀数组 *后缀树. 模式匹配算法. 问题: 给定一个字符串 s[1..n] (又称为母串),和另一个长度不超过 s 的字符串 t[1..m] (又称为模式串)。 问: t 有没有在 s 中出现过。 例如: s 串为 abcdaba , t 串为 cdab ,那么 t 在 s 中出现过。 s 串为 abcdaba , t 串为 cdad ,那么 t 在 s 中没出现过。. 朴素的模式匹配算法.
E N D
字符串 李子星
字符串相关算法和数据结构 • 字符串相关算法 • KMP算法 • RK算法 • *有穷状态自动机(DFA) • 字符串相关数据结构 • 后缀数组 • *后缀树
模式匹配算法 问题: 给定一个字符串s[1..n](又称为母串),和另一个长度不超过s的字符串t[1..m](又称为模式串)。 问:t有没有在s中出现过。 例如: s串为abcdaba,t串为cdab,那么t在s中出现过。 s串为abcdaba,t串为cdad,那么t在s中没出现过。
朴素的模式匹配算法 若t在s中出现过,则一定存在一个i满足: 1<=i<=n且s[i..i+m-1]=t[1..m]。
朴素的模式匹配算法 于是最朴素的算法就是,枚举所有的i,看是不是有一个i满足s[i..i+m-1]=t[1..m]这个条件: bool check(char *s, char *t, int n, int m){ for (int i = 1; i + m – 1 <= n; i++){ bool ok = true; for (int j = 1; j <= m; j++) if (s[i+j-1]!=t[j]){ ok = false; break; } if (ok) return true; } return false; }
朴素的模式匹配算法 下面检查下时间复杂度: 显然是O(n*m),虽然一般不会这么极端,但是效果肯定不好。 因为做了很多无用功。
a b c a b c a b d a c a a a a a a b b b b b b c c c c c c a a a a a a b b b b b b d d d d d d a a a a a a a a b b a b c a b d a c a b a b c a b d a a b c a b c a b d a b c a b a b c a 朴素算法的缺点 x x x 第4次匹配: d 第1次匹配: 第2次匹配: 第3次匹配:
t[next[i]] t[i] KMP算法 • KMP算法的关键就是找到所有这样的对应关系 • 通常用一个next数组来表示这样的对应关系 • next[i]=x表示:t[1..i-1]与t[1..x-1]右对齐后完全匹配,并且找不到另一个y大于x且满足同样的条件 • next[1]=0,这是边界情况
a b a b a a b a b c b a a a a b b b b a a a a b b b b c c c c 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 3 3 3 3 a c b b b a b c 3 2 1 KMP算法 有了这样的next数组后,匹配过程就可以大提速了: s串: t串: next数组:
KMP算法 参考代码: bool check(char *s, char *t, int n, int m, int *next){ int i = 1, j = 1; while (i <= n && j <= m) if (j == 0 || s[i] == t[j]) { i++; j++; } else j = next[j]; return (j > m); }
KMP算法 下面要讨论这个过程的时间复杂度: • “i++; j++;” 会执行多少次? • 由于i只会增加不会减少,而i最大就是n,所以这两句话最多执行n次 • “j=next[j];” 会执行多少次? • 由于next[j]<j总是成立,所以这句话一定会让i-j的差变大,而“i++;j++;”不会让i-j产生变化;i-j一开始是0,最大也就是n-0=n,所以这句话也最多执行n次 • 结论:这个过程的时间复杂度是O(n)
KMP算法——next数组 剩下的问题:怎么求next数组? 可以利用类似的思想:利用next数组中已知的部分求未知的部分。
0 a b a b c a b d a a a a b b b b a a a a b b b b c c c c a a a a b b b b d d d d 0 0 0 0 1 1 1 1 1 2 2 3 3 KMP算法——next数组 1 1 2 3 1 2 3 a a b a a a b 1 1 2 3 1 2 3
KMP算法——next数组 参考代码: make_next(char *t, int m, int *next){ next[1] = 0; int i = 2, j = 1; while (i <= m){ next[i] = j; while (j > 0 && t[i] != t[j]) j = next[j]; i++; j++; } }
KMP算法——next数组 下面讨论时间复杂度: • “next[i]=j;”和“i++; j++;” 会执行多少次? • 显然是m次 • “j=next[j];” 会执行多少次? • 一样可以用i-j的差来分析,结果也是m次 • 结论:这个过程的时间复杂度是O(m)
KMP算法——总结 • 先求next数组 • 然后利用next数组来寻找匹配的位置 • 总时间复杂度为O(m+n)
RK算法 假设构成字符串的字符的是0~9这十个数字,那么模式串实际上可以看做是一个十进制数(虽然可能很大)。 朴素的模式匹配中对s[i..i+m-1]与t[1..m]的比较就可以看做是两个数字的比较。
RK算法 若定义ints[i]=s[i..i+m-1],那么ints[i]与ints[i+1]之间就有明显的递推关系: ints[i+1]=10*(ints[i]-s[i]*10^(m-1))+s[i+m] 如果m比较小以至于ints[i]总是方便存储的,那么这个过程的效率肯定也是非常高的。可惜只要m稍大,ints[i]与intt=t[1..m]就没法方便的存储了。
RK算法 这个时候只要加上一点改进: 我们在存储ints[i]与intt时,不储存他们的准确值,而是储存他们模上一个较大的质数p的结果。 这样ints[i+1]%p的结果一样可以用ints[i]%p的结果递推得到。 虽然ints[i]%p=intt%p并不能保证ints[i]=intt成立,但是我们可以保证这一点: 若ints[i]%p<>intt%p,则ints[i]!=intt一定成立
RK算法 那么下一个问题就是,当ints[i]%p=intt%p时,怎么办? 这种情况肯定是需要进一步验证的。一个简单的想法是:直接比较t和s[i..i+m-1]。 考虑到ints[i]%p=intt%p而ints[i]<>intt的情况很难出现: (1)由于p是大质数,这种情况是不多见的,大家可以算下概率; (2)并且p是可以随便选的,很难出有针对性的数据。 所以这个策略虽然简单,但并不坏。
RK算法 RK算法的平均时间复杂度也是线性的。
模式匹配思考题 • 如果要问模式串一共在母串中出现了多少次,应该怎么修改算法? • 如果要问第k次出现的位置,或者最后一次出现的位置呢? • 如果有多个模式串呢?
后缀数组 首先明确几个概念: • 字符集:所有元素存在全序关系的集合 • 字符串:对于字符串s,定义s[i]为字符串的第i个字符,len(s)为字符串s的长度 • 子串:对于字符串s,和任意的i和j满足1<=i<=j<=len(s),定义s[i..j]为s[i]、s[i+1]、…、s[j]顺序排列构成的字符串,显然s=s[1..len(s)] • 后缀:对于字符串s和任意的i满足1<=i<=len(s),定义suf(s,i)=s[i..len(s)] • 前缀:对于字符串s和任意的i满足1<=i<=len(s),定义pre(s,i)=s[1..i]
后缀数组 后缀数组是针对一个字符串s的1..n的排列: Sa[1]、Sa[2]、…、Sa[n] 因为总是针对字符串s的,故用n来代替len(s) 这个排列满足: suf(Sa[1])<suf(Sa[2])<…<suf(Sa[n]) 因为总是针对字符串s的,所以用suf(i)来代替suf(s,i)了。这里一定不会出现相等的情况,因为若i<>j则suf(i)与suf(j)长度不等,所以肯定suf(i)与suf(j)也不等。 同时定义Sa的反函数Rank[Sa[i]]=i
后缀数组 • Sa数组: • Sa:Suf at,排第几位的后缀 • 下标:顺序号(序号) • 值:位置号(对应从此位置开始的后缀) • Rank数组 • 下标:位置号 • 值:顺序号
后缀数组 所以后缀数组其实就是字符串s的所有n个后缀的字典序。 那么问题就是:如何得到Sa序列?
后缀数组 • 首先扩展下前后缀的定义: • 若len(s)<i,则suf(s,i)等于空串 • 若len(s)<i,则pre(s,i)=s • 定义sufk(i)=pre(suf(i),k)=s[i]开始的后缀的k长前缀 • 那么有结论: • suf2k(i)=suf2k(j)等价于sufk(i)=sufk(j)且sufk(i+k)=sufk(j+k) • suf2k(i)<suf2k(j)等价于sufk(i)<sufk(j)或sufk(i)=sufk(j)且sufk(i+k)<sufk(j+k)
后缀数组 后缀数组求字典序的思想就是利用前面的结论,通过suf1(1..n)的字典序计算suf2(1..n)的字典序,再计算出suf4(1..n)的字典序,…… 直到算出suf2^k(1..n)的字典序,且2^k>=n,那么suf2^k(1..n)的字典序就是suf(1..n)的字典序了。 于是问题就变成了怎么通过sufk(1..n)的字典序,求得suf2k(1..n)的字典序。
后缀数组 如果我们想要将总时间复杂度控制在O(nlogn),那么 “通过sufk(1..n)的字典序,求得suf2k(1..n)的字典序” 就必须在O(n)的时间复杂度内解决。
后缀数组 定义Sak[1..n]为sufk(1..n)的字典序,即Sak是满足 sufk(Sak[1])<=sufk(Sak[2])<=…<=sufk(Sak[n]) 的1..n的排列。 注意这里是“<=”,而之前定义SA的时候是“<”。 定义Rankk[i]表示: 在“空串”、sufk(1)、sufk(2)、…、sufk(n)中小于sufk(i)的不同的字符串的个数。 这里去掉“不同”也可以,但是算起来就麻烦点,而Rankk[i]仅仅是用来比大小的,所以只需要知道“不同”的字符串的个数就够了,详见下文。
后缀数组 那么对于任意的1<=i,j<=n,sufk(i)与sufk(j)的大小关系就完全决定于Rankk[i]与Rankk[j]之间的大小关系。 于是对于任意的1<=i,j<=n,suf2k(i)与suf2k(j)的大小关系就完全决定于二元组 (Rankk[i], Rankk[i+k])与(Rankk[j], Rankk[j+k]) 按第一元为主关键字,第二元为次关键字的比较结果。
后缀数组 于是suf2k(1..n)的字典序Sa2k[1..n],就是三元组: (Rankk[1], Rankk[1+k], 1), (Rankk[2], Rankk[2+k], 2), …… (Rankk[n], Rankk[n+k], n) 按第一元为主关键字,第二元为次关键字排序后的结果,只保留第三元的序列。 这里Rankk的下标超n了也没有关系,照着之前扩展了定义的前后缀来理解,当x>n时显然Rankk[x]=0
后缀数组 如果已知了Sa2k[1..n],Rank2k[1..n]也很好求,只需要按Sa2k的顺序扫描一次,就可以求出来了: 若suf2k(Sa2k[i])与suf2k(Sa2k[i+1])相等,则Rank2k[Sa2k[i+1]]=Rank2k[Sa2k[i]],否则Rank2k[Sa2k[i+1]]=Rank2k[Sa2k[i]]+1。 因为判断suf2k(Sa2k[i])与suf2k(Sa2k[i+1])的大小只不过是比较两个二元组。所以这个扫描过程的时间复杂度是O(n)的。 因此关键是之前的排序。易知Rankk[i]都是小于等于n的非负整数,所以可以用基数排序
基数排序 对于序列a[0..n-1],若已知0<=v(a[i])<=x,则可以通过下面的过程排序(b[0..n-1]就是排序的结果): c[0..x]=s[0]=0 for i=0 to n-1 do c[v(a[i])]++ // 统计各值的出现频度 for i=1 to x do s[i]=s[i-1]+c[i-1] // 计算起始位置 for i=0 to n-1 do b[s[v(a[i])]++]=a[i] // 安排新位置 当然实际写程序时,s和c是可以想办法复用的。 v(a[i]) (即value(a[i]))是a[i]的比较基数,即a[i]与a[j]的大小关系由v(a[i])与v(a[j])的比较结果决定。 启用v(a[i])的概念是考虑到a[i]可能是一个结构,带有附属的不参与大小比较的内容。 这个过程有个特别的好处是:相同的元素排序后的位置关系与排序前的位置关系相同,在多关键字基数排序时这一点是很重要的。
多关键字的基数排序 • 对于二元组序列(a[0],b[0])、(a[1],b[1])、……、(a[n-1],b[n-1])按第一元为主关键字、第二元为次关键字进行基数排序,只需要将普通的基数排序走两遍就可以了。 • 先令v(a[i],b[i])=b[i],进行一轮基数排序; • 再令v(a[i],b[i])=a[i],进行一轮基数排序。 • 若0<=a[i],b[i]<=x,则这个过程的时间复杂度是O(x+n)。而我们要做基数排序的rankk[i]都是小于等于n的,所以后缀数组中一次基数排序的时间复杂度是O(n)。
后缀数组 于是后缀数组的程序就呼之欲出了: 首先轻松得到Sa1[1..n]和Rank1[1..n] k=1 while (k<n) do begin 利用Rankk[1..n]通过基数排序得到Sa2k[1..n] 扫描一遍Sa2k[1..n]得到Rank2k[1..n] k*=2 endwhile 易知时间复杂度为O(nlogn)。
后缀数组 于是对于一个给定的字符串s[1..n],我们可以在O(nlogn)的时间复杂度内求出Sa[1..n]和Rank[1..n],但是为了解题,通常还需要另一个概念——最长公共前缀: lcp(s,t)=min{max{x | pre(s,x)=pre(t,x)}, len(s), len(t)} 在后缀数组中,又可以定义: LCP(i,j)=lcp(suf(Sa[i]), suf(Sa[j]))
后缀数组 对于LCP有结论: • LCP(i,j)=LCP(j,i) • LCP(i,i)=len(suf(Sa[i]))=n-Sa[i]+1 • ( LCP引理)对任意1<=i<j<k<=n,有 • LCP(i,k)=min{LCP(i,j), LCP(j,k)} • ( LCP定理)对任意1<=i<j<=n,有 • LCP(i,j)=min{LCP(k,k+1) | i<=k<j} • ( LCP推论)对任意的1<=i<=j<k<=n,有 • LCP(i,k)<=LCP(j,k)
后缀数组 因为LCP定理,所以如果定义: • 若i>1则height[i]=LCP(i, i-1), • 否则height[i]=0 则LCP(i,j)=min{height[x] | i<x<=j} 于是LCP问题就成了RMQ问题。 但是height[1..n]又如何算得?
后缀数组 定义h[i]=height[rank[i]],即在排好序的后缀序列中,suf(i)与排在他前面的后缀的最长公共前缀。 而height[i]则是排好序的后缀序列中排第i位的后缀与排在他前面的后缀的最长公共前缀。
后缀数组 对于h[i],有性质: • 若1<=i,j<=n,且lcp(suf(i), suf(j))>0,则 • suf(i)<suf(j)等价于suf(i+1)<suf(j+1) • lcp(suf(i), suf(j))=1+lcp(suf(i+1), suf(j+1)) • 若i>1则 • h[i]>=h[i-1]-1 • 前面的应该比较显然,最后一条就不是很显然了
后缀数组 • 当h[i-1]<=1时,因为h[i]>=0>=h[i-1]-1,所以h[i]>=h[i-1]-1 • 当h[i-1]>1时,显然suf(i-1)不会是排序排在第一位的后缀,此时令x=Sa[Rank[i-1]](即suf(x)是排在suf(i-1)前一位的后缀),显然suf(x)<suf(i-1) • 因为h[i-1]=lcp(suf(x), suf(i-1))>0,所以由上一页前两条性质可知: • suf(x+1)<suf(i),即Rank[x+1]<Rank[i],即Rank[x+1]<=Rank[i]-1 • lcp(suf(x+1), suf(i))+1=lcp(suf(x), suf(i-1)=h[i-1],即 lcp(suf(x+1),suf(i))=h[i-1]-1 • 即在suf(i)的前面有一个后缀suf(x+1),他们两个的最长公共前缀长度正好是h[i-1]-1,而由LCP推论和LCP的定义可知: • h[i-1]-1 = lcp(suf(x+1),suf(i)) = LCP(Rank[x+1],Rank[i])<=LCP(Rank[i]-1, Rank[i]) = h[i] • 即h[i]>=h[i-1]-1
后缀数组 …. suf(x) suf(i-1) … …. suf(x+1) … suf(j) suf(i) … LCP=h[i-1]>0 LCP=h[i-1]-1 LCP=h[i] 由LCP推论可知:h[i]>=h[i-1]-1
后缀数组 于是可以得到计算height和h数组的算法: for (int i=1; i<=n; i++){ if (Rank[i]=1) h[i]=0; else { j=Sa[Rank[i]-1]; // h[i]=lcp(suf(i), suf(j)) h[i]=0; // 首先得到h[i]可能的最小值 if (i>1 && h[i-1]>=1) h[i]=h[i-1]-1; while (max(i,j)+h[i]<=n && s[i+h[i]]==s[j+h[i]]) h[i]++; } height[Rank[i]]=h[i]; } 已知,计算h[1..n]判断字符是否符相等(“s[i+h[i]]==s[j+h[i]]”这个语句)的总次数应该是O(n)次。所以这个过程的时间复杂度是O(n)的。
例题一:最长回文子串问题 题意:给定一个字符串str,求其一个最长的是回文的子串的长度。 方法: 构造字符串s为:str加上一个一定不在str中出现的字符(假设是#字符),再加上str的反向串。若str的长度为m,则s的长度n=2*m+1 求s的后缀数组Sa、Rank、h、height,以及height的RMQ。
例题一:最长回文子串问题 对于任意的i满足1<=i<=m, • str[i..m]就对应了suf(i)不包含#的最长的前缀; • str[1..i]的反向串就对应了suf(n-i+1)。
例题一:最长回文子串问题 • 如果我们要求以str[i]为中间线的最长回文子串长度,只需要知道str[i..m]与str[1..i]的反向串的最长公共前缀,而这个问题等价于求suf(i)与suf(n-i+1)的最长公共前缀长度,即lcp(suf(i), suf(n-i+1)) • 而lcp(suf(i), suf(n-i+1))=LCP(Rank(i), Rank(n-i+1)),由RMQ我们知道O(1)的时间复杂度就能够得到这个值。 • 于是枚举所有的i,就可以求出所有长度为奇数的回文子串的最大长度。 • 长度为偶数的回文子串也是类似。
例题一:最长回文子串问题 于是问题就完美得解决了。 首先是构造s,然后解后缀数组,最后是枚举中间线,总时间复杂度是O(nlogn)。
例题二 FOJ1872:A New Sequence Problem 题意: 给定一个序列A的定义如下(其中Fib[i]为Fibonacci数列的第i项): A[0]=((Fib[a^b]%c)*c)%200003 A[i]=(A[i-1]^2)%200003(i>=1) 定义B[j,k]为A[j..j+k-1]构成的子序列 定义Cnt[j,k]为B[j,k]在A中出现的次数 定义S[j,k]=(Cnt[j,k]-1)*k 令n为A序列的长度 输入a,b,c,n,求对所有可能的j和k,S[j,k]的最大值
例题二 对于Cnt[j,k],假设A序列为{1,1,1,1,1},则B[0,2]为{1,1},Cnt[0,2]=4