大白话DC3算法

DC3算法是什么

DC3算法(也称为Skew算法)是一种高效的构建后缀数组的算法,全称为Difference Cover Modulo 3算法。

该算法于2002年被提出,论文参考:

https://www.cs.cmu.edu/~guyb/paralg/papers/KarkkainenSanders03.pdf

后缀树的引入

后缀数组是什么

后缀数组是一个整数数组,用来表示某个字符串所有后缀在字典序中的排序。

  • sa数组

后缀数组又称为sa数组(Suffix Array),其元素表示原字符串各后缀在字典序中的位置排序。

如果字符串S的后缀树组为sa,那么sa[i] 表示S中第i小的后缀在原字符串中的起始位置。

  • rank数组

与sa数组对应的是rank数组,它表示每个位置上的后缀在后缀数组中的排名。

如果字符串S的后缀树组为sa,那么rank[i]表示以i口头的后缀在后缀数组中的排名。

  • 两者关系

根据上面的定义,我们可以知道,rank数组是sa数组的逆数组,也即是:

rank[sa[i]] = i,sa[rank[i]] = i


我们通过 banana 这个字符串来演示 sa数组 以及 rank数组的工作原理

  • step1:构建所有后缀并按字典顺序排序

列出 “banana” 的所有后缀

    0: banana
    1: anana
    2: nana
    3: ana
    4: na
    5: a

按字典顺序排序后

    5: a
    3: ana
    1: anana
    0: banana
    4: na
    2: nana
  • step 2: 构建后缀数组 SA 和 rank 数组

根据排序结果,构建sa数组 :

    SA = [5, 3, 1, 0, 4, 2]

构建 rank 数组:

    rank[5] = 0
    rank[3] = 1
    rank[1] = 2
    rank[0] = 3
    rank[4] = 4
    rank[2] = 5

因此 rank数组为:

    rank = [3, 2, 5, 1, 4, 0]

再来看一个例子

img

能解决什么

后缀数组+rank数组 可以接解决非常多的字符串问题

还能去做RMQ问题,也即是Range Maximum/Minimum Query 的缩写,表示区间最大(最小)值

关键点在于后缀数组怎么样形成最方便

image

后缀数组的地位

它的地位非常高, 很多的问题都是由 后缀树后缀数组 实现的

后缀树 实现 跟后缀数组实现的区别

后缀数组可以替代非常多后缀树的内容

因为后缀树生成是更麻烦的一个东西

我们用后缀数组可以代表字符串的一些后缀信息, 可以解决很多字符串的问题

后缀树案例说明

后缀字符串

从开头开始往后所有都要

image

对所有后缀字符串做典序排序

所谓的后缀数组它其实代表所有的后缀字符串在排完名之后,从第0名到第7名依次写下来,这就是所谓的后缀数组

image

注意一点:不会有排名相同的,因为所有后缀串的长度不同

怎么样生成排名

先看一下暴力方法生成的流程

  • step1:先生成所有的后缀字符串, 复杂度O(N^2)

image

  • step2:后缀字符串数量 O(N), N个后缀字符串排序, 代价O(N*logN)

两个字符串比较大小的复杂度是O(1)吗? ==> 和字符串的长度有关,后缀串的平均长度O(N)

暴力生成的复杂度

image

暴力方法生成后缀数组

  • 生成所有的后缀串

光生成后缀数组的字符串数组代价就已经是O(N^2)了

后缀字符串的数量是O(N)的

image

  • 给定长度为N的字符串

1)生成所有后缀数组的字符串数组, 复杂度O(N^2), 也是N

image

2)排序, N*logN, 但问题是这里的排序是两个字符串直接比较大小

字符串的平均长度是N/2

把字符串数组排好序, 单个两个字符串比对的代价O(N)

字符串排序的代价是O(N^2*logN)

是一个很痛的瓶颈

image

DC3算法引入

上面暴力方法说明生成后缀树时,字符串间的字典序比较是一个耗时操作,我们需要寻求是否有一种算法来优化这一部分内容。

于是DC3算法应运而生,它能够做到时间复杂度为 O(N) 解决上面的问题。

其核心思想:根据下标模3来分组, 做了一个类似于递归的事情来把这个问题解决的。

后缀数组的复杂度跟要求

后缀数组可以做到生成复杂度: O(N)

但是有一个前提:初始时, 数组里头每一个的值不要太大

