最長公共子串

2021-08-01 01:49:20 字數 3408 閱讀 3692

最長公共子串問題的基本表述為:

給定兩個字串,求出它們之間最長的相同子字串的長度。

最直接的解法自然是找出兩個字串的所有子字串進行比較看他們是否相同,然後取得相同最長的那個。對於乙個長度為n的字串,它有n(n+1)/2 個非空子串。所以假如兩個字串的長度同為n,通過比較各個子串其演算法複雜度大致為o(n4)。這還沒有考慮字串比較所需的時間。簡單想想其實並不需要取出所有的子串,而只要考慮每個子串的開始位置就可以,這樣可以把複雜度減到o(n3)。

但這個問題最好的解決辦法是動態規劃法,在後邊會更加詳細介紹這個問題使用動態規劃法的契機:有重疊的子問題。進而可以通過空間換時間,讓複雜度優化到o(n2),代價是空間複雜度從o(1)一下子提到了o(n2)。

從時間複雜度的角度講,對於最長公共子串問題,o(n2)已經是目前我所知最優的了,也是面試時所期望達到的。但是對於空間複雜度o(n2)並不算什麼,畢竟演算法上時間比空間更重要,但是如果可以省下一些空間那這個演算法就會變得更加美好。所以進一步的可以把空間複雜度減少到o(n),這是相當美好了。但有一天無意間讓我發現了乙個演算法可以讓該問題的空間複雜度減少回原來的o(1),而時間上如果幸運還可以等於o(n)。

1、暴力解法 – 所得即所求

對於該問題,直觀的思路就是問題要求什麼就找出什麼。要子串,就找子串;要相同,就比較每個字元;要最長就記錄最長。所以很容易就可以想到如下的解法。

int longestcommonsubstring_n3(const string& str1, const string& str2)

if (longest < length)}}

#ifdef ider_debug

cout<< "(first, second, comparisions) = ("

<< start1 << ", " << start2 << ", " << comparisons

<< ")" << endl;

#endif

return longest;

}

該解法的思路就如前所說,以字串中的每個字元作為子串的端點,判定以此為開始的子串的相同字元最長能達到的長度。

其實從表層上想,這個演算法的複雜度應該只有o(n2)因為該演算法把每個字元都成對相互比較一遍,但關鍵問題在於比較兩個字串的效率並非是o(1),這也導致了實際的時間複雜度應該是滿足ω(n2)和o(n3)。

2、動態規劃法 – 空間換時間

有了乙個解決問題的方法是一件很不錯的事情了,但是拿著上邊的解法回答面試題肯定不會得到許可,面試官還是會問有沒有更好的解法呢?不過上述解法雖然不是最優的,但是依然可以從中找到乙個改進的線索。不難發現在子串比較中有很多次重複的比較。

比如再比較以i和j分別為起始點字串時,有可能會進行i+1和j+1以及i+2和j+2位置的字元的比較;而在比較i+1和j+1分別為起始點字串時,這些字元又會被比較一次了。也就是說該問題有非常相似的子問題,而子問題之間又有重疊,這就給動態規劃法的應該提供了契機。

暴力解法是從字串開端開始找尋,現在換個思維考慮以字元結尾的子串來利用動態規劃法。

假設兩個字串分別為s和t,s[i]和t[j]分別表示其第i和第j個字元(字元順序從0開始),再令l[i, j]表示以s[i]和s[j]為結尾的相同子串的最大長度。應該不難遞推出l[i, j]和l[i+1,j+1]之間的關係,因為兩者其實只差s[i+1]和t[j+1]這一對字元。若s[i+1]和t[j+1]不同,那麼l[i+1, j+1]自然應該是0,因為任何以它們為結尾的子串都不可能完全相同;而如果s[i+1]和t[j+1]相同,那麼就只要在以s[i]和t[j]結尾的最長相同子串之後分別添上這兩個字元即可,這樣就可以讓長度增加一位。合併上述兩種情況,也就得到l[i+1,j+1]=(s[i]==t[j]? l[i,j]+1:0)這樣的關係。

