什么叫后綴數組 首先要知道什么叫后綴 ?
比如 字符串 abcdef 那么 abcdef bcdef cdef def ef f 就叫做后綴 也就是從最后一個字母之前的一個字母開始一直到最后一個字母(所以所 bcd不是后綴 因為沒有到最后一位f) 所構成的字符串就叫做后綴
至於后綴數組能干什么?我在這就不介紹了 這不是本文的重點!本文主要講解后綴數組應該怎么寫代碼!
寫本文的原因
但是自己之前讀過很多后綴數組的文章 短短二三十代碼 卻沒有找到一篇博客從頭到尾講解的
可能是因為我沒有搜索到
自己斷斷續續一個月終於算是對倍增算法(就是一個名字 不必糾結什么叫倍增算法)有個比較深入理解
這是原始代碼
int wa[maxn],wb[maxn],wv[maxn],ws[maxn]; int cmp(int *r , int a, int b, int l) { return r[a] == r[b] && r[a+l] == r[b+l]; } void da (int *r , int *sa , int n, int m) { int i, j, p, *x = wa, *y = wb , *t; for(i = 0; i < m; i++) ws[i] = 0; for(i = 0; i < n; i++) ws[x[i] = r[i]]++; for(i = 1; i < m; i++) ws[i] += ws[i-1]; for(i = n-1; i >= 0; i--) sa[--ws[x[i]]] = i; for(j = 1,p = 1; p < n ; j <<= 1,m = p) { for(p = 0, i = n - j; i < n; i++) y[p++]=i; for(i = 0; i < n; i++) if(sa[i] >= j) y[p++] = sa[i] - j; for(i = 0; i < n; i++) wv[i] = x[y[i]]; for(i = 0; i < m; i++) ws[i] = 0; for(i = 0; i < n; i++) ws[wv[i]]++; for(i = 1; i < m; i++) ws[i] += ws[i-1]; for(i = n-1; i >= 0; i--) sa[--ws[wv[i]]] = y[i]; for(t = x,x = y,y = t,p = 1,x[sa[0]] = 0,i = 1; i < n;i++) x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++; } }
要想了解上面的代碼 首先你要知道什么叫基數排序(基數排序 百度百科)
假設你也已經了解了了基數排序 那么下面我們就要解析上面的代碼
還有在這里你首先要知道
什么是兩個概念
后綴數組(SA[i]存放排名第i大的后綴首字符下標) 下面引號內內容可以不看
后綴數組 SA 是一個一維數組,它保存1..n 的某個排列 SA[1] ,SA[2] , ……, SA[n] ,並且保證Suffix(SA[i])<Suffix(SA[i+1]), 1 ≤ i<n 。也就是將 S 的 n 個后綴從小到大進行排序之后把排好序的后綴的開頭位置順次放入SA 中。
名次數組(rank[i]存放各個后綴的優先級)
名次數組 Rank[i] 保存的是 以下標 i 開頭的后綴在所有后綴中從小到大排列的 “ 名次 ” 。
最后總結為 SA[i] = j表示為按照從小到大排名為i的后綴 是以j(下標)開頭的后綴
rank[i] = j 表示為按照從小到大排名 以i為下標開始的后綴 排名為j
RANK表示你排第幾 SA表示排第幾的是誰
(記住這個就行)
下面的這張圖就是上面算法的思想 但是我當時看的時候 暈了
下面我們一步步來
首先 不管什么算法 我們來點暴力的 假設現在 我們直接來求一個字符串所有后綴的大小(所謂后綴的大小就是比較字符串的大小 這個一定要知道) 你會怎么做 ?
想到了!
兩個for循環比較唄 但是這樣算法肯定慢
int smpStr(char* str,int len){ int k=0; for(int i=0;i<len;i++){ for(int j=i;j<len;j++){ if(strcmp(str+k,str+j)>0){ k = j; } } rank[k] = i; } }
考慮到后綴數組的特殊性 我們換一種比較方式
為什么稱它特殊 因為一個字符串所有的后綴之間是有關系的
比如以字符串 abcdef 來舉例
后綴bcdef 與 cdef 就有比較強的關系 后一個是前一個的組成部分 准確來說是后半組成部分
那么怎么利用他們這種關系 請繼續看
一首先考慮到比較方便我們把所有的字母都減去 a-1 這里我只考慮所有字母都是小寫字母的方式
加入字符串是 aabaaaab
下面將相鄰倆個數合並為一個整數
這樣下面使用基數排序對這個合並后的整數進行排序 為什么使用基數排序 因為它的位數固定 也許你會問那
字母 ‘z’ 減去‘a’- 1 不是大於10了嗎 那不是3位數了嗎 不是這樣的 把 z 減去‘a’- 1 =26 看做是一個數 而不是二十六
將相當於16進制 一樣15不是看做兩位數 而是用F來表示 當然你高興 完全可以把26寫作Z以后 Z就是26
下面我講解一下 這個很重要 為什么要兩兩合並為一個數
首先求所有后綴數組最后組成為下圖
那么每一個后綴之間都是有重復的 第1個后綴的前兩個就是第0個后綴的第一到第三個字母
那么一次類推 也就是說我按下圖分為兩兩一組
將上圖的兩兩一組一個整數按照基數排序的結果為
解釋一下 第一個11 排第一名 第二個12 排第二名
那么你有沒有發現第0個后綴到第7個后綴的前兩個字母的比較已經出來了 因為第一個11 就是第1個后綴的前兩個字母 第二個12 就是第2個后綴的前兩個字母
什么意思 看圖
好了 現在我們已經比較所有后綴的前兩個字母 下面我開始比較后面 那么我怎么比較前兩個字母后面的字符串呢 因為剛才我已經把所有的兩兩字母的大小已經比較出來了 我現在可以利用下面的結果再比較 看圖 其中合並后的 1121 就是第一個后綴的前四個字母 1211 就是第二個后綴的前四個字母
下面開始再次拼接 如圖 最后這號拼成八位數 也就是正好字符串的長度 這時候可以使用基數排序來比較 但是假如字符串10000個呢 那么有10000個后綴 每個后綴的長度是10000 意味着最后拼接成的數也是有10000位 10000*10000我們需要開辟這么大數據這是不可行的 那么我們能不能將每次拼接的大數縮小呢?
先看圖
首先后綴數組最終要獲得的是后綴的排名 那么到底是1112 還是 11 是1221 還是24 無所謂
我只要把他們保持合適的大小 就比如說 小明考了100分 小紅考了89分 小剛考了55分
那么我現在把小剛設為0分 小紅設為1分 小明設為2分 那么對他們最后的排名有影響嗎 沒有
小明還是第一名 就是這個道理 這樣我們可以最大程度減小存儲的開銷
所以我只要每次對合並的數據進行按照從小到大排個序 用序號替換它 然后再次按照之前的步驟再次合並 再次排序替換 (什么時候結束)當全部的字符串都參與了比較就停止了
那么現在對1121 1211 2111 1111 1112 1120 1200 2000進行排序 分成兩組 前兩個字母一組后兩個字母一組 比如 1121這四個數字 11 與 21 兩份來基數排序
等等 你有沒有發現 我們上面的排序后的排名 跟第一關鍵字與第二關鍵字 有關系 也就是說
排名的大小就是第二關鍵字排名的 為什么 因為排序后的排名就是 第二關鍵字的排序結果
那么與第一關鍵字有什么關系 ? 有沒有發現 就是把第一關鍵字的11去掉 然后再加一個00
舉個生動的例子 現在有很多人在排隊 高矮不等 ,一開始是亂序的 現在保安要求 按從矮到高排列
排好序之后 大家都有了自己的位置 現在保安走開了 隊伍又回到一開始的狀態 並且原來站在最開始的人(亂序是的站在最開始的人)走了 來了一個小矮人 肯定是最矮的 保安回來 要求再次排隊 那么小矮人肯定站在最前面 下面保安喊道 上次排序排第一的人接上 如果走的那個人是第一 那么就繼續后面 如果不是上次排名第一的人就站上來 然后保安繼續叫 一直到上次排名最后的一個
上面這個故事就對應於下面的代碼
for(p = 0, i = n - j; i < n; i++)
y[p++]=i;
for(i = 0; i < n; i++)
if(sa[i] >= j)
y[p++] = sa[i] - j;