五大算法之一-動態規划(從《運籌學》和《算法導論》兩個角度分析)


 

 

動態規划專題

 

    摘要:本文先從例子出發,講解動態規划的一個實際例子,然后再導出動態規划的《運籌學》定義和一般解法。接着運用《運籌學》中的階段狀態狀態轉移方程三個關鍵詞來分析例2的解法。緊接着又給出了《算法導論》中動態規划的定義和一般解法,並運用《算法導論》中的最優子結構子問題重疊自下而上三個關鍵詞來分析例3.並比較了這兩種做法的優劣。最后列舉了幾個例子,並給出了部分實現代碼。適合初學者學習動態規划。

    

例1 整錢划分問題

問題描述:我們有面值為1元、3元和5元的硬幣若干枚,如何用最少的硬幣湊夠11元?

問題分析:為什么是湊夠11元,而不是其他的數目(比如說是10元、9元等等)?這里將11改成10元,改變問題的本質嗎?深入思考一下發現,我們將問題抽象出來,如何用最少的硬幣湊夠i(i<11)元,i的取值只是決定了問題的解的規模,而沒有改變的問題的本質。於是我們從i=0開始說起。

我們規定:d(i)=j;表示i元至少需要j個硬幣。

i=0,

顯然只需要0個硬幣可以湊成0元,即有d(0)=0;

i=1,

要湊夠一元,我們只能使用1元的硬幣,即d(1)=d(0)+1=1;

I=2,

要湊夠兩元,我們還是只能使用1元的硬幣,即d(2)=d(1)+1=2;

I=3,

要湊夠三元,就有兩種情況了,我們既可以使用1元的硬幣,同時也可以使用3元的硬幣,於是有d(3)=d(3-3)+1=1(使用3元的硬幣);還可以有:d(3)=d(3-1)+1=d(2)+1=3;通過比較這兩種方法,發現通過使用3元硬幣使得使用的硬幣數目最少,所以d(3)=1;

I=4,

要湊夠4元,可以使用3元硬幣,也可以使用1元硬幣,如果使用3元硬幣:d(4)=d(4-3)+1=d(1)+1=2;如果使用1元硬幣:d(4)=d(4-1)+1=d(3)+1=2;由此可得d(4)=2;

I=5,

要湊夠5元,

  使用1元硬幣:d(5)=d(5-1)+1=d(4)+1=3;

  使用3元硬幣:d(5)=d(5-3)+1=d(2)+1=3;

  使用5元硬幣:d(5)=d(5-5)+1=d(0)+1=1;

於是有d(5)=1;

到此為止,我們可以找到遞推公式:

( d(i)=min{d(i-vj)+1 | vj<=i && vj屬於{1,3,5}})

…依次類推,可以推到d (11).

解析:我們現在回顧是如何解決這個問題的。題目要求我們求解11元的最小分解時,我們沒有直接去求解d(11),而是將這個問題分解成了許多相同(或相似)的子問題。而這些子問題的解的方式相同(或相似)。同時注意到,這些子問題的解具有層次性,一個子問題的解需要用到其他子問題的解。說到這里,我們似乎對動態規划有了一個初步認識。

動態規划 (基於《運籌學》)

  1. 1.     什么是動態規划?

《運籌學》給出的定義是在多階段決策問題中,各個階段所采取的決策一般來說是與時間(也可能是空間,根據實際情況而定)相關的,決策依賴於當前的狀態,又隨即引起狀態的轉移,一個決策序列就是在變化的狀態中產生出來的,故有”動態“的含義。因此把處理它的方法稱為動態規划方法。說白了動態規划算法是按照階段將原問題分解成一個一個的狀態, 當前的狀態將由上一次的狀態利用狀態轉移方程推導出。動態規划主要需要抓住三個關鍵詞:階段狀態狀態轉移方程

1.1    階段

把所給問題的過程,恰當的分解成若干個相互聯系的階段,以便能夠按照一定的次序去求解。例如上面按照需要分解的錢的值(即變量i)來划分階段。

1.2 狀態

狀態是問題的子問題,大部分情況下,狀態之間是相關的,且狀態只與前面出現的狀態有關。我們需要用一張表來保留各個階段的狀態。而且狀態是由底層往上推導的。

1.3狀態轉移方程

如何由前一個狀態推導出后一個狀態,這就需要狀態轉移方程。狀態轉移方程對於所有的子問題來說是通用的。狀態轉移方程是動態規划的核心內容,它表現了如何利用前面的狀態進行決策的過程。