最後就是要小心的就是臨界位置:如若兩個字串中任何乙個是空串,那麼最長公共子串的長度只能是0;當i為0時,l[0,j]應該是等於l[-1,j-1]再加上s[0]和t[j]提供的值,但l[-1,j-1]本是無效,但可以視s[-1]是空字元也就變成了前面一種臨界情況,這樣就可知l[-1,j-1]==0,所以l[0,j]=(s[0]==t[j]?1:0)。對於j為0也是一樣的,同樣可得l[i,0]=(s[i]==t[0]?1:0)。

最後的演算法**如下:

int longestcommonsubstring_n2_n2(const string& str1, const string& str2) 

for (int i = 1; i < size1; ++i)}}

for (int i = 0; i < size1; ++i)}}

#ifdef ider_debug

cout<< "(first, second, comparisions) = ("

<< start1 << ", " << start2 << ", " << comparisons

<< ")" << endl;

#endif

return longest;

}

演算法開闢了乙個矩陣記憶體來儲存值來保留計算值,從而避免了重複計算,於是運算的時間複雜度也就降至了o(n2)。

3、動態規劃法優化 – 能省一點是一點

仔細回顧之前的**,其實可以做一些合併讓**變得更加簡潔,比如最後乙個求最長的巢狀for迴圈其實可以合併到之前計算整個表的for迴圈之中,每計算完l[i,j]就檢查它是的值是不是更長。當合併**之後,就會發現內部迴圈的過程重其實只用到了整個表的相鄰兩行而已,對於其它已經計算好的行之後就再也不會用到,而未計算的行曽之前也不會用到,因此考慮只用兩行來儲存計算值可能就足夠。

於是新的經過再次優化的演算法就有了:

int longestcommonsubstring_n2_2n(const string& str1, const string& str2)}}

for (int i = 1; i < size1; ++i)

}for (int j = 1; j < size2; ++j)

}else }}

#ifdef ider_debug

cout<< "(first, second, comparisions) = ("

<< start1 << ", " << start2 << ", " << comparisons

<< ")" << endl;

#endif

return longest;

}

跟之前的動態規劃演算法**相比,兩種解法並沒有實質的區別,完全相同的巢狀for迴圈,只是將檢查最長的**也併入其中,然後table中所擁有的行也只剩下2個。

此解法的一些技巧在於如何交換兩個行陣列作為工作陣列。可以交換陣列中的每個元素,異或交換一對指標。上邊**中所用的方法類似於後者,根據奇偶性來決定那行陣列可以被覆蓋,哪行陣列有需要的快取資料。不管怎麼說,該演算法都讓空間複雜度從o(n2)減少到了o(n),相當有效。

最長公共子串行 最長公共子串

1 最長公共子串行 採用動態規劃的思想,用乙個陣列dp i j 記錄a字串中i 1位置到b字串中j 1位置的最長公共子串行,若a i 1 b j 1 那麼dp i j dp i 1 j 1 1,若不相同,那麼dp i j 就是dp i 1 j 和dp i j 1 中的較大者。class lcs el...

最長公共子串行 最長公共子串

1.區別 找兩個字串的最長公共子串,這個子串要求在原字串中是連續的。而最長公共子串行則並不要求連續。2 最長公共子串 其實這是乙個序貫決策問題,可以用動態規劃來求解。我們採用乙個二維矩陣來記錄中間的結果。這個二維矩陣怎麼構造呢?直接舉個例子吧 bab 和 caba 當然我們現在一眼就可以看出來最長公...

最長公共子串 最長公共子串行

子串要求連續 子串行不要求連續 之前的做法是dp求子序列 include include include using namespace std const int inf 0x3f3f3f3f const int mod 1000000007 string s1,s2 int dp 1010 10...