1. 程式人生 > >動態規劃的經典例子

動態規劃的經典例子

最長不減子序列

一個序列有N個數:A[1],A[2],…,A[N],求出最長非降子序列的長度。 (講DP基本都會講到的一個問題LIS:longest increasing subsequence)

正如上面我們講的,面對這樣一個問題,我們首先要定義一個“狀態”來代表它的子問題, 並且找到它的解。注意,大部分情況下,某個狀態只與它前面出現的狀態有關, 而獨立於後面的狀態。

5,3,4,8,6,7

根據上面找到的狀態,我們可以得到:(下文的最長非降子序列都用LIS表示)

  • 前1個數的LIS長度d(1)=1(序列:5)
  • 前2個數的LIS長度d(2)=1(序列:3;3前面沒有比3小的)
  • 前3個數的LIS長度d(3)=2(序列:3,4;4前面有個比它小的3,所以d(3)=d(2)+1)
  • 前4個數的LIS長度d(4)=3(序列:3,4,8;8前面比它小的有3個數,所以 d(4)=max{d(1),d(2),d(3)}+1=3)

狀態轉換分成為:

d(i) = max{1, d(j)+1},其中j

#include <iostream>

using namespace std;

int lis(int A[], int n){
    int *d = new int[n];
    int len = 1;
    for(int i=0; i<n; ++i){
        d[i] = 1
; for(int j=0; j<i; ++j) if(A[j]<=A[i] && d[j]+1>d[i]) d[i] = d[j] + 1; if(d[i]>len) len = d[i]; } delete[] d; return len; } int main(){ int A[] = { 5, 3, 4, 8, 6, 7 }; cout<<lis(A, 6)<<endl; return
0; }

以上是O(n2)的演算法,還有O(nlogn)的實現如下:

//在非遞減序列 arr[s..e](閉區間)上二分查詢第一個大於等於key的位置,如果都小於key,就返回e+1
int upper_bound(int arr[], int s, int e, int key)
{
    int mid;
    if (arr[e] <= key)
        return e + 1;
    while (s < e)
    {
        mid = s + (e - s) / 2;
        if (arr[mid] <= key)
            s = mid + 1;
        else
            e = mid;
    }
    return s;
}

int LIS(int d[], int n)
{
    int i = 0, len = 1, *end = (int *)alloca(sizeof(int) * (n + 1));
    end[1] = d[0]; //初始化:長度為1的LIS末尾為d[0]
    for (i = 1; i < n; i++)
    {
        int pos = upper_bound(end, 1, len, d[i]); //找到插入位置
        end[pos] = d[i];
        if (len < pos) //按需要更新LIS長度
            len = pos;
    }
    return len;
}

參考:

收集最多的蘋果

平面上有N*M個格子,每個格子中放著一定數量的蘋果。你從左上角的格子開始, 每一步只能向下走或是向右走,每次走到一個格子上就把格子裡的蘋果收集起來, 這樣下去,你最多能收集到多少個蘋果。

解這個問題與解其它的DP問題幾乎沒有什麼兩樣。第一步找到問題的“狀態”, 第二步找到“狀態轉移方程”,然後基本上問題就解決了。

首先,我們要找到這個問題中的“狀態”是什麼?我們必須注意到的一點是, 到達一個格子的方式最多隻有兩種:從左邊來的(除了第一列)和從上邊來的(除了第一行)。 因此為了求出到達當前格子後最多能收集到多少個蘋果, 我們就要先去考察那些能到達當前這個格子的格子,到達它們最多能收集到多少個蘋果。 (是不是有點繞,但這句話的本質其實是DP的關鍵:欲求問題的解,先要去求子問題的解)

經過上面的分析,很容易可以得出問題的狀態和狀態轉移方程。 狀態S[i][j]表示我們走到(i, j)這個格子時,最多能收集到多少個蘋果。那麼, 狀態轉移方程如下:

s[i][j]=A[i][j]+max(S[i1][j],if i > 0;S[i][j1],if j > 0)

其中i代表行,j代表列,下標均從0開始;A[i][j]代表格子(i, j)處的蘋果數量。

S[i][j]有兩種計算方式:1.對於每一行,從左向右計算,然後從上到下逐行處理;2. 對於每一列,從上到下計算,然後從左向右逐列處理。 這樣做的目的是為了在計算S[i][j]時,S[i-1][j]和S[i][j-1]都已經計算出來了。

虛擬碼如下:

#include <iostream>  

using namespace std;  

#define MAX_N 100  
#define MAX_M 100  

int arr[ MAX_N ][ MAX_M ] = { 0 };  
int dp[ MAX_N + 2 ][ MAX_M + 2 ] = { 0 };   