乘熱打鐵,我們運用上面的方法可以進行更加深入的討論了,以上的1是一維的動態規划,下面我們將介紹如何解決二維的動態問題。

例2 二維動態規划問題

問題描述:在M*N個格子里,每個格子裝着若干個蘋果,用A[i][j]表示(i,j)位置的蘋果數目。你從左上角的格子開始, 每一步只能向下走或是向右走,每次走到一個格子上就把格子里的蘋果收集起來, 這樣下去,你最多能收集到多少個蘋果。

問題分析:按照《運籌學》對動態規划的闡述,我們主要抓住三點:階段、狀態、狀態轉移方程。題目求解的是最多能夠收集到多少個蘋果,沒有走動的步驟限制,即理論上可以到達任意一個格子的位置。緊接着考慮什么時候收集的蘋果數目最多?當然是走到(M,N)位置的時候,即右下角時候收集的蘋果數目最多。再接着考慮,怎樣走到右下角?可以從(M-1,N)的位置往下走一行或者從(M,N-1)的位置往右走一行即可到達(M,N)位置,那么怎么取舍呢?當然是選擇這兩種方式中蘋果比較多的一個位置來移動到(M,N)位置。這樣分析而來就可以很容易知道狀態和狀態轉移方程分別是什么了。

(a)位置(m,n)表示目前所處的階段

(b)D(m,n)表示走到(m,n)時能夠收集到的最多的蘋果數目,為狀態

(c)D(m,n)=max{D(m-1,n)(m>0);D(m,n-1)(n>0)}+A(m,n)為狀態轉移方程

根據初始狀態和狀態轉移方程,可以得到遞歸版本的解見Program-2-1:遞歸版本(java)。同時我們提供了一個非遞歸版本的解法,見Program-2-2:非遞歸版本(java)。讀者可以先看看這兩者之間的區別,后面會對遞歸和非遞歸進行詳細的探討。

