一.概述
動態規劃的基本思想:若要解一個給定問題,我們需要解其不同部分(即子問題),再合併子問題的解以得出原問題的解。 通常許多子問題非常相似,爲此動態規劃法試圖僅僅解決每個子問題一次,從而減少計算量: 一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次需要同一個子問題解之時直接查表。 這種做法在重複子問題的數目關於輸入的規模呈指數增長時特別有用。
三大重要性質:
最優子結構性質:如果問題的最優解所包含的子問題的解也是最優的,我們就稱該問題具有最優子結構性質(即滿足最優化原理)。最優子結構性質爲動態規劃算法解決問題提供了重要線索。
子問題重疊性質:子問題重疊性質是指在用遞歸算法自頂向下對問題進行求解時,每次產生的子問題並不總是新問題,有些子問題會被重複計算多次。動態規劃算法正是利用了這種子問題的重疊性質,對每一個子問題只計算一次,然後將其計算結果保存在一個表格中,當再次需要計算已經計算過的子問題時,只是在表格中簡單地查看一下結果,從而獲得較高的效率。
無後效性:將各階段按照一定的次序排列好之後,對於某個給定的階段狀態,它以前各階段的狀態無法直接影響它未來的決策,而只能通過當前的這個狀態。換句話說,每個狀態都是過去歷史的一個完整總結。這就是無後向性,又稱爲無後效性。
二.分類解析
1.最長遞增子序列.
例題:
題意:給出序列a [1], a [2], a [3] ...... a [n],計算子序列的最大總和。
思路:最大子序列是要找出由數組成的一維數組中和最大的連續子序列。方法是:只要前i項和還沒有小於0子序列就一直往後擴展,否則丟棄之前的子序列開始新的子序列,同時記錄各個子序列的和,最後取他們中的最大值。
代碼:
#include<stdio.h>#include<iostream>
using
namespace std;
int main()
{
int i,ca=1,t,s,e,n,x,now,before,max;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
for(i=1;i<=n;i++)
{
scanf("%d",&now);
if(i==1)
{
max=before=now;
x=s=e=1;
}
else {
if(now>now+before)
{
before=now;
x=i;
}
else before+=now;
}
if(before>max)
max=before,s=x,e=i;
}
printf("Case %d:\n%d %d %d\n",ca++,max,s,e);
if(t)printf("\n");
}
return 0;
}
2.最長公共子序列.
例題:
題意:求兩個字符串的最長公共子序列。
思路:動態的方程在第一個元素的相等的時,dp[0][0] = dp[-1][-1] + 1, 天哪,這肯定就會出錯了。在處理時可以選擇字符的讀取從第一個位置開始,或者把 i 號字符的狀態存儲到i+1號位置去,這樣就從1號開始處理了,判定是就是 s1[i-1] == s1[j-1] ?
代碼:
#include<iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio.h>
#define Max( a, b ) (a) > (b) ? (a) : (b)
using namespace std;
char s1[1005], s2[1005];
int dp[1005][1005];
int main()
{
int len1, len2;
while( scanf( "%s %s", s1+1, s2+1 ) != EOF )
{
memset( dp, 0, sizeof(dp) );
len1 = strlen( s1+1 ), len2 = strlen( s2+1 );
for( int i = 1; i <= len1; ++i )
{
for( int j = 1; j <= len2; ++j )
{
if( s1[i] == s2[j] )
{
dp[i][j] = dp[i-1][j-1] + 1;
}
else
{
dp[i][j] = Max ( dp[i-1][j], dp[i][j-1] );
}
}
}
printf( "%d\n", dp[len1][len2] );
}
return 0;
}
<1>01揹包.
例題:
題意:一個人收集骨頭。給出他的揹包容量和可選的骨頭的體積和價值,輸出他的揹包能裝下的骨頭的最大價值。
思路:01揹包問題,DP公式都類似:F[i;v] = maxfF[i-1;v];F[i-1;v-Ci] + Wi,由這個公式做變形就可以。下面再來分析一下這個公式:
每種骨頭僅有一件,可以選擇放或不放。用子問題定義狀態:即F[i;v] 表示前i 件物品恰放入一個容量爲v的揹包可以獲得的最大價值。“將前i 個骨頭放入容量爲v的揹包中”這個子問題,若只考慮第i 個骨頭的策略(放或不放),那麼就可以轉化爲一個只和前i-1個骨頭相關的問題。如果不放第i 個骨頭,那麼問題就轉化爲“前i-1個骨頭放入容量爲v的揹包中”,價值爲F[i-1; v];如果放第i 個骨頭,那麼問題就轉化爲“前i-1個骨頭放入剩下的容量爲v-Ci 的揹包中”,此時能獲得的最大價值就是F[i-1;v-Ci] 再加上通過放入第i 個骨頭獲得的價值Wi。代碼;
#include<iostream>
#include<stdio.h>
#include<string>
#define M 1009
using namespace std;
typedef struct pack
{
int cost;
int val;
}PACK;
int f[M][M];
int main()
{
int cas,n,v,i,j;
PACK a[M];
scanf("%d",&cas);
while(cas--)
{
scanf("%d%d",&n,&v);
memset(f,0,sizeof(f));
for(i=1;i<=n;i++)
scanf("%d",&a[i].val);
for(i=1;i<=n;i++)
scanf("%d",&a[i].cost);
for(i=1;i<=n;i++)
for(j=0;j<=v;j++)
if(j-a[i].cost>=0&&f[i-1][j]<f[i-1][j-a[i].cost]+a[i].val)
f[i][j]=f[i-1][j-a[i].cost]+a[i].val;
else
f[i][j]=f[i-1][j];
printf("%d\n",f[n][v]);
}
return 0;
}
<2>多重揹包.(沒做出來題,只列下思路。)
題意:有N種物品和一個容量爲V的揹包。第i種物品最多有n[i]件可用,每件費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。
基本算法:這題目和完全揹包問題很類似。基本的方程只需將完全揹包問題的方程略微一改即可,因爲對於第i種物品有n[i]+1種策略:取0件,取1件……取n[i]件。令f[i][v]表示前i種物品恰放入一個容量爲v的揹包的最大權值,則有狀態轉移方程:f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=n[i]}複雜度是O(V*Σn[i])。
<3>完全揹包.(依然是隻給出基本思路)
題意:有N種物品和一個容量爲V的揹包,每種物品都有無限件可用。第i種物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。
基本思路:這個問題非常類似於01揹包問題,所不同的是每種物品有無限件。也就是從每種物品的角度考慮,與它相關的策略已並非取或不取兩種,而是有取0件、取1件、取2件……等很多種。如果仍然按照解01揹包時的思路,令f[i][v]表示前i種物品恰放入一個容量爲v的揹包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程,像這樣:
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}
這跟01揹包問題一樣有O(VN)個狀態需要求解,但求解每個狀態的時間已經不是常數了,求解狀態f[i][v]的時間是O(v/c[i]),總的複雜度可以認爲是O(V*Σ(V/c[i])),是比較大的。
總結解題一般步驟:(1)建立模型,確認狀態。(2)找出狀態轉移方程。(3)找出初始條件。