如果非常大, 下面的方法就不合适了, 常数项就会有点大

image

回顾:基数排序

比如所有的数字都是三位

  • step1:先按照个位数进桶

image

  • step2:从 0~9 顺序从桶中拿出数字来,然后按照十位数字进桶,后进的权重更大

  • step3:重复step2,直到最高位

image

这里我们可以做一个升级, 有两位数据, 每一个数据有两个指标

56, 4 : 56 算一位, 不要拆开

对上面数据排序

3-7 最前 17-79 第二 23-34第三位 最后56-4

image

如果N个样本, 数据的维度不超过3维, 而且每个维度的值相对较小(比如1000以内),否则基数排序的常数项极大

则N个样本排序的代价可以做到O(N)

image

如果一个数据有3维信息, 每个位置的值不会特别大的话,

如果有N个数据, 排完序的代价还是O(N)的

前提是每一位上的值不要太大

image

再谈后缀树组

把字符串可以认为是一个数组, 每个位数据是这个字符串的ASCII码

=>可以把一个字符串求后缀数组,等同于对一个数组在求后缀数组。

image

所以字符串可以求后缀数组,对于单独数组来说也可以求后缀数组

image

背景介绍

比赛中常用的后缀数组生成算法,时间复杂度最优的解是DC3算法,复杂度为O(N)

但是由于后缀数组一般耗时很久,所以比赛中的测试数据不会给的很高,选手也常用倍增算法解决, 它的复杂度为O(NlogN), 但是常数项优秀(从上面的介绍中可以看到,DC3算法使用了基数排序,常数项其实是很差的)。

image

扯得有点远,我们说回到 DC3算法。

DC3算法思路

下标分类

  • 首先, 按 索引对3取模 做下标分类

i%3== 0 ==> 0类下标, 代号s0类

i%3== 1 ==> 1类下标, 代号s1类

i%3== 2 ==> 2类下标, 代号s2类

所有后缀字符串, 长度不一样, 所以不会有后缀串排名一样

image

S12推出S0内部排名

假设有一个非常方便的方法 先把S1, S2类的后缀数组排名求出来(后面展开,这里先把S12当做一个可以使用的黑盒

我们能不能得出所有0类, 1类, 2类下标的整体排名?

所有3类混在一起的下标排名能不能通过S1, S2类的已经出来的信息,给它都加工出来?

image

怎么使用S1,2类的信息呢?

先看看S12类能不能把S0内部的排名搞定

image

字符串不相同的情况

  • 0开头的a

  • 3开头的a

  • 6开头的c

如果只看一个字符, 6的排名是不如0,3的

这个过程可以用基数排序

s0内部的首字母不一样, 根据首字母就可以划块了

首字母不一样的块之间的排名, 我一定能求出顺序

下面的问题就是0,3开头,这些同样在一个块内部的这些后缀串怎么排名?

image

  • 0开头的字符是a,后面的是1开头及其后面所有,S12中1开头排第3位,因此S0中记为a_3

  • 3开头的字符也是a,后面的是4开头及其后面所有,S12中4开头排第1位,因此S0中记为a_1

在S12中:1开头及其后面的所有 跟 4开头及其后面的所有的总排名我们是知道的

虽然第一维数据一样, 但是0开头的 a_3 是干不过3开头的 a_1

依然可以基数排序, 过一遍决定顺序, 时间复杂度O(S0的大小),也即O(N/3)=O(N)

还是基数排序, 相当于一个对象 两位的信息

image

S12与S0的Merge

由上面的推导可以知道,如果已知S12,那么可以在O(N)的时间复杂度内推出S0。

接下来,自然而然地想到,如果需要得到整体排名,我们只需要得到 S12与S0的merge策略。

image

3 开头的 跟 2开头的谁小谁排出

依次从上往下, 如果我们存在merge策略

总排名就出来了

image

怎么进行merge呢?

我们先整体审视一变,同常规的 归并排序,或者说 map-reduce的reduce过程类似,由于S0以及S12都是有序的,我们只需要在S0以及S12侧都给到一个指针(表示在S0以及S12中数组的索引),逐一比较大小即可。

不妨设S0此时指针来到 p0,S12指针此时来到p12,如果p0指向的索引开头的字符以及p1指向的索引开头的字符不同则直接可以比较大小,我们不用进行后面的操作了。

如果 p0 以及 p12 指向的索引对应的字符相同,我们需要分情况讨论:

  • 1)假设p12此时指向的是S1,例如 p0 此时指向6,p12此时指向10,那么只需要再向后看一位就一定可以知道排序,由于p0后面一位的字符为第7位以及后面所有的字符,而p12后面一位的字符为第11位以及后面所有的字符,我们知道 7和11的位置都在S12中,而S12的总体排序又是知道的,那么直接判断即可。

  • 2)假设p12此时指向的是S2,例如 p0 此时指向6,p12此时指向11,那么只需要最多再向后看两位就一定可以知道排序,由于p0后面一位的字符为第7位以及后面所有的字符,而p12后面一位的字符为第12位以及后面所有的字符,如果第7位以及第12位字符不同,那么第6位以及第11位的后缀数组的字典序也必就能比较出来,如果还是相同,由于7在S12,而12在S0,不能直接比较,但是再往后看一位,6后面的第二位为8,11后面的第二位为13,8和13的位置都在S12,可以直接比较,那么6和11的顺序也就出来了。

