關於後綴數組的詳細介紹,可見另一篇轉載過來的介紹:
http://blog.csdn.net/slime_kirito/article/details/49050043
本文主要是對於實際問題的代碼實現
後綴數組是解決部分字符串問題的利器。
主要是運用height數組,Suff數組(SA數組)
對於求以上數組的模板:
#include<iostream>
#include<string>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 100;
int height[MAXN];
string Suff[MAXN];
bool cmp(string a, string b)//比較函數
{
return a < b;
}
//最長公共前綴
int Comlen_Suff(string s1, string s2)
{
int p = 0, q = 0;
int len = 0;
while (p != s1.length() && q != s2.length() && s1[p++] == s2[q++])
{
++len;
}
return len;
}
void Make(string s)
{
int len = s.length();
memset(height, 0, sizeof(height));
for (int i = 0; i < len; i++)
{
Suff[i] = &s[i];
}
sort(Suff, Suff + len, cmp);
/*for (int i = 0; i < len; i++)
{
cout << Suff[i] << endl;
}*/
for (int i = 0; i < len - 1; i++)
{
height[i] = Comlen_Suff(Suff[i], Suff[i + 1]);
}
/*for (int i = 0; i < len - 1; i++)
{
cout << height[i] << endl;
}*/
}
後綴數組應用
①
單個字符串相關問題
1 可重疊最長重複子串(LRS)
給定一個字符串,求最長重複子串,這兩個子串可以重疊。
『解析』只需要求height 數組裏的最大值即可。
#include<iostream>
#include<string>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 100;
bool cmp(string a, string b)//比較函數
{
return a < b;
}
//最長公共前綴
int Comlen_Suff(string s1, string s2)
{
int p = 0, q = 0;
int len = 0;
while (p != s1.length() && q != s2.length() && s1[p++] == s2[q++])
{
++len;
}
return len;
}
void LRS(string s)
{
string Suff[MAXN];
int len = s.length(),maxlen = 0,maxi=0;
for (int i = 0; i < len; i++)
{
Suff[i] = &s[i];
}
sort(Suff, Suff + len, cmp);
for (int i = 0; i < len - 1; i++)
{
int len = Comlen_Suff(Suff[i], Suff[i + 1]);
if (len > maxlen)
{
maxlen = len;
maxi = i;
}
}
cout << "最長重複子串的長度爲:" << maxlen << endl;;
cout << "該串爲:" << Suff[maxi] << endl;
}
2 不可重疊最長重複子串
給定一個字符串,求最長重複子串,這兩個子串不能重疊。
對這個字符串構造後綴數組,在每個後綴數組中,尋找沒有重複字符的最長前綴,就是要找的子串。
#include<iostream>
#include<string>
#include<cstring>
using namespace std;
const int MAXN = 10000;
//得到字符串最長的無重複的前綴長度
int longestlen(char * p)
{
int vis[MAXN];//MAXN要多開一些空間
int len = 0;
memset(vis, 0, sizeof(vis));
while (*p && !vis[*p])
{
vis[*p] = 1;
++len;
++p;
}
return len;
}
//使用後綴數組解法
void longest_unique_substring(char * str)
{
int maxlen = -1, maxi = 0;
char *Suff[MAXN];//MAXN要多開一些空間
int n = 0;
while (*str != '\0')
{
Suff[n++] = str++;
}
for (int i = 0; i<n; i++)
{
int len = longestlen(Suff[i]);
if (len > maxlen)
{
maxlen = len;
maxi = i;
}
}
printf("最長的無重複的前綴長度爲%d:\n", maxlen);
printf("該串爲:");
printf("%.*s\n", maxlen, Suff[maxi]);
//%.*s:從maxi開始連續manlen個字符
}
3 可重疊的k 次最長重複子串
給定一個字符串,求至少出現k 次的最長重複子串,這k 個子串可以重疊。
『解析』 先二分答案,然後將後綴分成若干組。不同的是,這裏要判斷的是有沒有一個組的後綴個數不小於k。如果有,那麼存在k 個相同的子串滿足條件,否則不存在。這個做法的時間複雜度爲O(nlogn)。
(未)
4 後綴數組求最長迴文子串(LPS)
給定一個字符串,求最長迴文子串。
『解析』 將整個字符串反過來寫在原字符串後面,中間用一個特殊的字符隔開。
這樣就把問題變爲了求這個新的字符串的某兩個後綴的最長公共前綴。
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int MAXN = 1000;
//用於sort的比較函數
bool cmp(string a, string b)
{
return a < b;
}
//最長公共前綴
int comlen_suff(string s1, string s2)
{
int p = 0, q = 0;
int len = 0;
int count = 0; //保證兩個子串中只有一個含有‘#’,LCS纔來自兩個字符串,否則可能來自同一個字符串
while (p != s1.length() && q != s2.length() && s1[p++] == s2[q++])
{
++len;
if (s1[p] == '#' || s2[q] == '#')
{
break;
}
}
while (p != s1.length())
{
if (s1[p++] == '#')
{
++count;
break;
}
}
while (q != s2.length())
{
if (s2[q++] == '#')
{
++count;
break;
}
}
if (count == 1)
return len;
return 0;
}
//最長公共子串
void LCS(string s)
{
int maxlen = 0;
int suf_index;
int len_suff = s.length()*2 + 1;
string suff[MAXN], arr; // 將X和Y連接到一起
arr += s;
arr += '#';
for (int i = s.length() - 1; i >= 0; i--)
{
arr += s[i];
}
arr += '\0';
for (int i = 0; i < len_suff; ++i) // 初始化後綴數組
{
suff[i] = &arr[i];
}
sort(suff, suff + len_suff, cmp);
for (int i = 0; i < len_suff - 1; ++i)
{
int len = comlen_suff(suff[i], suff[i + 1]);
if (len > maxlen)
{
maxlen = len;
suf_index = i;
}
}
cout << "LCS長度爲:" << maxlen << endl;
cout << "該串爲:" << suff[suf_index] << endl;
}
5 連續重複子串
給定一個字符串L,已知這個字符串是由某個字符串S 重複R 次而得到的,求R 的最大值。
『解析』窮舉字符串S 的長度k,然後判斷是否滿足。判斷的時候,先看字符串L 的長度能否被k 整除,再看suffix(1)和suffix(k+1)的最長公共前綴是否等於n-k。在詢問最長公共前綴的時候,suffix(1)是固定的,所以RMQ問題沒有必要做所有的預處理, 只需求出height 數組中的每一個數到height[rank[1]]之間的最小值即可。整個做法的時間複雜度爲O(n)。
(未)
6 重複次數最多的連續重複子串
給定一個字符串,求重複次數最多的連續重複子串。
『解析』先窮舉長度L,然後求長度爲L 的子串最多能連續出現幾次。首先連續出現1 次是肯定可以的,所以這裏只考慮至少2 次的情況。假設在原字符串中連續出現2 次,記這個子字符串爲S,那麼S 肯定包括了字符r[0], r[L], r[L*2],r[L*3], ……中的某相鄰的兩個。所以只須看字符r[L*i]和r[L*(i+1)]往前和往後各能匹配到多遠,記這個總長度爲K,那麼這裏連續出現了K/L+1 次。最後看最大值是多少。
窮舉長度L 的時間是n,每次計算的時間是n/L。所以整個做法的時間複雜度是O(n/1+n/2+n/3+……+n/n)=O(nlogn)。
(未)
② 兩個字符串相關問題
1 後綴數組求最長公共子串(LCS)
解法:將兩個字符串用一個特殊符號(兩個字符串中都沒有,比如‘#’)連接起來,
構造連接後字符串的後綴數組,求後綴數組中的最長公共前綴,要保證最長的公共前
綴在原來兩個字符串中都出現,而不是隻出現在一個字符串中,這就是特殊連接符號
的作用。
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
const int MAXN = 1000;
//用於sort的比較函數
bool cmp(string a, string b)
{
return a < b;
}
//最長公共前綴
int comlen_suff(string s1,string s2)
{
int p=0, q=0;
int len = 0;
int count = 0; //保證兩個子串中只有一個含有‘#’,LCS纔來自兩個字符串,否則可能來自同一個字符串
while (p!=s1.length() && q!=s2.length() && s1[p++] == s2[q++])
{
++len;
if (s1[p] == '#' || s2[q] == '#')
{
break;
}
}
while (p!=s1.length())
{
if (s1[p++] == '#')
{
++count;
break;
}
}
while (q!=s2.length())
{
if (s2[q++] == '#')
{
++count;
break;
}
}
if (count == 1)
return len;
return 0;
}
//最長公共子串
void LCS(string s1,string s2)
{
int maxlen = 0;
int suf_index;
int len_suff = s1.length()+s2.length() + 1;
string suff[MAXN], arr; // 將X和Y連接到一起
arr += s1;
arr += '#';
arr += s2;
arr += '\0';
for (int i = 0; i < len_suff; ++i) // 初始化後綴數組
{
suff[i] = &arr[i];
}
sort(suff, suff+len_suff, cmp);
for (int i = 0; i < len_suff - 1; ++i)
{
int len = comlen_suff(suff[i], suff[i + 1]);
if (len > maxlen)
{
maxlen = len;
suf_index = i;
}
}
cout << "LCS長度爲:" << maxlen << endl;
cout << "該串爲:" << suff[suf_index]<<endl;
}
2 長度不小於k 的公共子串的個數
給定兩個字符串A 和B,求長度不小於k 的公共子串的個數(可以相同)。
『解析』基本思路是計算A 的所有後綴和B 的所有後綴之間的最長公共前綴的長度,把最長公共前綴長度不小於k 的部分全部加起來。先將兩個字符串連起來,中間用一個沒有出現過的字符隔開。按height 值分組後,接下來的工作便是快速的統計每組中後綴之間的最長公共前綴之和。掃描一遍,每遇到一個B 的後綴就統計與前面的A 的後綴能產生多少個長度不小於k 的公共子串,這裏A 的後綴需要用一個單調的棧來高效的維護。然後對A 也這樣做一次。
(未)
③ 多個字符串相關問題
1 不小於k 個字符串中的最長子串
給定n 個字符串,求出現在不小於k 個字符串中的最長子串。
『解析』將n 個字符串連起來,中間用不相同的且沒有出現在字符串中的字符隔開,求後綴數組。然後二分答案:將後綴分成若干組,判斷每組的後綴是否出現在不小於k 個的原串中。這個做法的時間複雜度爲O(nlogn)。
(未)
2 每個字符串至少出現兩次且不重疊的最長子串
給定n 個字符串,求在每個字符串中至少出現兩次且不重疊的最長子串。
『解析』做法和上題大同小異,也是先將n 個字符串連起來,中間用不相同的且沒有出現在字符串中的字符隔開,求後綴數組。然後二分答案,再將後綴分組。判斷的時候,要看是否有一組後綴在每個原來的字符串中至少出現兩次,並且在每個原來的字符串中,後綴的起始位置的最大值與最小值之差是否不小於當前答案(判斷能否做到不重疊,如果題目中沒有不重疊的要求,那麼不用做此判斷)。這個做法的時間複雜度爲O(nlogn)。
(未)
3 出現或反轉後出現在每個字符串中的最長子串
給定n 個字符串,求出現或反轉後出現在每個字符串中的最長子串。
『解析』這題不同的地方在於要判斷是否在反轉後的字符串中出現。其實這並沒有加大題目的難度。只需要先將每個字符串都反過來寫一遍,中間用一個互不相同的且沒有出現在字符串中的字符隔開,再將n 個字符串全部連起來,中間也是用一個互不相同的且沒有出現在字符串中的字符隔開,求後綴數組。然後二分答案,再將後綴分組。判斷的時候,要看是否有一組後綴在每個原來的字符串或反轉後的字符串中出現。這個做法的時間複雜度爲O(nlogn)。
(未)
總結
後綴數組實際上可以看作後綴樹的所有葉結點按照從左到右的次序排列放入數組中形成的,所以後綴數組的用途不可能超出後綴樹的範圍。甚至可以說,如果不配合【最長公共前綴】函數,後綴數組的應用範圍是很狹窄的。但是有【最長公共前綴】 函數配合下的後綴數組就非常強大,可以完成大多數後綴樹所能完成的任務,因爲這個【最長公共前綴】函數實際上給出了任意兩個葉子結點的最近公共祖先,這方面的內容大家可以自行研究。