后缀数组的DC3构造法


本文的大部分内容都是在理解了这篇文章的大部分内容后而写:http://www.cppblog.com/superKiki/archive/2010/05/15/115421.aspx


在字符串相关的问题中,后缀数组是一种很有效的武器。

后缀数组至少可以解决如下一些问题(当然并不一定是所有下列问题的最优解法):

1. 在一个字符串中找出一个子串,这个子串出现至少2次(或者指定出现k次),且长度最长。(分为可重叠和不可重叠)

2. 在多个字符串中查找最长公共子串。

3. 某个字符串的最长回文子串。

4. 上述问题的变种。


作为一个菜鸟级程序员,对于理解后缀数组这样的算法着实费了不少劲。下面将我看到的和理解的关于后缀数组的内容介绍如下。


后缀数组

后缀数组是一种对字符串所有后缀进行排序得到的数组。比如对于字符串S=banana$,它的所有后缀为:

后缀i
banana$0
anana$1
nana$2
ana$3
na$4
a$5
$6

对所有这些后缀进行字典序排序的结果为:

后缀i
$6
a$5
ana$3
anana$1
banana$0
na$4
nana$2

如何构造后缀数组

构造后缀数组的算法主要分为:倍增算法、利用后缀树构造后缀数组以及一些递归算法。

本文主要介绍DC3算法,这是一种可以在线性时间内构造后缀数组并且已经有广泛应用的算法。关于该算法原汁原味的介绍为:http://algo2.iti.kit.edu/documents/jacm05-revised.pdf

该算法首先将所有后缀分为两部分:后缀起始位置模3为0和后缀起始位置模3不为0。先对模3不为0的后缀进行排序,方法如下图:


将后缀起始位置为1和后缀起始位置为2的两部分连接起来。注意这里连接的两部分后面都添加了0,这是为了让连接的两部分的长度都为3的倍数,这样才能3个字符一组进行合并。我们把诸如"aba","aaa"等字符串整体看成一个字符,那么上面连接后的字符串的长度就缩短为原来的三分之一,对于原始字符串来说,则缩短为三分之二。然后对于合并后的字符串递归进行DC3排序。这样,剩下的问题便是如何在已知后缀起始位置模3不为0的排序后得到模3为0的排序,并与模3不为0的后缀排序进行合并。

首先,对于模3为0的后缀的排序,可以看做是模3为1的后缀加上一个前缀组成的两部分。因此使用一次基数排序就可以完成。

然后,对于合并,可以采用一次简单的归并,因为模3为0的后缀与模3不为0的后缀有简单的转化关系,所以在合理的设计数据结构后可以达到线性复杂度。


复杂度分析

