字符串相关处理kmp,前缀数,后缀树,后缀数组,最长回文串,最长重复字串,最长非重复字串
1. 最長回文串
一般用后綴數組或者后綴樹可以解決,
用此方法:http://blog.csdn.net/v_july_v/article/details/6897097
? ?找出S[0 - i].reverse() 與 S[i, n]的LCA
? ?找出S[0 - i].reverse() 與 S[i + 1, n]的LCA
從些個LCAs里面選出最大的1
)
1.1 是不是想到一種求s和s'最長公共子串(注意不是最長公共子序列)?
? ? ?這個方法在有時候不work,
S = “abacdfgdcaba”, S’ = “abacdgfdcaba”.
The longest common substring between S and S’ is “abacd”.明顯錯誤。
1.2
依次遍歷字符串S中可能的中心點,注意,中心點可能在字符,也可能在兩個字符之間
//從中心向兩端擴展string expandAroundCenter(string s,int c1,int c2){int l=c1;int r=c2;int n=s.length();while (l>=0 && r<=n-1 && s[l]==s[r]){l--;r++;}return s.substr(l+1,r-l-1);}//方法3:從中心向兩端擴展string IsPalindrome3(string str){int n=str.length();if (n==0){return "";}string longest=str.substr(0,1);for (int i=0;i<n-1;i++){string p1=expandAroundCenter(str,i,i);if (p1.length()>longest.length()){longest=p1;}string p2=expandAroundCenter(str,i,i+1);if (p2.length()>longest.length()){longest=p2;}}return longest;}
1.3 動態規劃(時間復雜度也是n^2)
Define P[ i, j ] ← true?iff?the substring Si?… Sj?is a palindrome, otherwise false.
P[ i, j ] ← ( P[ i+1, j-1 ]?and?Si?= Sj?)
This yields a straight forward DP solution, which we first initialize the one and two letters palindromes, and work our way up finding all three letters palindromes, and so on…?
//方法2:動態規劃string IsPalindrome2(string str){if (str==""){return "";}int n=str.length();int maxIndex=0;int maxLength=1;bool IsPal[1000][1000]={false};for (int i=0;i<n;i++){IsPal[i][i]=true;}for (int i=0;i<n-1;i++){if (str[i]==str[i+1]){IsPal[i][i+1]=true;maxIndex=i;maxLength=2;}}for (int len=3;len<=n;len++){for (int i=0;i<=n-len;i++){int j=i+len-1;if (IsPal[i+1][j-1] && str[i]==str[j]){IsPal[i][j]=true;maxIndex=i;maxLength=len;}}}return str.substr(maxIndex,maxLength);}
1.4
但是有一種巧妙的方法,不構建后綴樹在o(n)完成
轉自:http://www.felix021.com/blog/read.php?2040
首先用一個非常巧妙的方式,將所有可能的奇數/偶數長度的回文子串都轉換成了奇數長度:在每個字符的兩邊都插入一個特殊的符號。比如 abba 變成 #a#b#b#a#, aba變成 #a#b#a#。 為了進一步減少編碼的復雜度,可以在字符串的開始加入另一個特殊字符,這樣就不用特殊處理越界問題,比如$#a#b#a#。
下面以字符串12212321為例,經過上一步,變成了 S[] = "$#1#2#2#1#2#3#2#1#";
然后用一個數組 P[i] 來記錄以字符S[i]為中心的最長回文子串向左/右擴張的長度(包括S[i]),比如S和P的對應關系:
P? 1? 2? 1? 2? 5? 2? 1? 4? 1? 2? 1? 6? 1? 2? 1? 2? 1
(p.s. 可以看出,P[i]-1正好是原字符串中回文串的總長度)
那么怎么計算P[i]呢?該算法增加兩個輔助變量(其實一個就夠了,兩個更清晰)id和mx,其中id表示最大回文子串中心的位置,mx則為id+P[id],也就是最大回文子串的邊界。
然后可以得到一個非常神奇的結論,這個算法的關鍵點就在這里了:如果mx > i,那么P[i] >= MIN(P[2 * id - i], mx - i)。就是這個串卡了我非常久。實際上如果把它寫得復雜一點,理解起來會簡單很多: //記j = 2 * id - i,也就是說 j 是 i 關于 id 的對稱點。
if (mx - i > P[j])
? ? P[i] = P[j];
else /* P[j] >= mx - i */
? ? P[i] = mx - i; // P[i] >= mx - i,取最小值,之后再匹配更新。
當然光看代碼還是不夠清晰,還是借助圖來理解比較容易。
當 mx - i > P[j] 的時候,以S[j]為中心的回文子串包含在以S[id]為中心的回文子串中,由于 i 和 j 對稱,以S[i]為中心的回文子串必然包含在以S[id]為中心的回文子串中,所以必有 P[i] = P[j],見下圖。
當 P[j] > mx - i 的時候,以S[j]為中心的回文子串不完全包含于以S[id]為中心的回文子串中,但是基于對稱性可知,下圖中兩個綠框所包圍的部分是相同的,也就是說以S[i]為中心的回文子串,其向右至少會擴張到mx的位置,也就是說 P[i] >= mx - i。至于mx之后的部分是否對稱,就只能老老實實去匹配了。
對于 mx <= i 的情況,無法對 P[i]做更多的假設,只能P[i] = 1,然后再去匹配了。
于是代碼如下: //輸入,并處理得到字符串s
int p[1000], mx = 0, id = 0;
memset(p, 0, sizeof(p));
for (i = 1; s[i] != '\0'; i++) {
? ? p[i] = mx > i ? min(p[2*id-i], mx-i) : 1;
? ? while (s[i + p[i]] == s[i - p[i]]) p[i]++;
? ? if (i + p[i] > mx) {
? ? ? ? mx = i + p[i];
? ? ? ? id = i;
? ? }
}
//找出p[i]中最大的 //主要原理就是先通過對稱性獲得以i為中心點的半徑;然后再向左向右擴展,試圖增大半徑
int Palindrome(char *str) {int len = strlen(str);char *Str = new char[len+len+3];memset(Str,0,len+len+3);//Str[0] = '$';int Len = len+len+2;int *scale = new int[Len];memset(scale,0,sizeof(int)*Len);int j=0;for (int i=1;i<Len;++i){if(i%2==0)Str[i] = str[j++];elseStr[i] = '#';}Str[Len] = '\0';int id = 0;//在i之前的回文段,這個回文段的中心點,這個回文段的右邊的邊界最大int mx = 0;//在i之前的回文段,這個回文段的右邊的邊界最大for (int i=1;i<Len;++i){if (mx >i)//如果之前的回文邊界超過i,要找到i關于回文中心點id的對稱點j{int j = 2*id - i;int rightSpan = mx - i;if(scale[j]>rightSpan)scale[i] = rightSpan;elsescale[i] = scale[j];}else scale[i] = 1;int right = i + scale[i] +1;int left = i - scale[i] -1;while ( (right<Len) && (Str[right]==Str[left]) ){++scale[i];++right;--left;}if (i+scale[i]>mx){mx = scale[i];id = i;}}int maxValue = 0;//cout<<"Len "<<Len<<endl;for (int i=0;i<Len;++i){//cout<<i<<endl;if(scale[i]>maxValue)maxValue = scale[i];}//cout<<"out"<<endl;delete []Str;delete []scale;return maxValue ;}
2. july:http://blog.csdn.net/v_july_v/article/details/6897097提到后綴樹還可以解決一下幾個問題:
??方案:用S構造后綴樹,按在trie中搜索字串的方法搜索o即可。?
??原理:若o在S中,則o必然是S的某個后綴的前綴。?
例如S: leconte,查找o: con是否在S中,則o(con)必然是S(leconte)的后綴之一conte的前綴.有了這個前提,采用trie搜索的方法就不難理解了。。?
??方案:用S+’$'構造后綴樹,搜索T節點下的葉節點數目即為重復次數?
??原理:如果T在S中重復了兩次,則S應有兩個后綴以T為前綴,重復次數就自然統計出來了。。?
??方案:原理同2,具體做法就是找到最深的非葉節點。?
??這個深是指從root所經歷過的字符個數,最深非葉節點所經歷的字符串起來就是最長重復子串。?
為什么要非葉節點呢?因為既然是要重復,當然葉節點個數要>=2。 (此方法應該只能找到可重疊的最長重復字串,譬如abcbcbd,可重疊最長重復字串是bcb,可以看出兩個字串bcb在b上重疊了;其不重疊最長重復字串是bc和cb,用后綴樹暫時想不出什么方法)
??方案:將S1#S2$作為字符串壓入后綴樹,找到最深的非葉節點,且該節點的葉節點(這里是指該節點的所有葉子節點)既有#也有$。 (譬如s1=abcd, s2 = ebce,連接組合成abcd#ebce$, ? ??
第四題可以看做是longest common substring,可以就是說要求最長公共字串必須是連續的,用后綴樹或者后綴數組的時間復雜度是o(n+m)
當然還可以用動態規劃實現,o(n*m):(為了避免邊界檢查,f[][]都是從1開始計算的,但是f[i][j],表示的是a[i-1]b[j-1])
char a[]="acbedf";char b[]="ecbedfacabe";int f[50][50];memset(f,0,sizeof(f));int maxL=0,ai=0;for (int i=1;i<=sizeof(a);++i){for(int j=1;j<=sizeof(b);++j){if(a[i-1]==b[j-1]){f[i][j] =f[i-1][j-1] +1;if (f[i][j]>maxL){maxL = f[i][j];ai = i-1;}}}}char c[50];memset(c,0,sizeof(c));strncpy(c,a+ai-maxL+1,maxL);cout<<maxL<<endl<<c<<endl;以上代碼主要參考:http://sagittarix.blogspot.com/2008/11/blog-post_06.html
| \ | e | d | c | e | d | n |
| n | 0 | 0 | 0 | 0 | 0 | 1 |
| c | 0 | 0 | 1 | 0 | 0 | 0 |
| e | 1 | 0 | 0 | 1 | 0 | 0 |
| d | 0 | 1 | 0 | 0 | 1 | 0 |
| t | 0 | 0 | 0 | 0 | 0 | 0 |
我們把兩個字符串看作矩陣的x-y軸:首先遍歷字符串時,把兩個字符相同的位置標記成1,不相同的地方標記成0。很容易證明,如果能形成公共子序列,則最長子序列一定是從某個位置開始的對角線的長度,所以我們需要做的就是統計對角線的長度。
例如str1=”ncedt”與str2=”edcedn”生成的矩陣如左矩陣,遍歷找到最長的對角線即可。
| e | d | c | e | d | n | |
| n | 0 | 0 | 0 | 0 | 0 | 1 |
| c | 0 | 0 | 1 | 0 | 0 | 0 |
| e | 1 | 0 | 0 | 2 | 0 | 0 |
| d | 0 | 2 | 0 | 0 | 3 | 0 |
| t | 0 | 0 | 0 | 0 | 0 | 0 |
其實這里還可以優化,即在生成表格的時候,加上對角線上比它前面的元素,這樣一遍遍歷就可以生成左圖所示矩陣,中間記下最大值便可,省下最后一趟比較,降低時間復雜度;空間復雜度也可降低,其實只要一維的數組便可以了,因為每次更新只用到上面一行(或左邊一列,看你程序怎么寫了,都可以)。
這個直觀上很好理解,但拆分到子問題的思想表達起來比較繞,就轉一下別人的文字:
---------------------------------------------------------------------------------------------------------------------
定義f(m, n)為串Xm和Yn之間最長的子字符串的長度并且該子字符串結束于Xm?和?Yn。因為要求是連續的,所以定義f的時候多了一個要求,即字符串結束于Xm?和?Yn。
于是有f(m, 0) = f(0, m) = 0
如果xm != yn,?則f(m, n) = 0
如果xm = yn,?則f(m, n) = f(m-1, n-1) + 1
因為最長字符串不一定結束于Xm?和?Yn末尾,所以這里必須求得所有可能的f(p, q) | 0 <>最大的f(p, q)就是解。依照公式用Bottom-up DP可解。
代碼就不寫了,簡單。但是它與其它的DP填表又有所不同,不是直接得出最大的填到表格最后一格中,而是找出格子中的最大值,其中差別,細細體會
還有一個問題是 longest common subsquence ,要求最長公共序列不一定是連續的,但是有序的,一般只能用動態規劃實現,o(n*m)
算法導論講的很透徹,這個博客也不錯:http://blog.csdn.net/orbit/article/details/6717125
?現在來分析一下本問題的最優子結構。首先定義問題,假設有字符串str1長度為m,字符串str2長度為n,可以把子問題描述為:求字符串str1<1..m>中從第1個到第i(i <= m)個字符組成的子串str1<1…i>和字符串str2<1..n>中從第1個到第j(j <= n)個字符組成的子串str2<1…j>的最長公共序列,子問題的最長公共序列可以描述為d[i,j] = { Z1,Z2, … Zk },其中Z1-Zk為當前子問題已經匹配到的最長公共子序列的字符。子問題定義以后,還要找出子問題的最優序列d[i,j]的遞推關系。分析d[i,j]的遞推關系要從str1[i]和str2[j]的關系入手,如果str1[i]和str2[j]相同,則d[i,j]就是d[i-1,j-1]的最長公共序列+1,Zk=str1[i]=str2[j];如果str1[i]和str2[j]不相同,則d[i,j]就是d[i-1,j]的最長公共序列和d[i,j-1]的最長公共序列中較大的那一個。
??????? 最后是確定d[i,j]的邊界值,當字符串str1為空或字符串str2為空時,其最長公共子串應該是0,也就是說當i=0或j=0時,d[i,j]就是0。d[i,j]的完整遞推關系如下:
d[i,j]表示,對于序列str1[0,i]和str2[0,j]的lcs
構建一顆后綴樹的編碼難度較大,一般解題應該用后綴數組:http://dongxicheng.org/structure/suffix-array/
有時間再研究下后綴數組吧。。。
總結
以上是生活随笔為你收集整理的字符串相关处理kmp,前缀数,后缀树,后缀数组,最长回文串,最长重复字串,最长非重复字串的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据库B-树B+树
- 下一篇: IO - 同步,异步,阻塞,非阻塞