Program-2-1:遞歸版本(java

//這里采用的是遞歸解法。
    public static int getMaxApples(int i,int j,int[][] maxGet,int[][] apple){
        
        if(i==0 && j==0)
            maxGet[i][j]=apple[i][j];
        else{
            int temp1=0;
            int temp2=0;
            if(i>0)
                temp1=getMaxApples(i-1,j,maxGet,apple); //從上面過來
            if(j>0)
                temp2=getMaxApples(i,j-1,maxGet,apple); //從左面過來
            maxGet[i][j]=temp1>temp2? temp1+apple[i][j] : temp2+apple[i][j];
        }            
        return maxGet[i][j];        
    }    

Program-2-2:非遞歸版本(java

//非遞歸版本
    public static int getMinNumber_(int total,int[] coins){
        int minNumber[]=new int[total+1];//保存每一步驟的結果
        minNumber[0]=0;//初始狀態
        for(int i=1;i<=total;i++){
            minNumber[i]=Integer.MAX_VALUE;
            for(int j=0;j<coins.length;j++){
                if(coins[j]<=i && minNumber[i-coins[j]]+1<minNumber[i]){
                    minNumber[i]=minNumber[i-coins[j]]+1;
                }
            }
        }
        return minNumber[total];
        
    }

例2拓展:現在我們改變一下所求問題。其他條件不變,求走動K步后能夠得到的最多的蘋果數目。

    通過例1和例2的訓練,我們差不多已經知道這類問題的一般性解法了。主要抓住階段、狀態和狀態轉移方程三個關鍵詞。仔細思考如何將原來的問題按照這三個標准來進行分解。筆者認為《運籌學》是一本很好的很切合實際的教材,所以將其思想列在前面,后續篇章將從《算法導論》中汲取知識,進行總結。這是本文的重點所在。

例3:裝配線問題

問題描述:一個汽車公司在有2條裝配線的工廠內生產汽車,每條裝配線有n個裝配站,不同裝配線上對應的裝配站執行的功能相同,但是每個站執行的時間是不同的。在裝配汽車時,為了提高速度,可以在這兩天裝配線上的裝配站中做出選擇,即可以將部分完成的汽車在任何裝配站上從一條裝配線移到另一條裝配線上。裝配過程如下圖所示:

 

    裝配過程的時間包括:進入裝配線時間e、每裝配線上各個裝配站執行時間a、從一條裝配線移到另外一條裝配線的時間t、離開最后一個裝配站時間x。舉個例子來說明,現在有2條裝配線,每條裝配線上有6個裝配站,各個時間如下圖所示:

 

這道題看上去很復雜的樣子,仔細理一理思路,還是很簡單很基礎的一道動態規划題目,如果運用《運籌學》的思想,很容易抽出三個關鍵詞。

D[i,j]表示到達第i條裝配線的第j個站點時所需的最短時間;那么它可能由第1條裝配線的第j-1個站點而來,也可能是由第2條裝配線的第j-1個站點而來,故狀態轉移方程為:

D[1,j]=min{D[1,j-1]+a[1,j] , D[2,j-1]+t[2,j-1]+a[1,j]}

D[2,j]=min{D[2,j-1]+a[2,j] , D[1,j-1]+t[1,j-1]+a[2,j]}

初始狀態有:

D[1,1]=9;

D[2,1]=12;

最終狀態有:

Dfinal=min{D[1,n]+x1 , D[2,n]+x2}。

現在,我們換一種思路,暫時忘掉階段、狀態、狀態轉移方程三個關鍵詞。我們將引進最優子結構子問題重疊自底向上三個關鍵詞。

動態規划(基於《算法導論》)

  1. 1.     動態規划定義

和分治算法一樣,動態規划是通過組合子問題[1]的解而解決整個問題的。分值算法是將問題分成一些獨立的子問題,遞歸的求解各個子問題,然后合並子問題的解而得到原問題的解。與此不同,動態規划適用於子問題不是獨立的情況,也就是各子問題包含公共的子子問題[2]。在這種情況下,若用分治算法則會做出許多不必要的工作,即重復的求解公共子問題。動態規划算法對每個子子問題只求解一次,將其結果保存在一張表[3]中,從而避免每次遇到子問題時重新計算。

[1] 組合子問題:動態規划是將原問題的解分解成若干個子問題,那么這種分解是否需要滿足什么規律?-最優子結構。

[2] 各子問題包含公共的子子問題:即分解產生的若干子問題,他們的子子問題具有公共部分。-子問題重疊。

[3]結果保存在一張表:將每次得到的一個子問題的解保存在一張表中,這張表自底向上依次構建,下次遇到相同的子問題時,查閱該表即可。-自底向上。

通過上面的解析,我們下面重點講解一下:最優子結構、子問題重疊和自底向上三個重要的關鍵詞。

1.1    最優子結構

用動態規划求解的第一步是找出最優子結構。最如果問題的一個最優解包含了子問題的最優解,則該問題具有最優子結構。什么意思呢?拿例3來說,對於D[1,j]來說,他可以是前面一個1號線裝配站[1,j-1]過來的,也可以是2號線裝配站[2,j-1]過來的。有且僅有這兩種選擇。我們假設選擇了k號線過來的裝配站,那么對於裝配站來說[k,j-1]也必須是耗時最短的。因為如果存在另外一條線路使得[k,j-1]的耗時更短,我們就選擇更短的那個耗時,而不是原來的那個。(說起來有點拗口,但是原理和貪心算法類似)

1.2    子問題重疊

子問題重疊是動態規划算法區別於分治算法的重要原因。子問題重疊的意思是不同子問題可能會用到相同的子子問題。例如再計算D[1,6]的時候,他必然會用到D[1,2],計算D[1,5]的時候,他也必然會用到D[1,2],所以子問題D[1,6]和D[1,5]的子問題有重疊。這也是動態規划能夠提高計算效率的本質原因。

1.3    自底向上

可以說這個是動態規划能夠有效提高計算效率的關鍵技術所在。(另外一種技術是使用備忘錄,也可以起到相同的效果,詳細請參考《算法導論》)動態規划建表的順序是自底向上的,由最底層的子問題開始,逐步網上推導,最終得到原問題的解。

  1. 2.     動態規划使用方法

上面介紹了動態規划的定義,下面簡單講解一下如何運用這種思想來解題。第一步:找到最優子結構。觀察原問題,嘗試修改原問題的規模,比如將11元改成10元等等,看看原問題是否可以分解,而且這種分解是否還滿足最優子結構性質。如何檢查問題是否滿足最優子結構性質,可以采用“剪貼法”。第二步:判斷子問題是否重疊。第三步:自底向上簡歷表格。我們得到例3的動態規划解法,見Program-3-1.

Program-3-1.非遞歸版(java

public static void main(String args[]){
        /*測試數據
         2 4 
         6 
         7 9 3 4 8 4 
         8 5 6 4 5 7 
         2 3 1 3 4
         2 1 2 2 1
         3 2
         
         */
        Scanner input=new Scanner(System.in);
        System.out.println("請輸入分別進入裝配線1,2所需要的時間:");
        int e1=input.nextInt();
        int e2=input.nextInt();
        System.out.println("請輸入裝配線的機器數目:");
        int N=input.nextInt();
        System.out.println("請輸入1.2 裝配線機器加工時間");
        int a[][]=new int[2][N]; //每台機器裝配所需時間
        for(int i=0;i<N;i++)
            a[0][i]=input.nextInt();
        for(int i=0;i<N;i++)
            a[1][i]=input.nextInt();
        int t[][]=new int[2][N-1]; //交換裝配線所需的轉移時間
        System.out.println("請輸入轉移裝配線所需時間:");
        for(int i=0;i<N-1;i++)
            t[0][i]=input.nextInt();
        for(int i=0;i<N-1;i++)
            t[1][i]=input.nextInt();
        System.out.println("請輸入輸出裝配線所需時間:");
        int x1=input.nextInt();
        int x2=input.nextInt();
        
        //函數主程序
        int d[][]=new int[2][N];//存儲到達[i,j]裝配站時的最小時間
        int d_final=0; //存儲最終所需最小時間
        //初始狀態
        d[0][0]=e1+a[0][0];
        d[1][0]=e2+a[1][0];
        //狀態轉移方程
        for(int j=1;j<N;j++){
            //修改d[0][j]
            d[0][j]=d[0][j-1]<d[1][j-1]+t[1][j-1] ? d[0][j-1]+a[0][j] : d[1][j-1]+t[1][j-1]+a[0][j];
            //修改d[1][j]
            d[1][j]=d[1][j-1]<d[0][j-1]+t[0][j-1] ? d[1][j-1]+a[1][j] : d[0][j-1]+t[0][j-1]+a[1][j];
        }
        d_final=d[0][N-1]+x1<d[1][N-1]+x2 ? d[0][N-1]+x1 : d[1][N-1]+x2;
        
        System.out.println("所需最小時間為:"+d_final);
    }

《運籌學》和《算法導論》

看到這里,我們可以做一個簡單的總結。

《運籌學》從階段、狀態、狀態轉移方程三個關鍵詞來描述問題、建立模型和解決問題;《算法導論》從最優子結構、子問題重疊、自下而上三個關鍵詞來描述問題、建立模型和解決問題。這兩者之間有什么聯系和區別呢?仔細想想就可以發現確實有一一對應關系。狀態對應子問題,狀態轉移方程對應最優子結構!他們是對同一類問題從不同的角度出發去解決問題,都有自己的優缺點。筆者認為《運籌學》從細節出發,去發現采用什么樣的粒度將問題分解比較合適,注重問題分解的過程。而《算法導論》一上來就是先將問題分解,然后再思考怎么將這些分解的問題自底向上合並起來。此外,算法導論還說明了如果問題不滿足最優子結構,則不能使用動態規划,這種說法非常嚴謹,而運籌學沒有強調這個關鍵性問題。

既然《算法導論》更為嚴謹,那筆者為什么還要介紹《運籌學》呢?因為筆者腦子不夠用啊(= =||),確實如此,如果接觸不多動態規划,誰能夠第一眼就看出最優子結構?(對於接下來筆者要解析的例4和例5,讀者可以自己嘗試一下)如果我們按照運籌學的思想,將問題抽絲剝繭,分成階段、狀態,進而找到狀態轉移方程,豈不是很好的一種入門手段?

所以說,對於初學者來說,可以先利用《運籌學》的思想,來找到狀態轉移方程(最優子結構),然后再利用《算法導論》思想,講其轉換成非遞歸的自下而上逐層建表的模式。我們將在例4詳細闡述這種做法。

例4. 矩陣鏈乘法

問題描述:給定n個矩陣構成的一個鏈<A1,A2,A3,…An>,矩陣Ai為pi-1*pi維數(i=1,2,3,…n),對乘積A1A2A3…An以一種最小化標量乘法次數的方式進行加全部括號。

問題分析:對於n個矩陣相乘,他們是怎么進行運算的呢?舉個例子來說,當兩個矩陣相乘時,只有一種運算方式,直接相乘即可。當三個矩陣相乘時,可以先將前兩個矩陣相乘,然后再與第三個矩陣相乘,也可以先將后兩個矩陣相乘,然后再與第一個矩陣相乘。可見隨着矩陣數量的增加,這種相乘的順序會急劇增長。如何采用一種相乘方式來使得乘法運算總次數最少呢?這么一分析,我們已經找到了狀態

必須注意到這樣一個現象:對於任意的n>1,他都可以分解成兩個矩陣的乘積。具體來說對於矩陣鏈A1A2A3…An來說,它可以分解成(A1A2A3…Ak)和(Ak+1AK+2Ak+3…An)這兩個矩陣的乘積,其中k=1,2,…n-1.為了敘述簡便,我們記

Ai…j表示AiAi+1Ai+2…Aj

M[i,j]表示Ai…j所需最少的乘法運算次數。

那么要使得A1…n乘法運算次數最少,這里的k必須使得A1…k 和 Ak+1…n 乘法運算次數之和加上p0pk-1pn(表示分解成A1…k和Ak+1…n后的兩個矩陣相乘時乘法運算次數)的值最少。要求得最優的M[1,n],則對於分解后的A1…k和 Ak+1…n來說,他們的子分解也必須是最優的。於是有M[1,n]=min{M[1,k]+M[k+1,n]+ p0pk-1pn}(1<=k<n)。這樣我們就找到了最優子結構,同時也找到了狀態轉移方程。

於是就得到了遞歸方程:

根據遞歸方程,我們可以得到一個遞歸解(見Program-4-1)。但是需要注意的是,如果用遞歸來解決這個題目,就違背了動態規划的本質。動態規划是遞歸,遞歸不一定是動態規划。這就是動態規划和遞歸的本質區別。動態規划強調的是自底向上構建一個表,遇到重疊的子問題,直接查找表格即可,而不是再次的去計算。

那么如何構建這樣的一個表呢?

                                                                                          圖1.

如圖1所示,我們從最底層出發,逐層往上求解,即可求得【1,6】的值(見Program-4-2)。

Program-4-1:遞歸版本(java

public static int getMinSubMuti(int[] P, int start, int end){
//P:矩陣的維數;start:起始矩陣;end:終止矩陣。
        if(start==end)
            return 0;
        else{
            int tempMin=Integer.MAX_VALUE;
            for(int i=start;i<end;i++)
                if(getMinSubMuti(P,start,i)+getMinSubMuti(P,i+1,end)+P[start-1]*P[i]*P[end]<tempMin){                    tempMin=getMinSubMuti(P,start,i)+getMinSubMuti(P,i+1,end)+P[start-1]*P[i]*P[end];
                }
            return tempMin;
        }    
    }
View Code

Program-4-2:非遞歸版本(java

public static int getMinMuti_(int[] P){
        int M[][]=new int[P.length-1][P.length-1];//存儲最少乘法次數
        for(int i=0;i<P.length-1;i++){
            M[i][i]=0;//初始狀態
        }
        for(int i=0;i<P.length-2;i++){
            M[i][i+1]=P[i]*P[i+1]*P[i+2]; //相鄰的兩個矩陣相乘的乘法次數
        }
        for(int i=2;i<P.length-1;i++){
            for(int j=0;j+i<P.length-1;j++){
                //求M[j,j+i];表示從第i+1個矩陣到第j+i+1個矩陣
                M[j][j+i]=Integer.MAX_VALUE;
                for(int k=j+1;k<j+i+1;k++){//k表示從第k的矩陣分裂
                    if(M[j][k-1]+M[k][j+i]+P[j]*P[k]*P[j+i+1]<M[j][j+i])
                        M[j][j+i]=M[j][k-1]+M[k][j+i]+P[j]*P[k]*P[j+i+1];
                }
            }
        }        
        return M[0][P.length-2];
    }
View Code

例5:最長公共子序列(LCS)

問題描述:給定兩個序列X=<x1,x2,x3,…,xm>和Y=<y1,y2,y3,…,yn>,找出X和Y的最大長度公共子序列。

問題分析:這道題第一次看到的時候,不知道該怎么下手。首先我們來看看最大長度公共子序列的性質:

假設Z=<z1,z2,z3,…,zk>是X和Y的任意一個最大長度公共子序列,則

(1)  若xm= yn ,則有Zk-1是Xm-1和Yn-1的一個最大長度公共子序列。

(2)  若xm yn ,則由zk yn可以得出Zk是Xm和Yn-1的一個最大長度子序列

(3)  若xm yn ,則由zk xm可以得出Zk是Xm-1和Yn的一個最大長度子序列

簡單解釋一下這三條性質的含義。

(1)  第一條的意思是如果X和Y的末尾含有相同元素,則此相同元素一定在最長共公共子序列中,那么X、Y和Z就可以同時減掉最后一個相同元素。

(2)  第二條和第三條是說如果X和Y的末尾元素不相同,那么Z的末尾元素不可能同時和xm,yn相等。(即有三種情況:(a)zk ynzk=xm(b)zk xm ,zk yn(c)zk ynzk xm)果Z末尾元素不是xm,則可以將xm從X的末尾移除而不影響結果;同樣的道理適用於Y。

有了這些性質,我們該怎樣運用呢?即未知變量該如何設置?這個未知變量要能夠同時包含X和Y的相關信息,設M[i,j]表示X=< x1,x2,x3,…,xi >和Y=<y1,y2,y3,…,yj>的最大公共子串的長度。我們將上面的性質翻譯成數學符號:

根據遞歸式,可以得到一個遞歸解(見Program-5-1)。

Program-5-1java

//遞歸版本的解:
    public static String getLCS(String X,String Y){
        String Z="";
        Z=getSubLCS(X,X.length()-1,Y,Y.length()-1);
        return Z;
    }
    public static String getSubLCS(String X,int i,String Y,int j){
        if(i==-1 || j==-1) //空的字符串和任意字符串的LCS都是空。
            return "";
        else if(X.charAt(i)==Y.charAt(j)){ //如果兩個字符串的末尾元素相同,則繼續往頭找LCS,並將相同的元素記錄下來。
            return getSubLCS(X,i-1,Y,j-1)+X.charAt(i);
        }
        else{ //如果兩個字符串的末尾元素不同,則求其分別剪枝后的最長LCS。
            String s1=getSubLCS(X,i-1,Y,j);
            String s2=getSubLCS(X,i,Y,j-1);
            return s1.length()>s2.length() ? s1 : s2;
        }
    }
View Code

同時,我們繼續思考如何構建一個表格來建立自下而上的求解過程。這個表的建立過程類似於例2,如圖2所示。

圖2.

如圖展示了長度為4的字符串和長度為3的字符串計算子問題時的順序。由第一層出發,逐層往上計算。具體代碼見Program-5-2

Program-5-2(java)

//非遞歸版本的解:
    public static String getLCS_(String X,String Y){
        int x_length=X.length();
        int y_length=Y.length();
        int M[][]=new int[y_length+1][x_length+1];//所需要建立的表
        int flag[][]=new int[y_length+1][x_length+1];//用來記錄查找過程
        for(int i=0;i<=x_length;i++)
            M[0][i]=0; //初始化第一行的數據為0;
        for(int j=0;j<=y_length;j++)
            M[j][0]=0; //初始化第一列數據為0;
        int layer=x_length<y_length?x_length:y_length;
        for(int k=1;k<=layer;k++){
            for(int j=k;j<=x_length;j++){ //掃描第k層的一行
                //求M[k,j]:k表示Y中前k個元素,j表示X中前j個元素
                if(Y.charAt(k-1)==X.charAt(j-1)){
                    M[k][j]=M[k-1][j-1]+1;
                    flag[k][j]=2;//表示從左上角過來的
                }
                    
                else{
                    if(M[k-1][j]>M[k][j-1]){
                        M[k][j]=M[k-1][j];
                        flag[k][j]=1; //表示從上面過來的
                    }
                    else{
                        M[k][j]=M[k][j-1];
                        flag[k][j]=0;//表示從左邊過來的
                    }
                }                
            }
            for(int i=k;i<=y_length;i++){//掃描第k層的一列
                //求解M[i][k]
                if(X.charAt(k-1)==Y.charAt(i-1)){
                    M[i][k]=M[i-1][k-1]+1;
                    flag[i][k]=2;//表示從左上角過來的
                }
                else{
                    if(M[i-1][k]>M[i][k-1]){
                        M[i][k]=M[i-1][k];
                        flag[i][k]=1;//表示從上面過來的
                    }
                    else{
                        M[i][k]=M[i][k-1];
                        flag[i][k]=0;//表示從左邊過來的
                    }
                }
            }
        }
        int end_x=y_length; //終點所在的行
        int end_y=x_length; //終點所在的列
        String z="";
        while(end_x!=0 && end_y!=0){
            if(flag[end_x][end_y]==2){
                z=X.charAt(end_y-1)+z;
                end_x--;
                end_y--;
            }
            else if(flag[end_x][end_y]==1)
                end_x--;
            else
                end_y--;
        }
        return z;
    }
View Code

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM