1. 程式人生 > >動態規劃7:最優編輯練習題

動態規劃7:最優編輯練習題

題目:對於兩個字串A和B,我們需要進行插入、刪除和修改操作將A串變為B串,定義c0,c1,c2分別為三種操作的代價,請設計一個高效演算法,求出將A串變為B串所需要的最少代價。給定兩個字串A和B,及它們的長度和三種操作代價,請返回將A串變為B串所需要的最小代價。保證兩串長度均小於等於300,且三種代價值均小於等於100。

測試樣例:"abc",3,"adc",3,5,3,100返回:8

思路:這種題目如果不使用動態規劃基本無從著手,難度很大,時間複雜度很高,因此在思考時就應該從動態規劃的角度去思考問題,動態規劃有4部曲,逐步解決即可。對於這種能夠將問題拆分成為2個維度的問題(A串作為縱向,B串作為橫向),通常就是使用動態規劃建立二維陣列來進行解決。在建立二維陣列時要注意是建立n*m的矩陣還是建立(n+1)*(m+1)的矩陣,關鍵是思考對於陣列為””空串的情形是否要作為初始條件,即是否應該讓空串作為初始條件,從實際意義上來理解,空串也是一種字串,且與可能要求將空串編輯成為某個字串,或者將某個字串編輯成為空串,因此需要對字串A,B從空串“”開始進行拆分考慮。但是實際上是否從空串或者0開始分解要看如果分解成為這種初始條件是否可以據此求出後面的所有情況的結果,即所求的第1行,第1列應當是有效的,能夠據此求出後面的一系列情況,而不是固定的記住。

找零錢問題:

使用0~i種零錢湊出j,這裡i從0即第1個數開始,j從0開始到aim共有aim+1個數值,之所以要從0考慮並且將使用0~i湊出j的值都記為1(其實這沒有實際意義)是為了讓之後的dp[i][j]計算時利用1恰好能夠得到所需的結果,因此並不是先確定了1再求結果而是根據結果需要將這一列放到dp陣列中並令其為1,其實這裡1的本質是遞迴條件的邊界條件,怎樣算作一種方法呢?當aim=0時表示拼湊結束於是當前的零錢0~i算作一種方案。也可以記住,零錢問題是一個典型的動態規劃問題,應該記住。

臺階問題:

f(n)=f(n-1)+f(n-2);

先求邊界初始值,f(1)=1表示上1級臺階有1中方法;f(2)=2表示上2級臺階有2種方法,那麼f(0)是否需要呢?如果需要值應該為幾呢?根據需要來思考,為了使得f(2)=f(1)+f(0)成立,得出f(0)應該等於1,即上0級臺階的方法數目是1,這其實沒有什麼實際意義,僅僅從需要出發,對於每次跳1或2級的簡單臺階問題,臺階數是否從0開始考慮都沒有關係,但是對於變態臺階問題,每次可以跳的數目是arr[0~i],那麼必須建立二維dp[][]矩陣才能解決,等價於零錢湊整問題。因此為了使得dp[][]可以遞推求得,必須先計算出j=0時的dp[][]的值。

矩陣最小路徑和問題:

這裡行i和列j是已經給定的i和j是離散值不是連續值,同時第1行和第1列的值容易求出,之後的dp[i][j]可以根據第1行和第1列的值求出,因此建立的dp[][]矩陣是n*m

LIS最長遞增子序列問題:

給定的需要研究的是一個數組的最長遞增子序列,對於陣列顯然只能從第1個元素,即i=0開始進行研究和分解,不像零錢問題中的零錢需要考慮湊出0元的情況和臺階問題中需要考慮上0級臺階的情況。

LCS最長公共子序列問題:

給定的是2個字串,求最長公共子序列,按照前面的經驗應該從空串開始考慮,即先求String1空串與String2前j個字元的公共子序列長度,再求String空串與String1前i個字元的公共子序列長度,但是分析發現此時第1行和第1列都是0,顯然無法據此推出之後的dp[][],因此還是需要計算第2行和第2列,因此一開始建立dp[][]的時候就不應該考慮String1和String2為空串的情況,即是否考慮最基礎的空串或者aim=0作為第1行和第1列沒有固定規律可循,關鍵是看以誰作為第1行第1列可以遞推出後面的所有dp[][]值以及此時的第1行第1列是否容易求出(動態規劃中的第1行和第1列總是可以很簡單的直接求出且不依賴於前面的結果,如果計算時還需要依賴前面的結果那麼必然不是最初始的情況)

01揹包練習題:

本題中重量j是一個連續值,按照前面的經驗,應該從j=0開始考慮到cap共cap+1個值,先求出第1行和第1列,發現第1列全部是0,發現這1列其實是沒有意義的,無法據此推出後面的結果,那麼應該計算第2列作為初始值,但是這裡特殊的,發現對於任意dp[i][j]它僅僅依賴於上一層的資料而不依賴於本層左邊的資料,因此只需要求出第1行的基礎資料,之後就可以遞推求處所有的資料了。但是為了統一,還是將j從0開始考慮。

