動態規劃套路在最長公共子串、最長公共子序列和01揹包問題中的應用

適合動態規劃(DP,dynamic programming)方法的最優化問題有兩個要素:最優子結構和重疊子問題。html

最優子結構指的是最優解包含的子問題的解也是最優的。python

重疊子問題指的是每次產生的子問題並不老是新問題,有些子問題會被重複計算屢次。ios

因此能夠用以下步驟來解決:算法

  1. 遞歸定義最優解計算公式
  2. 構造dp矩陣,按照公式逐步計算。

接下來咱們分別在最長公共子串、最長公共子序列和01揹包問題中演示如何應用上面的套路。數組

開始討論以前要明白子串和子序列的區別在於:子串要求在原字符串中是連續的,而子序列則沒有要求[1]。例如, 字符串 s1=abcde,s2=ade,則LCStr=de,LCSeq=ade。post

其中LCStr表示Longest Common Substring 。LCSeq表示Longest Common Subsequence。測試

1 最長公共子串

問題定義優化

對於兩個字符串,請設計一個時間複雜度爲O(m*n)的算法(這裏的m和n爲兩串的長度),求出兩串的最長公共子串的長度。 給定兩個字符串A和B,同時給定兩串的長度n和m。spa

測試樣例:"1AB2345CD",9,"12345EF",7設計

返回:4

1.1 遞歸公式

假設有字符串x,y

f(i,j)表示x中以x[i]結尾的子串集合和y中以y[j]結尾的子串集合,二者交集中最長串的長度。(i和j都在合法範圍內)

好比x="caba",以a[2]結尾的子串集合以下: { "b","ab","cab"}

當x[i]!=y[j]時,毫無疑問,交集爲空集,此時f(i,j) = 0

當x[i]==y[j]時,f(i,j) = f(i-1,j-1) + 1

當i=0或者j=0時,f(i,j) = 0 //遞歸出口,邊界狀況

1.2 dp矩陣

有了遞歸公式就能夠設計dp表格了,假設x="caba" , y="bab" , 二維數組dp[i][j]對應f(i,j)。當x[i]==y[j]時,dp[i][j]=dp[i-1][j-1]+1

從左到右,從上到下計算獲得dp表格:

一樣的本題的dp表格以下

1.3 AC代碼

class LongestSubstring {
public:
    int findLongest(string A, int n, string B, int m) {
        //初始化n*m matrix
        vector<vector<int> > dp(n,vector<int>(m,0));
        int res=0;
        for(int i=0;i<n;i++)//x串
        {
            for(int j=0;j<m;j++)// y串
            {
                if(A.at(i)==B.at(j))
                {
                    if(i==0||j==0)  dp[i][j]=1;
                    else    dp[i][j]=dp[i-1][j-1]+1;
                    // udpate maximum
                    if(res<dp[i][j]) res=dp[i][j];
                }
            }
        }
        return res;
    }
};

2 最長公共子序列LCS

問題定義

對於兩個字符串,請設計一個高效算法,求他們的最長公共子序列的長度, 這裏的最長公共子序列定義爲:有字符串A的下標序列U1,U2,U3...Un和字符串B的下標序列V1,V2,V3...Vn,其中Ui < Ui+1,Vi < Vi+1。且A[Ui] == B[Vi]。

給定兩個字符串A和B,同時給定兩個串的長度n和m,請返回最長公共子序列的長度。保證兩串長度均小於等於300。

測試樣例:"1A2C3D4B56",10,"B1D23CA45B6A",12

返回:6

2.1 遞歸公式

假設有字符串x和y, 這裏咱們用x[0,i]來表示x中下標爲[0,i]的子串切片,對應於python中的x[0:i+1]。好比x="abcdea",那麼x[0,1]就表示"ab"。

定義f(i,j)表示x[0,i]和y[0,j]之間的最長公共子序列。

毫無疑問咱們又要想辦法用f(i-1,j-1)來表示f(i,j)。

當x[i]==y[j]時,f(i,j) = f(i-1,j-1) +1

當x[i]!=y[j]時,f(i,j) = max( f(i-1,j) , f(i,j-1) )

一樣的爲保證f()參數合法,當參數小於0時,返回值爲0。

當i<0或者j<0時,f(i,j)=0 //遞歸出口

這不難理解,好比f(0,0)實際上比較的是x[0]和y[0]兩個字符的最長公共子序列,無非是0或者1。

而f(-1,2)中參數-1表示x取一個空串,y取y[0,2]。空串和任何字符串的LCS毫無疑問都是0