可以发现,算法的复杂度表示为:f(n)=O(n)+f(2*n/3)。
经过简单的运算可知,f(n)=O(n)。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以辅助您回答问题。以下是利用 Javascript 语言编写 DC3 算创建后缀数组的代码: ```javascript // 定义用于 DC3 算排序的变量 let t1, t2, sa12, sa0 function sortRank(t, sa, n, m) { // 记录排序结果到 t 数组 for (let i = 0; i < m; i++) counter[i] = 0 for (let i = 0; i < n; i++) counter[t[sa[i]]]++ let sum = 0 for (let i = 0; i < m; i++) { let t = counter[i] counter[i] = sum sum += t } for (let i = 0; i < n; i++) { let k = sa[i] t2[counter[t[k]]++] = k } for (let i = 0; i < m; i++) counter[i] = 0 for (let i = 0; i < n; i++) counter[t[t2[i] + k < n ? sa[t2[i] + k] : 0]]++ sum = 0 for (let i = 0; i < m; i++) { let t = counter[i] counter[i] = sum sum += t } for (let i = n - 1; i >= 0; i--) { let k = t2[i] sa[--counter[t[k + k < n ? sa[k + k] : 0]]] = k } // 合并排序结果,保存到 sa 数组 let j = 0 for (let i = 0; i < n; i++) { let p = sa[i], q = sa[j] let pp = p + k < n ? t[p + k] : -1, qq = q + k < n ? t[q + k] : -1 if (pp != qq) { pp = -1 } if (p + k < n && pp == qq) { if (q < sa12n) { pp = t[p + k + sa12n] == t[q + k + sa12n] ? 1 : 2 } else { pp = t[p + k] < t[q + k] ? 1 : 2 } } sa[j++] = pp == 1 ? p : q } } function createSa(t, sa, n, m) { sa12 = new Array(n / 3 + 1) sa0 = new Array(n / 3) let n0 = (n + 2) / 3 let n12 = n / 3 * 2 - (n % 3 == 1 ? 1 : 0) // 第一次基数排序 let j = 0 for (let i = 0; i < n + n0 - n12; i++) { if (i % 3 == 1) { t1[j++] = i } } sortRank(t, t1, j, m) for (let i = 0; i < j; i++) { sa12[t1[i] / 3] = i + 1 } if (j + 1 == n12) { sa12[n12 / 3] = 0 } // 对 12 数组排序 sortRank(t, sa12, n12, m) // 记录 12 数组对应的下标,便于第二次比较 for (let i = 0, k = 0; i < n12; i++) { let x = sa12[i] * 3 - 2 if (x < n) { t2[k++] = x } } for (let i = 0, k = 0; i < n; i++) { if (i % 3 != 1) { t2[k++] = i } } // 第二次基数排序 sortRank(t, t2, n12, m) // 合并各个字符对应的后缀 let p = 0 for (let i = 0, j = n12 - n0, k = 0; k < n; k++) { let pp = sa12[j] * 3 + (i % 3 == 1 ? 0 : 1) let qq = t2[i] if (j == n12 || (i < n0 && t[pp] < t[qq + k < n ? sa12[j + n0] * 3 + (qq + k < n ? 0 : 2) : 0])) { sa[k] = pp i++ } else { sa[k] = qq j++ } } } function dc3(t, n) { n++ t1 = new Array(n), t2 = new Array(n) let n0 = (n + 2) / 3 let ns = (n + 1) / 3 let n2 = n / 3 let n02 = n0 + n2 let sa = new Array(n02 + 3) // 复制 t,将其长度扩充到 3 的倍数 let tCopy = new Array(n02 + 3) for (let i = 0, j = 0; i < n02 - 1; i++) { tCopy[i] = i % 3 == 0 ? 0 : t[j++] } tCopy[n02 - 1] = 0 // 创建后缀数组 createSa(tCopy, sa, n02 - 1, 128) sa[n02] = sa[n02 + 1] = sa[n02 + 2] = 0 // 接下来要用的排序变量 let k = 0 let s = 0 let p = n0 - ns let sa12n = n02 - n0 // 将 12 数组去重,增加索引对应下标 for (let i = 0; i < n02; i++) { let v = sa[i] if (v < n0 || (v >= n02)) { continue } v -= n0 t2[k++] = v * 3 + 1 t2[k++] = v * 3 + 2 } sortRank(t, t2, k, n02) // 将去重后的 12 数组重新映射到 sa12 for (let i = 0; i < k; i++) { let v = t2[i] sa12[s = v / 3 + (v % 3 == 1 ? p : 0)] = i + 1 } // 如果 12 数组去重后还有重复值,递归进行 DC3 算 if (s + 1 < sa12n) { dc3(t2, sa12n) for (let i = 0; i < n02; i++) { sa12[t2[i] < n0 ? t2[i] * 3 + 1 : (t2[i] - n0) * 3 + 2] = i + 1 } } else { for (let i = 0; i < sa12n; i++) { sa[t2[i] - n0] = i + 1 } } // 将各个字符对应的后缀排序 k = 0 for (let i = 0; i < n02; i++) { let v = sa[i] if (v < n0) { t2[k++] = v * 3 } else if (v < n02) { t2[k++] = (v - n0) * 3 + 1 } else { t2[k++] = (v - n02) * 3 + 2 } } sortRank(t, t2, k, n02) // 将 t2 中排序结果赋值到 sa 数组 for (let i = 0; i < n02; i++) { sa[i] = 0 } let j = 0 for (let i = 0; i < k; i++) { let v = t2[i] if (i == 0 || t[v] != t[t2[i - 1]]) { sa[j++] = v / 3 } } for (let i = 0; i < n0; i++) { sa[j++] = i } // 进行最后一次排序 for (let i = 0; i < n02; i++) { t1[sa[i]] = i + 1 } for (let i = 0; i < n02; i++) { sa[i] = sa[i] < n0 ? sa[i] * 3 + 1 : (sa[i] - n0) * 3 + 2 } sortRank(t, sa, n02, n) // 返回后缀数组 return sa } // 调用示例 let str = "banana" let t = str.split("").map((c) => c.charCodeAt(0)) let sa = dc3(t, t.length) console.log(sa) ``` 注意:为了保护您的隐私,我不会保存您的任何要求或消息,也不会与其他人分享您的任何信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值