最優編輯問題:

本題給定2個字串A,B求將A轉換為B的最小代價,無論從經驗還是從實際意義上來考慮都需要從空串進行考慮,於是應該建立一個(n+1)(m+1)的dp[][]矩陣。

先計算第1行和第1列的結果,對於第1行,就是要將空串“”轉變為“”或者str2的前j個字串,轉換方式有很多種,可以刪刪改改增增減減,但是代價最小的方式一定是逐個插入元素,於是,第一行的dp值就是插入j個元素所需要的代價,即dp[0][j]=ic*j;同理對於第1列表示從str1的前i個元素字串轉變為空串,最小代價一定是逐個刪除元素,於是對於第一列,dp[i][0]=dc*i;此外,對於任何位置dp[i][j]表示用字串str1中0~i-1的字串來轉換為str2中0~j-1的字串,此時的最小代價只可能來自4種情況:其實在動態規劃中是為了利用前面已經計算得到的結果而去找出當前所求的dp[i][j]與之前已經求出的dp[i-1][j]或者dp[i][j-1]或者dp[i-1][j-1]的關係,即是主動的去找可能可以得出結果的情況,然後想辦法將其轉變為當前所求的值,可以直接有前面的值得到當前值,也可以找幾個可能可以推出當前所求值的情況作比較得出當前值。

情況1:對於dp[i][j]例如上圖中要將“ab12c”轉變為“abcd”,可以先將“ab12c”刪除最後一個字元c得到“ab12”,然後將“ab12”轉變為“abcd”,由於“ab12”轉變為“abcd”的最小代價已知是dp[i-1][j],於是此時的最小代價是dp[i-1][j]+c1;

情況2:可以先將“ab12c”轉變為“abc”即最小代價為dp[i][j-1],然後再插入最後一個元素d,此時的最小代價為dp[i][j-1]+c0;

情況3:如果2個要轉換的字串最後一個元素不同,即str1[i-1]!=str2[j-1],那麼還可以先將“ab12”轉變為“abc”,然後將最後一個元素進行替換,即從c替換為d,此時的最小代價為dp[i-1][j-1]+c2;

情況4:當2個要轉換的字串的最後一個元素相同是,即str1[i-1]==str2[j-1],那麼此時的最小代價就是不用動最後一個元素,將前面的2個字串進行轉換的最小代價,即為dp[i-1][j-1].

理解:對於dp[i][j]的最小值,它必然來自於上面4中情況之一,不可能有其他情況,即思考時按照轉變時最後一個步驟的操作的情況來劃分的,最後一個步驟只能是插入、刪除或者替換。

動態規劃4部曲:

①建立動態規劃二維陣列存放答案dp[n+1][m+1];

②計算第1行和第1列的結果

第1行:

dp[0][j]=c0*j;

第1列:

dp[i][0]=c1*i;

③從上到下,從左到右計算任意dp[i][j],按照str1[i-1]==str[j-1]是否成立來分開考慮

If(str1[i-1]==str[j-1])dp[i][j]=dp[i-1][j-1]

If(str1[i-1]!=str[j-1])dp[i][j]=Math.min(Math.min(dp[i][j-1]+c0,dp[i-1][j]+c1),dp[i-1][j-1]+c2);

④返回結果,矩陣右下角的值就是結果dp[n][m];
import java.util.*;
//最優編輯問題:動態規劃
public class MinCost {
    public int findMinCost(String A, int n, String B, int m, int c0, int c1, int c2) {
        //特殊輸入
        if(A==null||B==null||n<0||m<0) return 0;
        //先將字串轉變為陣列才能進行操作
        char[] arrA=A.toCharArray();
        char[] arrB=B.toCharArray();
        //①建立二維陣列存放結果dp[][]
        int[][]dp=new int[n+1][m+1];
        //②計算第1行的結果
        for(int j=0;j<=m;j++){
            dp[0][j]=c0*j;
        }
        //②計算第1列的結果
        for(int i=0;i<=n;i++){
            dp[i][0]=c1*i;
        }
        //③從上到下,從左到右計算任意位置dp[][]
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                if(arrA[i-1]==arrB[j-1]){
                    dp[i][j]=dp[i-1][j-1];
                }else{
                    dp[i][j]=Math.min(Math.min(dp[i-1][j]+c1,dp[i][j-1]+c0),dp[i-1][j-1]+c2);
                }
            }
        }
        //④返回結果,即右下角的值
        return dp[n][m];
    }
}
總結:動態規劃的方法並不難,套路很明顯,難點還是在於對問題邏輯的處理,對關聯推導關係的挖掘,即如何通過已經計算得到的結果來計算出dp[i][j];