程序员面试题精选100题(20)-最长公共子串[算法]
例如:輸入兩個(gè)字符串BDCABA和ABCBDAB,字符串BCBA和BDAB都是是它們的最長公共子串,則輸出它們的長度4,并打印任意一個(gè)子串。
分析:求最長公共子串(Longest Common Subsequence, LCS)是一道非常經(jīng)典的動(dòng)態(tài)規(guī)劃題,因此一些重視算法的公司像MicroStrategy都把它當(dāng)作面試題。
完整介紹動(dòng)態(tài)規(guī)劃將需要很長的篇幅,因此我不打算在此全面討論動(dòng)態(tài)規(guī)劃相關(guān)的概念,只集中對LCS直接相關(guān)內(nèi)容作討論。如果對動(dòng)態(tài)規(guī)劃不是很熟悉,請參考相關(guān)算法書比如算法討論。
先介紹LCS問題的性質(zhì):記Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}為兩個(gè)字符串,而Zk={z0,z1,…zk-1}是它們的LCS,則:
1.?????? 如果xm-1=yn-1,那么zk-1=xm-1=yn-1,并且Zk-1是Xm-1和Yn-1的LCS;
2.?????? 如果xm-1≠yn-1,那么當(dāng)zk-1≠xm-1時(shí)Z是Xm-1和Y的LCS;
3.?????? 如果xm-1≠yn-1,那么當(dāng)zk-1≠yn-1時(shí)Z是Yn-1和X的LCS;
下面簡單證明一下這些性質(zhì):
1.?????? 如果zk-1≠xm-1,那么我們可以把xm-1(yn-1)加到Z中得到Z’,這樣就得到X和Y的一個(gè)長度為k+1的公共子串Z’。這就與長度為k的Z是X和Y的LCS相矛盾了。因此一定有zk-1=xm-1=yn-1。
既然zk-1=xm-1=yn-1,那如果我們刪除zk-1(xm-1、yn-1)得到的Zk-1,Xm-1和Yn-1,顯然Zk-1是Xm-1和Yn-1的一個(gè)公共子串,現(xiàn)在我們證明Zk-1是Xm-1和Yn-1的LCS。用反證法不難證明。假設(shè)有Xm-1和Yn-1有一個(gè)長度超過k-1的公共子串W,那么我們把加到W中得到W’,那W’就是X和Y的公共子串,并且長度超過k,這就和已知條件相矛盾了。
2.?????? 還是用反證法證明。假設(shè)Z不是Xm-1和Y的LCS,則存在一個(gè)長度超過k的W是Xm-1和Y的LCS,那W肯定也X和Y的公共子串,而已知條件中X和Y的公共子串的最大長度為k。矛盾。
3.?????? 證明同2。
有了上面的性質(zhì),我們可以得出如下的思路:求兩字符串Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}的LCS,如果xm-1=yn-1,那么只需求得Xm-1和Yn-1的LCS,并在其后添加xm-1(yn-1)即可;如果xm-1≠yn-1,我們分別求得Xm-1和Y的LCS和Yn-1和X的LCS,并且這兩個(gè)LCS中較長的一個(gè)為X和Y的LCS。
如果我們記字符串Xi和Yj的LCS的長度為c[i,j],我們可以遞歸地求c[i,j]:
????????? /????? 0?????????????????????????????? if i<0 or j<0
c[i,j]=????????? c[i-1,j-1]+1??????????????????? if i,j>=0 and xi=xj
???????? \?????? max(c[i,j-1],c[i-1,j]?????????? if i,j>=0 and xi≠xj
上面的公式用遞歸函數(shù)不難求得。但從前面求Fibonacci第n項(xiàng)(本面試題系列第16題)的分析中我們知道直接遞歸會(huì)有很多重復(fù)計(jì)算,我們用從底向上循環(huán)求解的思路效率更高。
為了能夠采用循環(huán)求解的思路,我們用一個(gè)矩陣(參考代碼中的LCS_length)保存下來當(dāng)前已經(jīng)計(jì)算好了的c[i,j],當(dāng)后面的計(jì)算需要這些數(shù)據(jù)時(shí)就可以直接從矩陣讀取。另外,求取c[i,j]可以從c[i-1,j-1] 、c[i,j-1]或者c[i-1,j]三個(gè)方向計(jì)算得到,相當(dāng)于在矩陣LCS_length中是從c[i-1,j-1],c[i,j-1]或者c[i-1,j]的某一個(gè)各自移動(dòng)到c[i,j],因此在矩陣中有三種不同的移動(dòng)方向:向左、向上和向左上方,其中只有向左上方移動(dòng)時(shí)才表明找到LCS中的一個(gè)字符。于是我們需要用另外一個(gè)矩陣(參考代碼中的LCS_direction)保存移動(dòng)的方向。
參考代碼如下:
#include "string.h"// directions of LCS generation enum decreaseDir {kInit = 0, kLeft, kUp, kLeftUp};/ // Get the length of two strings' LCSs, and print one of the LCSs // Input: pStr1 - the first string // pStr2 - the second string // Output: the length of two strings' LCSs / int LCS(char* pStr1, char* pStr2) {if(!pStr1 || !pStr2)return 0;size_t length1 = strlen(pStr1);size_t length2 = strlen(pStr2);if(!length1 || !length2)return 0;size_t i, j;// initiate the length matrixint **LCS_length;LCS_length = (int**)(new int[length1]);for(i = 0; i < length1; ++ i)LCS_length[i] = (int*)new int[length2];for(i = 0; i < length1; ++ i)for(j = 0; j < length2; ++ j)LCS_length[i][j] = 0;// initiate the direction matrixint **LCS_direction;LCS_direction = (int**)(new int[length1]);for( i = 0; i < length1; ++ i)LCS_direction[i] = (int*)new int[length2];for(i = 0; i < length1; ++ i)for(j = 0; j < length2; ++ j)LCS_direction[i][j] = kInit;for(i = 0; i < length1; ++ i){for(j = 0; j < length2; ++ j){if(i == 0 || j == 0){if(pStr1[i] == pStr2[j]){LCS_length[i][j] = 1;LCS_direction[i][j] = kLeftUp;}elseLCS_length[i][j] = 0;}// a char of LCS is found, // it comes from the left up entry in the direction matrixelse if(pStr1[i] == pStr2[j]){LCS_length[i][j] = LCS_length[i - 1][j - 1] + 1;LCS_direction[i][j] = kLeftUp;}// it comes from the up entry in the direction matrixelse if(LCS_length[i - 1][j] > LCS_length[i][j - 1]){LCS_length[i][j] = LCS_length[i - 1][j];LCS_direction[i][j] = kUp;}// it comes from the left entry in the direction matrixelse{LCS_length[i][j] = LCS_length[i][j - 1];LCS_direction[i][j] = kLeft;}}}LCS_Print(LCS_direction, pStr1, pStr2, length1 - 1, length2 - 1);return LCS_length[length1 - 1][length2 - 1]; }/ // Print a LCS for two strings // Input: LCS_direction - a 2d matrix which records the direction of // LCS generation // pStr1 - the first string // pStr2 - the second string // row - the row index in the matrix LCS_direction // col - the column index in the matrix LCS_direction / void LCS_Print(int **LCS_direction, char* pStr1, char* pStr2, size_t row, size_t col) {if(pStr1 == NULL || pStr2 == NULL)return;size_t length1 = strlen(pStr1);size_t length2 = strlen(pStr2);if(length1 == 0 || length2 == 0 || !(row < length1 && col < length2))return;// kLeftUp implies a char in the LCS is foundif(LCS_direction[row][col] == kLeftUp){if(row > 0 && col > 0)LCS_Print(LCS_direction, pStr1, pStr2, row - 1, col - 1);// print the charprintf("%c", pStr1[row]);}else if(LCS_direction[row][col] == kLeft){// move to the left entry in the direction matrixif(col > 0)LCS_Print(LCS_direction, pStr1, pStr2, row, col - 1);}else if(LCS_direction[row][col] == kUp){// move to the up entry in the direction matrixif(row > 0)LCS_Print(LCS_direction, pStr1, pStr2, row - 1, col);} }擴(kuò)展:如果題目改成求兩個(gè)字符串的最長公共子字符串,應(yīng)該怎么求?子字符串的定義和子串的定義類似,但要求是連續(xù)分布在其他字符串中。比如輸入兩個(gè)字符串BDCABA和ABCBDAB的最長公共字符串有BD和AB,它們的長度都是2。
博主何海濤對本博客文章享有版權(quán)。網(wǎng)絡(luò)轉(zhuǎn)載請注明出處http://zhedahht.blog.163.com/。整理出版物請和作者聯(lián)系。對解題思路有任何建議,歡迎在評論中告知,或者加我微博http://weibo.com/zhedahht或者http://t.163.com/zhedahht與我討論。謝謝。
總結(jié)
以上是生活随笔為你收集整理的程序员面试题精选100题(20)-最长公共子串[算法]的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 程序员面试题精选100题(19)-反转链
- 下一篇: 程序员面试题精选100题(21)-左旋转