int main(){  
    int n, m;  
    cin>> n >> m;  //輸入n*m的方格  
    int i, j ;  
    for( i = 0; i < n; i++){  
        for( j = 0; j < m; j++)  
            cin>> arr[ i ][ j ];  
    } //輸入方格中各個格子中的蘋果數量  
    for( i = 0; i < n; i++){  
        for( j = 0; j < m; j++){  
            dp[ i + 1 ][ j + 1 ] = dp[ i ][ j + 1] > dp[ i + 1 ][ j ] ?   
                        dp[ i ][ j + 1] + arr[ i ][ j ] : dp[ i + 1][ j ] + arr[ i ][ j ];   
        }  
    }   
    cout<<dp[i][j]<<endl;  

    return 0;  
}  

01揹包問題

問題描述

有編號分別為a,b,c,d,e的五件物品,它們的重量分別是2,2,6,5,4,它們的價值分別是6,3,5,4,6,現在給你個承重為10的揹包,如何讓揹包裡裝入的物品具有最大的價值總和?

問題分析

01揹包的狀態轉換方程 f[i,j] = Max{ f[i-1,j-Wi]+Pi( j >= Wi ), f[i-1,j] }

程式碼實現

 #include<stdio.h>  
 #include<iostream>  
 using namespace std;  

 int table[10][100]={0};  
 int tableTwo[10][100];  
 int flag[10]={-1};   


 int Knapsack(int v[],int w[],int c,int n){//value weight capacity num   
    for(int i=1;i<n+1;i++){//因為涉及到i-1的計算,所以下標從1開始   
        for(int j=1;j<c+1;j++){  
            if(j<w[i]){  
                table[i][j]=table[i-1][j];  
                //flag[i]=0;  
            }else{  
                table[i][j]=max(table[i-1][j],table[i-1][j-w[i]]+v[i]);  
                //flag[i]=table[i-1][j]>(table[i-1][j-w[i]]+v[i])?0:1;  
            }  
        }  
    }  
    return table[n][c];  
 }  

 int KnapsackTwo(int v[],int w[],int c,int n){//此方法從n->1計算 。自底向上,自左向右   
    int jMax=min(w[n]-1,c);   
    for(int j=0;j<jMax;j++)tableTwo[n][j]=0;//j<當前揹包容量或者當前物品重量時,tableTwo[n][j]=0;   
    for(int j=w[n];j<=c;j++)tableTwo[n][j]=v[n];//當前揹包容量可以裝得下時, tableTwo[n][j]=v[n];  
    for(int i=n-1;i>1;i--){  
        jMax=min(w[i],c);  
        for(int j=0;j<=jMax;j++)tableTwo[i][j]=tableTwo[i+1][j];  
        for(int j=w[i];j<=c;j++)tableTwo[i][j]=max(tableTwo[i+1][j],tableTwo[i+1][j-w[i]]+v[i]);//當前揹包容量裝得下,但是要判斷其價值是否最大,確定到底裝不裝   
    }  
    tableTwo[1][c]=tableTwo[2][c];//先假設1物品不裝   
    if(c>=w[1])tableTwo[1][c]=max(tableTwo[1][c],tableTwo[2][c-w[1]]+v[1]);//根據價值,判斷到底裝不裝   
    return tableTwo[1][c];//返回最優值   
  }  
 void Traceback(int w[],int c,int n){//根據最優值,求最優解   
    for(int i=1;i<n;i++){  
        if(tableTwo[i][c]==tableTwo[i+1][c])flag[i]=0;  
        else {  
            flag[i]=1;  
            c-=w[i];      
        }  
    }  
    flag[n]=tableTwo[n][c]?1:0;  
 }  

 int main(){  
    int weight[6]={0,2,2,6,5,4};//最低位補了0,從weight[1]開始賦值   
    int value[6]={0,6,3,5,4,6};  
    int c=10;  
    cout<<"第一種方法->總價值最大為:"<<Knapsack(value,weight,c,5)<<endl;  
    cout<<"第二種方法->總價值最大為:"<<KnapsackTwo(value,weight,c,5)<<endl;  
    Traceback(weight,c,5);  
    cout<<"最優值的解:";   
    for(int i=1;i<5+1;i++)cout<<flag[i]<<" ";  
    cout<<endl;  
    for(int i=1;i<6;i++){  
        for(int j=0;j<11;j++){  
            printf("%2d ",tableTwo[i][j]);  
        }  
        cout<<endl;  
    }  
    return 0;  

 }  
 /* 
第一種方法->總價值最大為:15 
第二種方法->總價值最大為:15 
最優值的解:1 1 0 0 1 
 0  0  0  0  0  0  0  0  0  0 15 
 0  0  3  3  6  6  9  9  9 10 11 
 0  0  0  0  6  6  6  6  6 10 11 
 0  0  0  0  6  6  6  6  6 10 10 
 0  0  0  0  6  6  6  6  6  6  6 
 */