1. 程式人生 > >對動態規劃演算法的簡單理解

對動態規劃演算法的簡單理解

動態規劃演算法通常基於一個遞推公式及一個或多個初始狀態。當前子問題的解將由上一次子問題的解推出。使用動態規劃來解題只需要多項式時間複雜度,因此它比回溯法、暴力法等要快許多。

首先,我們要找到某個狀態的最優解,然後在它的幫助下,找到下一個狀態的最優解,“狀態"用來描述該問題的子問題的解。“狀態”並不是隨便給的,大部分情況下,某個狀態只與它前面出現的狀態有關,而獨立於後面的狀態。然後分析出狀態轉移方程以及初始狀態。

動態規劃經典的問題就是求最長遞增序列長度LIS:longest increasing subsequence

例如: 一個序列有n個數:a[1],a[2],…,a[N],求出最長非降子序列的長度。思路如下:

1、要求n個數的序列中最長非降子序列的長度,如果能先求出a[1]、a[2]、a[i](i<N)的最長非降子序列,那麼上面的問題變成了原問題的一個子問題(問題規模變小了,你可以讓i=1,2,3等來分析) 然後我們定義d(i),表示前i個數中以a[i]結尾的最長非降子序列的長度。這個d(i)就是我們要找的狀態。如果我們把d(1)到d(N)都計算出來,那麼最終我們要找的答案就是這裡面最大的那個。狀態找到了,下一步找出狀態轉移方程。

2、為了方便理解我們是如何找到狀態轉移方程的,我先把下面的例子提到前面來講。如果我們要求的這n個數的序列是:

6,3,4,9,6,8

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

  • 前1個數的LIS長度d(1)=1(序列:6)
  • 前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,9;9前面比它小的有3個數,所以 d(4)=max{d(1),d(2),d(3)}+1=3)

到這裡我們基本可以找到狀態轉移方程了,如果我們已經求出了d(1)到d(i-1),那麼d(i)可以用下面的狀態轉移方程得到:

d(i) = max{1, d(j)+1},其中j<i,a[j]<=a[i],則可寫出如下程式:

private void Longest(int[] a, int n) {
int d[]=new int[n];
int len=1;
d[0]=1;
for(int i=1;i<n;i++){
d[i]=1;
for(int j=0;j<i;j++){
if(a[i]>=a[j]&&d[j]+1>d[i]){
d[i]=d[j]+1;
}
if(len<d[i])len=d[i];
}
}
System.out.println(len);
}

還有另一個簡單的例子,同樣根據動態規劃可簡單求解

/*
* 有3個硬幣面值分別為1,3,5,現要一最小個數地組合湊齊目標面值
* */

private static void Min(int[] v, int n){ //v[]為硬幣面值陣列
int[] min=new int[n+1];  //目標數值為n的最小硬幣個數組合陣列
min[0]=0;
for(int k=1;k<=n;k++){
min[k]=n;      //先賦值為最大
}
for(int i=1;i<=n;i++){   //目標數求1到n的最小硬幣數
for(int j=0;j<v.length;j++){
if(v[j]<=i&&min[i-v[j]]+1<min[i])   //目標值要大於等於硬幣面值
min[i]=min[i-v[j]]+1;       //狀態轉移函式
}
}
System.out.println(min[n]);
}

難點主要在於確定狀態以及找到狀態轉移方程~一般通過對問題的分解找到子問題並求解,找到“規律”,還是要通過練習慢慢加深理解的。