2.2 dp矩陣

以x="abcdea",y="aebcda"爲例,構造dp表格以下:

2.3 AC代碼

class LCS {
public:
    int findLCS(string A, int n, string B, int m) {
        vector<vector<int> > dp(n,vector<int>(m,0));
        for(int i=0;i<n;i++)
        {
            for(int j=0;j<m;j++)
            {
                if(A[i]==B[j]){                    
                    int t=0;
                    if(i-1>=0 && j-1>=0) t=dp[i-1][j-1];
                    dp[i][j]=t+1;
                }                    
                else{                    
                    int t1=0;
                    int t2=0;
                    if(i-1>=0) t1=dp[i-1][j];
                    if(j-1>=0) t2=dp[i][j-1];
                    dp[i][j]=max(t1,t2);
                }
            }
        }
        //右下角
        return dp[n-1][m-1];
    }
};

這裏須要考慮的邊界狀況較多,爲簡化代碼,能夠考慮字符串從1開始數

dp[i][j]就表示x[0,i-1]和y[0,j-1]的最長公共子序列

對應遞歸公式[1]:

這樣的dp矩陣,字符串的下標和矩陣的行號列號會有些錯位:

不須要判斷越界問題,代碼精簡,可是很差理解

class LCS {
public:
    int findLCS(string A, int n, string B, int m) {
         //(n+1)*(m+1)
        vector<vector<int> > dp(n+1,vector<int>(m+1,0));       
        //按照dp的index來填充矩陣
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=m;j++)
            {                
                if(A[i-1]==B[j-1]) dp[i][j]=dp[i-1][j-1]+1;
                else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
            }
        }
        return dp[n][m];
    }
};

//或者

class LCS {
public:
    int findLCS(string A, int n, string B, int m) {
        vector<vector<int> > dp(n+1,vector<int>(m+1,0));    
        //按照string的索引來填充dp矩陣
        for(int i=0;i<n;i++)
        {
            for(int j=0;j<m;j++)
            {
                if(A[i]==B[j]) dp[i+1][j+1]=dp[i][j]+1;
                else dp[i+1][j+1]=max(dp[i][j+1],dp[i+1][j]);
            }
        }
        return dp[n][m];
    }
};

3 01揹包問題

問題定義[2]

話說有一哥們去森林裏玩發現了一堆寶石,他數了數,一共有n個。 但他身上能裝寶石的就只有一個揹包,揹包的容量爲C。這哥們把n個寶石排成一排並編上號: 0,1,2,…,n-1。第i個寶石對應的價值和重量分別爲V[i]和W[i] 。排好後這哥們開始思考: 揹包總共也就只能裝下體積爲C的東西,那我要裝下哪些寶石才能讓我得到最大的利益呢?

揹包問題分爲01揹包問題和部分揹包問題,區別在於物品是否不可分割。

這裏的物品不能分割,討論的是01揹包問題。

假設有1,2,3...n一共n個物品。其中v[i]表示第i個物品價值。w[i]表示第i個物品重量。

d(i,j)表示,要把前i個物品放入揹包,揹包容量爲j

  • 1 遞歸公式:d(i,j)=

    • 0 //i is 0
    • max( d(i-1,j) , d(i-1,j-w[i])+v[i] ) //j>=w[i] and i>0
  • 2 列表計算

佔位

#include<iostream>
#include <stdio.h>
#include<vector>
//#include"fsjtools.h"
using  namespace std;
int main()
{
    int n,c;//number,cap
    while(cin>>n>>c)
    {
        vector<int> w(n+1,0);//weight
        vector<int> v(n+1,0);//value
        for(int i=1;i<=n;i++) cin>>w[i]>>v[i];//start from 1
        //printVector(w);
        //printVector(v);
        vector<vector<int> > dp(n+1,vector<int>(c+1,0));//初始化爲0
        for(int i=1;i<=n;i++)
        {
            for(int j=0;j<=c;j++)
            {
                if(j>=w[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]] + v[i]);
								else dp[i][j]=dp[i-1][j];
            }
        }
        cout<<dp[n][c]<<endl;
    }
}
樣例輸入

5 10
4 9
3 6
5 1
2 4
5 1

4 9
4 20
3 6
4 20
2 4

5 10
2 6
2 3
6 5
5 4
4 6

樣例輸出

19
40
15

References

  1. DP:LCS(最長公共子串、最長公共子序列)
  2. 動態規劃之揹包問題(一)
相關文章
相關標籤/搜索