总结一下:

  • 如果p0以及p12指向索引的字符不同,直接出结果

  • 如果p0以及p12指向索引的字符相同

    • p12位置为S1,再向后看一位出结果,加上当前位,也就是最多看2位就能出结果

    • p12位置为S2,最多再向后看两位出结果,加上当前位,也就是最多看3位就能出结果

也就是 merge 中每一步的比较时间都是 O(1) 的

下面还是看上面的具体案例

一开始 3 开头的 跟 2开头的 pk

  • 3开头的上面字符是a

= 2开头的上面字符也是a

所以要再往下比较, 都是a, 又比不出来

再往下

  • 考察5及其后面的整体

  • 和4及其后面的整体

这俩排名在s12里有 ==> 最多三维信息可以搞定merge,只用比三个位置的信息就可以决定3开头跟2开头谁大谁小

这都不是基数排序,就三个位置的值比较一下

image

3是一个s0类的下标, 遭遇的是2 s2类的下标

当S0类下标在merge的时候遭遇了S2类下标跟它pk的时候, 往后要比三位

S0 跟 S2下标pk的时候, 最多拿三个位置的数比一下, 就知道怎么merge了, 谁大谁小就出来了

image

后面的信息, 3开头的是 2, 2开头的是1, 3开头的没pk过2开头的

2开头的就是所有下标开头的Number0

接下来3开头的跟4开头的pk

image

怎么样pk3开头的跟4开头的?

  • 3开头的是s0类型

  • 4开头的是s1类型

当S0类 跟 S1类下标pk的时候, 最多比两个位置的数

3位置是a, 4位置也是a

接下来比4及其后面的整体 和 5及其后面的整体

这俩排名在s12内部的排名是有的

所以S0类 跟 S1类下标pk, 最多比两个位置的数, 可以决定大小

image

DC3算法实现

总体流程

首先字符串长度N

  • step1:有一个f()函数 帮我得到S12内部的排名

  • step2:用s12 的信息得到s0内部的排名

    • 使用基数排序: O(N)
  • step3:把S0 跟 S12去merge, 又利用到s12类的信息, 都排出来,时间复杂度: O(N)

step2以及step3的求解过程上面已经说明了,后面就不赘述了

可以看到, 整个流程中成败的关键就是第一步, 后面两个过程都是O(N)的过程

image

如何得到S12类的排名

处理流程

要求这个排名一定要是精确的,里面不要有重复值

画下划线的就是s12类

先这么想,怎么决出下面的整体排名

  • 1开头及其后面的所有

  • 2开头及其后面的所有

  • 4开头及其后面的所有

  • 5开头及其后面的所有

  • 7开头及其后面的所有

  • 8开头及其后面的所有

  • 10开头及其后面的所有

  • 11开头及其后面的所有

image

先只拿前三维信息做比较,如果此时字符不够3位了,用极小值来填充

  • 1开头前三个字符aab

  • 2开头前三个字符abb

  • 4开头前三个字符bbc

  • 5开头前三个字符bcc

  • 7开头前三个字符cca

  • 8开头前三个字符caa

  • 10开头前三个字符aa0

  • 11开头前三个字符a00

这个例子展示的是:如果我们仅拿3位信息就足以得到严格排名的话, 那岂不爽哉

