算法導論(十五)--動態規劃,最長公共子序列

算法導論(十五)--動態規劃,最長公共子序列

最長公共子序列(LCS)

給定兩個序列x[1,…,m]和y[1,…,n],找出它們的一個最長公共子序列(LCS)(可能不唯一)。
例如:
x: A B C B D A B
y: B D C A B A
先肉眼看看它們的LCS是什麼,是BDAB,BCAB和BCBA。
這個問題可以用窮舉法(Brute-Force)來解決:檢查x中的所有子序列,看看y裏是否有一樣的子序列。
分析一下這種方法:如果已知x的一個子序列,需要花費多少時間來判斷它是不是y的子序列呢?就是y的長度O(n)。把y掃描一遍,如果發現一樣的字符,就按x和y的下標開始遞歸,看剩下的有沒有一樣的。x有多少子序列呢?有2m個,因爲x的每一個字符可以選或者不選,一共有m個字符。因此複雜度是O(n·2m),是指數級的,非常的慢。

簡化:
1.Look at the length of LCS(x,y)
2.Extend the all to find LCS itself
方法:
考慮x和y的前綴以及如何用前綴描述LCS。定義c[i, j]是x[1, i]和y[1, j]的LCS,c[i, j]=|LCS(x[1, i],y[1, j])|,如果我們知道了c[i, j],那麼c[m, n]就是要求的答案,c[m, n]=|LCS(x,y)|。下面就是找出c[i, j]的歸納式。

c[i,j]={c[i1,j1]+1x[i]=y[j]max{c[i1,j],c[i,j1]}c[i, j] = \begin{cases} c[i-1, j-1]+1 & 如果x[i]=y[j] \\ max\{c[i-1 ,j], c[i, j-1]\} & 其他 \end{cases}

也就是說,給定c[i, j],它的值是由兩個嚴格小於它的值決定的。
就不證明了,可以自己舉例。

動態規劃的特徵之一就是最優子結構,即問題的一個最優解包含了子問題的最優解。例如,如果z=LCS(x, y),那麼任何z的前綴都是x的某個前綴和y的某個前綴的LCS。如果子問題的解不是最優的,那麼總是可以用剪貼法找到一個全局最優解。

LCS問題的算法

1. 遞歸法
僞代碼:

LCS(x,y,i,j)
if(x[i]==y[j])
	then c[i,j]<-LCS(x,y,i-1,j-1)+1
else
	c[i,j]<-max{LCS(x,y,i-1,j),LCS(x,y,i,j-1)}
return c[i,j]

分析:
這個程序的最壞情況是什麼?對於所有的i,j,x[i]≠y[j]。畫一下遞歸樹,假設m=7,n=6:
在這裏插入圖片描述
這棵樹的高度是m+n的指數階(2m+n),還是很慢。可以觀察到這棵樹的很多項都是重複的,整個子樹都是一樣的,說明要解決的是相同的子問題。
動態規劃的第二個特徵就是重疊子問題,即一個遞歸的過程中,少數獨立的子問題被反覆計算了很多次。LCS問題的子問題空間,也就是獨立子問題的個數,是m*n。

2. 自頂向下的備忘法:
在計算完每個子問題的時候記錄一下,那麼下次需要這個值的時候就不用再算了。
僞代碼:

//如果c[i,j]沒有計算過,就計算,否則就返回
LCS(x,y,i,j)
if(c[i,j]==nil)
	then if(x[i]==y[j])
			then c[i,j]<-LCS(x,y,i-1,j-1)+1
		else
			c[i,j]<-max{LCS(x,y,i-1,j),LCS(x,y,i,j-1)}
return c[i,j]

分析:
這個算法花費的時間是O(mn)(平攤分析),花費的存儲空間也是O(mn)。

3. 自底向上的計算表格法:
列出表格,根據公式計算:

字符 0 A B C B D A B
0 0 0 0 0 0 0 0 0
B 0 0 1 1 1 1 1 1
D 0 0 1 1 1 2 2 2
C 0 0 1 2 2 2 2 2
A 0 1 1 2 2 2 3 3
B 0 1 2 2 3 3 3 4
A 0 1 2 2 3 3 4 4

分析:
計算這個表的時間是O(m*n)。這個表可以通過回溯法重建LCS,就是從右下角的4出發,4是由一次選擇產生的,選擇左邊的4或者上面的4,假設是選擇左邊的4產生的,我們向左走,這個4是根據左上角的3加1產生的,所以我們往左上角走,並標記出現在這個4位置上的字符A,以此類推,上圖紅色標出的就是我們的回溯路徑。這只是LCS之一,如果在兩個相同的數裏選最大的,就會有不同的選擇。

*實際上,用O(min(m,n))的空間就能解決問題,因爲假如我們看行,只要算出了一行,那麼上一行的數就沒有用了。

代碼:

void LCS(int* x,int m,int* y,int n)
{
    int **c=new int*[m+1];
    for(int i=0;i<=m;i++)
        c[i]=new int[n+1];
    for(int i=0;i<=m;i++)
        for(int j=0;j<=n;j++)
            c[i][j]=0;
    std::vector<int> v;
    for(int i=0;i<m;i++)
        for(int j=0;j<n;j++)
        {
            if(x[i]==y[j])
            {
                c[i+1][j+1]=c[i][j]+1;
                v.push_back(x[i]);
            }
            else if(c[i][j+1]>c[i+1][j])
                c[i+1][j+1]=c[i][j+1];
            else
                c[i+1][j+1]=c[i+1][j];
                
        }
    cout<<c[m][n]<<endl;
    for(int i=0;i<v.size();i++)
        cout<<v[i]<<" ";
    for(int i=0;i<=m;i++)
    {
        delete[] c[i];
        c[i]=NULL;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章