1 / 61

字符串

字符串. 李子星. 字符串相关算法和数据结构. 字符串相关算法 KMP 算法 RK 算法 *有穷状态自动机( DFA ) 字符串相关数据结构 后缀数组 *后缀树. 模式匹配算法. 问题: 给定一个字符串 s[1..n] (又称为母串),和另一个长度不超过 s 的字符串 t[1..m] (又称为模式串)。 问: t 有没有在 s 中出现过。 例如: s 串为 abcdaba , t 串为 cdab ,那么 t 在 s 中出现过。 s 串为 abcdaba , t 串为 cdad ,那么 t 在 s 中没出现过。. 朴素的模式匹配算法.

damara
Download Presentation

字符串

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 字符串 李子星

  2. 字符串相关算法和数据结构 • 字符串相关算法 • KMP算法 • RK算法 • *有穷状态自动机(DFA) • 字符串相关数据结构 • 后缀数组 • *后缀树

  3. 模式匹配算法 问题: 给定一个字符串s[1..n](又称为母串),和另一个长度不超过s的字符串t[1..m](又称为模式串)。 问:t有没有在s中出现过。 例如: s串为abcdaba,t串为cdab,那么t在s中出现过。 s串为abcdaba,t串为cdad,那么t在s中没出现过。

  4. 朴素的模式匹配算法 若t在s中出现过,则一定存在一个i满足: 1<=i<=n且s[i..i+m-1]=t[1..m]。

  5. 朴素的模式匹配算法 于是最朴素的算法就是,枚举所有的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; }

  6. 朴素的模式匹配算法 下面检查下时间复杂度: 显然是O(n*m),虽然一般不会这么极端,但是效果肯定不好。 因为做了很多无用功。

  7. 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次匹配:

  8. t[next[i]] t[i] KMP算法 • KMP算法的关键就是找到所有这样的对应关系 • 通常用一个next数组来表示这样的对应关系 • next[i]=x表示:t[1..i-1]与t[1..x-1]右对齐后完全匹配,并且找不到另一个y大于x且满足同样的条件 • next[1]=0,这是边界情况

  9. 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数组:

  10. 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); }

  11. 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)

  12. KMP算法——next数组 剩下的问题:怎么求next数组? 可以利用类似的思想:利用next数组中已知的部分求未知的部分。

  13. 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

  14. 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++; } }

  15. KMP算法——next数组 下面讨论时间复杂度: • “next[i]=j;”和“i++; j++;” 会执行多少次? • 显然是m次 • “j=next[j];” 会执行多少次? • 一样可以用i-j的差来分析,结果也是m次 • 结论:这个过程的时间复杂度是O(m)

  16. KMP算法——总结 • 先求next数组 • 然后利用next数组来寻找匹配的位置 • 总时间复杂度为O(m+n)

  17. RK算法 假设构成字符串的字符的是0~9这十个数字,那么模式串实际上可以看做是一个十进制数(虽然可能很大)。 朴素的模式匹配中对s[i..i+m-1]与t[1..m]的比较就可以看做是两个数字的比较。

  18. 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]就没法方便的存储了。

  19. 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一定成立

  20. RK算法 那么下一个问题就是,当ints[i]%p=intt%p时,怎么办? 这种情况肯定是需要进一步验证的。一个简单的想法是:直接比较t和s[i..i+m-1]。 考虑到ints[i]%p=intt%p而ints[i]<>intt的情况很难出现: (1)由于p是大质数,这种情况是不多见的,大家可以算下概率; (2)并且p是可以随便选的,很难出有针对性的数据。 所以这个策略虽然简单,但并不坏。

  21. RK算法 RK算法的平均时间复杂度也是线性的。

  22. 模式匹配思考题 • 如果要问模式串一共在母串中出现了多少次,应该怎么修改算法? • 如果要问第k次出现的位置,或者最后一次出现的位置呢? • 如果有多个模式串呢?

  23. 后缀数组 首先明确几个概念: • 字符集:所有元素存在全序关系的集合 • 字符串:对于字符串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]

  24. 后缀数组 后缀数组是针对一个字符串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

  25. 后缀数组 • Sa数组: • Sa:Suf at,排第几位的后缀 • 下标:顺序号(序号) • 值:位置号(对应从此位置开始的后缀) • Rank数组 • 下标:位置号 • 值:顺序号

  26. 后缀数组 所以后缀数组其实就是字符串s的所有n个后缀的字典序。 那么问题就是:如何得到Sa序列?

  27. 后缀数组 • 首先扩展下前后缀的定义: • 若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)

  28. 后缀数组 后缀数组求字典序的思想就是利用前面的结论,通过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)的字典序。

  29. 后缀数组 如果我们想要将总时间复杂度控制在O(nlogn),那么 “通过sufk(1..n)的字典序,求得suf2k(1..n)的字典序” 就必须在O(n)的时间复杂度内解决。

  30. 后缀数组 定义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]仅仅是用来比大小的,所以只需要知道“不同”的字符串的个数就够了,详见下文。

  31. 后缀数组 那么对于任意的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]) 按第一元为主关键字,第二元为次关键字的比较结果。

  32. 后缀数组 于是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

  33. 后缀数组 如果已知了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的非负整数,所以可以用基数排序

  34. 基数排序 对于序列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]可能是一个结构,带有附属的不参与大小比较的内容。 这个过程有个特别的好处是:相同的元素排序后的位置关系与排序前的位置关系相同,在多关键字基数排序时这一点是很重要的。

  35. 多关键字的基数排序 • 对于二元组序列(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)。

  36. 后缀数组 于是后缀数组的程序就呼之欲出了: 首先轻松得到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)。

  37. 后缀数组 于是对于一个给定的字符串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]))

  38. 后缀数组 对于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)

  39. 后缀数组 因为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]又如何算得?

  40. 后缀数组 定义h[i]=height[rank[i]],即在排好序的后缀序列中,suf(i)与排在他前面的后缀的最长公共前缀。 而height[i]则是排好序的后缀序列中排第i位的后缀与排在他前面的后缀的最长公共前缀。

  41. 后缀数组 对于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 • 前面的应该比较显然,最后一条就不是很显然了

  42. 后缀数组 • 当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

  43. 后缀数组 …. 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

  44. 后缀数组 于是可以得到计算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)的。

  45. 例题一:最长回文子串问题 题意:给定一个字符串str,求其一个最长的是回文的子串的长度。 方法: 构造字符串s为:str加上一个一定不在str中出现的字符(假设是#字符),再加上str的反向串。若str的长度为m,则s的长度n=2*m+1 求s的后缀数组Sa、Rank、h、height,以及height的RMQ。

  46. 例题一:最长回文子串问题 对于任意的i满足1<=i<=m, • str[i..m]就对应了suf(i)不包含#的最长的前缀; • str[1..i]的反向串就对应了suf(n-i+1)。

  47. 例题一:最长回文子串问题 • 如果我们要求以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,就可以求出所有长度为奇数的回文子串的最大长度。 • 长度为偶数的回文子串也是类似。

  48. 例题一:最长回文子串问题 于是问题就完美得解决了。 首先是构造s,然后解后缀数组,最后是枚举中间线,总时间复杂度是O(nlogn)。

  49. 例题二 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]的最大值

  50. 例题二 对于Cnt[j,k],假设A序列为{1,1,1,1,1},则B[0,2]为{1,1},Cnt[0,2]=4

More Related