那么这样的话只需要经过一个O(N)的基数排序就拿到S12了,接下来可以直接去做后面2,3的步骤

现实情况是: 有可能只拿前三位信息它排不出严格的排名

image

我们拿mississippi这个单词来做说明(注:这个是DC3论文中作者的提过的案例)

只拿1,2类信息的前3维信息 能不能得出精确排名呢?答案是不行滴 o(╥﹏╥)o

image

这样的话, 我们就没有决出精确排名

image

好了,整个算法中最精髓的部分,它来了!!!

生成一个数组:

  • 把S1类整体放在数组左边

    • S1类所对应的排名放数组左边
  • 把S2类全放在数组右边

    • S2类所对应的排名放数组右边

image

获取结果

我们把这个数据认为是个字符串数组,递归的调用求它后缀数组的排名

求出来了哪个是第一名?

1554, 3位置是第一名, 对应会原数组中10 它是第一名

下面是2开头的 21554, 对应会原数组中7 它是第二名

接下来是321…, 是原数组第1位置

接下来是3321…, 是原数组第0位置

接下来是4, 是原数组第6位置

接下来是54, 是原数组第5位置

接下来是554, 是原数组第4位置

后缀数组的这个结论求出来以后, 它可以对应回原来我想求这个东西里面的s12类的排名!!!

image

结论解释

一开始有一个比较粗糙的排名, 没有得到精确的排名,比如说 有两个3, 两个5

如果你把这个排名的值当做是字符的话, 我这个1不可能在排完字符串之后排名往后掉

相当于我首字母就是1,首字母如果划分出块了, 即便后面怎么摆, 也不会让排名乱掉, 块与块之间排名不会乱掉

为什么要这么摆?这么摆可以把同一块内部的排名搞定!


先看两个相同索引同时落在S1或者同时落在S2的情况

看这个两个3

3: 1开头及其后面的所有

3: 4开头及其后面的所有

分不出来

因为我们只拿了前3位信息出来, 它们彻底相等, 所以区分不出来

接下来, 你需要什么信息?

3: 123 4开头及其后面的所有

3: 456 7开头及其后面的所有

s1类摆在整体左边, s2类摆在整体右边

对于同一组内部, 这么摆, 正好这个结构关系能帮它区分排名

image

5: 234 5开头及其后面的所有

5: 567 8开头及其后面的所有

这个例子展示的是, 相同的排名信息落在一组里的情况, 都落在左组或者都落在右组

image


那如果相同的信息落在不同组呢?

其实相同的排名即便是跨组, 这么摆, 也能搞出区分来

比如说原始位置为1和5的两个位置,它们在非严格区分排名中都是第3名

看看下一位的信息:

123,下一位为4,在新数组中紧靠着1,假设排名为x

567,下一位为8,在新数组中紧靠着5,假设排名为y

那么现在需要比较 3_x 以及 3_y的大小,这个显然与新数组的后缀树的排名仍然是保持一致的。

那么也就是说无论相同信息落在同一组还是不同组,都能验证上面的结论。


S1及S2交接位置处理

现在还有一个小坑:S1类以及S2类交接的位置相同时如何比较排名

这里如果S1的最后一位本来就补了0,那么S2中的数顺着S1往后一定可以找到不同的3位后缀

如果S1的最后一位实在无法补0,那我们人为给S1补一个000

  • 假设长度 % 3 == 2

S1的最后一位会补0,因此S2中的后缀值一定能够与它分出胜负

比如说长度为11,索引为 0~10,10的3位后缀,后面有两位用0补了

那么索引为2的后缀 与1、4、7 相等时,1、4、7对应的后缀为 4、7、10及后面的数字,这个其实在SA12中都有了,而且值也是正常的,唯一有点差异的是 索引为 10的,假设 num[2] = num[10] = 5,那么2的三个数的后缀位 5xy,x>0,y>0,而10的三个数的后缀位为500,基数排序时已经分出胜负了

image

  • 假设长度 % 3 == 0

情况与 长度 % 3 == 2 类似,此时 S1的最后一位会补0,S2中顺着S1至少可以找到一个不同值。

比如长度为9,索引为 0~8,S2前面的值与S1比较逻辑与上面的类似,如果是索引为8和索引为7的3位后缀进行比较,假设在这两个索引上的数字都是5,那么7对应550,8对应500,基数排序后 7 本来就在8的后面。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值