用 O(m) 时间复杂度找出一个长度为 m 的短字符串在一个长度为 n 的长字符串中的精确匹配(n>>m),限制长短字符串仅由 A、C、G、T 这四种字符组成。
输入:长短字符串
输出:短字符串在长字符串里的精确匹配
在匹配前先对长串进行编码和索引计算的预处理,该部分用到BWT编码的思想,并且利用后缀数组和倍增算法对编码过程进行优化。由于长串过长,在编码和存储时对其进行分治处理,并且在索引存储时进行优化,对索引进行间隔存储。在匹配时,用预处理得到的长串编码和索引进行匹配,用到的是FM索引-序列匹配。
BWT编码和索引计算
BWT的全称是The Burrows-Wheeler Transform,也就是所谓的轮转排序。在字符串匹配中首先用BWT对长字符串S进行预处理,得到编码结果BWT(S)和相应索引,用于后续FM索引技术对字符串进行匹配。
BWT编码对应具体步骤如下:
1、设需要编码的长字符串为S,在S末尾加上标记字符 $(该字符的字典序值小于长串中所有字符),得到新字符串S‘,记S’的长度为 len 。
2、对S’进行循环移位,每次将S’中的最后一个字符转移到整个字符串的最前面,一共转移 len 次,每次转移都得到一个长度为 len 的新字符串。
3、把上一步中得到的 len 个新字符串按照字典序排序,得到一个 len × len 的字符矩阵M。
4、M中每个字符串的第一个字符构成 F 列,最后一个字符构成 L 列。L 列即为BWT(S)。
通常为了节省空间,M 中各行字符串只保留 $ 之前的部分,也就得到所谓的后缀字符串组 Suffix,Suffix 中每一行的字符串都是 S’ 的一个后缀。
在求BWT(S)的过程中,需要同时建立索引,以节省后续字符串匹配时间。由于该问题中用到的长串和短串只包含 A、C、G、T 这四种字符,因此建立的索引主要这样三个部分:
1、num_a , num_c , num_g , num_t
:分别记录A、C、G、T 在 S 中出现的次数
2、后缀数组SA:SA[i]
表示后缀字符串组Suffix里的第 i 个字符串的起始字符在 S’ 中的下标,即F[i]
在 S’ 中的下标。
3、count_num[char][row]
:count_num[0][i]
表示到L[i - 1]
为止, A 出现过的次数;count_num[1][i]
表示到L[i - 1]
为止, C 出现过的次数,以此类推。即若L[i]
处的字符为 A,则要用当前 A 的计数将count_num[0][i]
的值赋完后才对 A 的计数+1,也就表示在 L 里的所有字符 A 中,L[i]
处的字符 A 的排序(该排序从0开始)。
根据上述BWT算法的处理步骤,对 S 进行BWT编码和计算索引的过程对应下述代码操作:
//轮转和排序
vector<string>M(len);
for(int i = 0; i < len; i++) {
string new_s(i + 1, ' ');
for(int j = 0; j < i + 1; j++)
new_s[j] = S[len - 1 - i + j];
M[i] = new_s;
}
sort(M.begin(), M.end());
//得到BWT(S)和编码
string L(len, ' ');
for(int i = 0; i < M.size(); i++) {
SA[i] = len - M[i].size();
if(SA[i] == 0)
L[i] = '$';
else {
L[i] = S[SA[i] - 1];
count_num[0][i] = num_a;
count_num[1][i] = num_c;
count_num[2][i] = num_g;
count_num[3][i] = num_t;
if