演算法筆記之動態規劃(4)
用動態分析解決0-1揹包問題
有n個物品,每個物品的重量為w[i],價值為v[i],購物車容量為W。選若干個物品放入購物車,在不超過容量的前提下使獲得的價值最大。
問題分析
(1)分析最優解的結構特徵
(2)建立具有最優值的遞迴式
可以對每個物品依次檢查是否放入或者不放入,對於第i個物品的處理狀態:用ci表示前i件物品放入一個容量為j的購物車可以獲得的最大價值。
- 不放入第i件物品,xi=0,裝入購物車的價值不增加。那麼問題就轉化為“前i-1件物品放入容量為j的揹包中”,最大價值為ci-1。
- 放入第i件物品,xi=1,裝入購物車的價值增加vi。
那麼問題就轉化為了“前i-1件物品放入容量為j-w[i]的購物車中”,此時能獲得的最大價值就是ci-1],再加上放入第i件物品獲得的價值v[i]。即ci-1]+v[i]。
購物車容量不足,肯定不能放入;購物車容量組,我們要看放入、不放入哪種情況獲得的價值更大。
所以,遞迴函式可以寫為:
ci=ci-1(當ji);
ci=max{ci-1]+v[i],ci-1}(當j>wi)
演算法設計
(1)確定合適的資料結構
採用一維陣列w[i]、v[i]分別記錄第i個物品的重量和價值;二維陣列用ci表示前i個物品放入一個容量為j的購物車可以獲得的最大價值。
(2)初始化
初始化c[][]陣列0行0列為0,其中i=01,2,…,n,j=0,1,2,…,W。
(3)迴圈階段
- 按照遞迴式計算第1個物品的處理情況,得到c1,j=1,2,…,W;
- 按照遞迴式計算第2個物品的處理情況,得到c2,j=1,2,…,W;
- 以此類推,按照遞迴式計算第n個物品的處理情況,得到cn,j=1,2,…,W。
(4)構造最優解
cn就是不超過購物車容量能放入物品的最大價值。如果還想知道具體放入了哪些物品,就需要根據c[][]陣列逆向構造最優解,我們可以用一維陣列x[i]來儲存解向量。
- 首先i=n,j=W,如果ci>ci-1,則說明第n個物品放入了購物車,令x[n]=1,j-=w[n];如果ci≤ci-1,則說明第n個物品沒有放入購物車,令x[n]=0.
- i–,繼續查詢答案。
- 直到i=1處理完畢。
圖解
假設現在有5個物品,每個物品重量為(2,5,4,2,3),價值為(6,3,5,4,6),購物車容量為10。
c[][]如下表:
c[][] | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
2 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 9 | 9 | 9 | 9 |
3 | 0 | 0 | 6 | 6 | 6 | 6 | 11 | 11 | 11 | 11 | 11 |
4 | 0 | 0 | 6 | 6 | 10 | 10 | 11 | 11 | 15 | 15 | 15 |
5 | 0 | 0 | 6 | 6 | 10 | 12 | 12 | 16 | 16 | 17 | 17 |
所以最大價值為cn=17。
首先讀取c5>c4,說明第5個物品裝入了購物車,即x[5]=1,然後j=10-w[5]=10-3=7
然後去c4;
c4=c3,說明第4個物品沒有裝入購物車,即x[4]=0;
然後去找c3,依次類推。
程式碼實現
#include <iostream>
#include<cstring>
#include<algorithm>
using namespace std;
#define maxn 10000
#define M 105
int c[M][maxn];//c[i][j] 表示前i個物品放入容量為j購物車獲得的最大價值
int w[M],v[M];//w[i] 表示第i個物品的重量,v[i] 表示第i個物品的價值
int x[M]; //x[i]表示第i個物品是否放入購物車
int main(){
int i,j,n,W;//n表示n個物品,W表示購物車的容量
cout << "請輸入物品的個數 n:";
cin >> n;
cout << "請輸入購物車的容量W:";
cin >> W;
cout << "請依次輸入每個物品的重量w和價值v,用空格分開:";
for(i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(i=1;i<=n;i++)//初始化第0列為0
c[i][0]=0;
for(j=1;j<=W;j++)//初始化第0行為0
c[0][j]=0;
for(i=1;i<= n;i++)//計算c[i][j]
for(j=1;j<=W;j++)
if(j<w[i]) //當物品的重量大於購物車的容量,則不放此物品
c[i][j] = c[i-1][j];
else //否則比較此物品放與不放是否能使得購物車內的價值最大
c[i][j] = max(c[i-1][j],c[i-1][j-w[i]] + v[i]);
cout<<"裝入購物車的最大價值為:"<<c[n][W]<<endl;
//用於測試
for (i=1; i<=n; i++ )
{
for (j=1; j<=W; j++ )
cout << c[i][j]<<" " ;
cout << endl;
}
cout << endl;
//逆向構造最優解
j=W;
for(i=n;i>0;i--)
if(c[i][j]>c[i-1][j])
{
x[i]=1;
j-=w[i];
}
else
x[i]=0;
cout<<"裝入購物車的物品序號為:";
for(i=1;i<=n;i++)
if(x[i]==1)
cout<<i<<" ";
return 0;
}
演算法分析及改進
1.演算法複雜度分析
(1)時間複雜度:O(n*W)
(2)空間複雜度O(n*W)
2.演算法優化改進
使用一個陣列dp[]保證第i次迴圈結束後dp[j]中表示的就是我們定義的ci。
所以程式碼如下:
void opt1(int n,int W)
{
for(i=1;i<=n;i++)
for(j=W;j>0;j--)
if(j>=w[i]) //當物品的重量大於購物車的容量,比較此物品放與不放是否能使得購物車內的價值最大
dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
}
我們可以縮小範圍,因為只有當購物車的容量大於等於物品重量的時候才要更新,所以程式碼如下:
void opt2(int n,int W)
{
for(i=1;i<= n;i++)
for(j=W;j>=w[i];j--)
//當物品的重量大於購物車的容量
//比較此物品放與不放是否能使得購物車內的價值最大
dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
}
我們還可以再縮小範圍,確定搜尋的下界bound
void opt3(int n,int W)
{
int sum[n];//sum[i]表示從1...i的物品重量之和
sum[0]=0;
for(i=1;i<=n;i++)
sum[i]=sum[i-1]+w[i];
for(i=1;i<=n;i++)
{
int bound=max(w[i],W-(sum[n]-sum[i-1]));
//w[i]與剩餘容量取最大值,
//sum[n]-sum[i-1]表示從i...n的物品重量之和
for(j=W;j>=bound;j--)
//當物品的重量大於購物車的容量
//比較此物品放與不放是否能使得購物車內的價值最大
dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
}
}
快速定位—最優二叉搜尋樹(OBST)
問題分析
遞迴表示式:
ci=0(j=i-1);
ci=min{ci+ck+1}+wi(j≥i)
wi=qi-1(j=i-1);
wi=wi+pj+qj
演算法設計
(1)確定合適的資料結構
一維陣列:p[]、q[]分別表示實結點和虛結點的搜尋概率
二維陣列:ci表示最優二叉搜尋樹T(i,j)的搜尋成本,wi表示最優二叉搜尋樹T(i,j)中的所有實結點和虛結點的搜尋概率之和,si表示最優二叉搜尋樹T(i,j)的根節點序號。
(2)初始化。ci=0.0,wi=q[i-1],其中i=1,2,3,…,n+1。
(3)迴圈階段。
- 按照遞迴式計算元素規模是1的{si}(j=i)的最優二叉搜尋樹的搜尋成本ci,並記錄最優策略,即樹根si,i=1,2,3,..,n。
- 按照遞迴式計算元素規模是2的{si,si+1}(j=i)的最優二叉搜尋樹的搜尋成本ci,並記錄最優策略,即樹根si,i=1,2,3,..,n-1。
- 以此類推,直到求出所有元素{s1,s2,…,sn}的最優二叉搜尋樹的搜尋成本c1和最優策略s1。
(4)構造最優解。
- 首先讀取s1,令k=s1,輸出sk為最優二叉搜尋樹的根。
- 判斷如果k-1<1,表示虛結點ek-1是sk的左子樹;否則,遞迴求解左子樹Construct_Optimal_BST(1,k-1,1)。
- 判斷如果k≥n,表示虛結點ek是sk的右孩子。;否則,輸出sk+1是sk的右孩子,遞迴求解右子樹Construct_Optimal_BST(k+1,n,1)。
w[][] | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
1 | 0.06 | 0.18 | 0.37 | 0.52 | 0.59 | 0.76 | 1.00 |
2 | 0.08 | 0.27 | 0.42 | 0.49 | 0.66 | 0.90 | |
3 | 0.10 | 0.25 | 0.32 | 0.49 | 0.73 | ||
4 | 0.07 | 0.14 | 0.31 | 0.55 | |||
5 | 0.05 | 0.22 | 0.46 | ||||
6 | 0.05 | 0.29 | |||||
7 | 0.10 |
c[][] | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
1 | 0 | 0.18 | 0.55 | 0.95 | 1.23 | 1.76 | 2.52 |
2 | 0 | 0.27 | 0.67 | 0.90 | 1.38 | 2.09 | |
3 | 0 | 0.25 | 0.46 | 0.94 | 1.48 | ||
4 | 0 | 0.14 | 0.45 | 0.98 | |||
5 | 0 | 0.22 | 0.68 | ||||
6 | 0 | 0.29 | |||||
7 | 0 |
s[][] | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
1 | 1 | 2 | 2 | 2 | 3 | 5 | |
2 | 2 | 2 | 3 | 3 | 5 | ||
3 | 3 | 3 | 3 | 5 | |||
4 | 4 | 5 | 5 | ||||
5 | 5 | 6 | |||||
6 | 6 | ||||||
7 |
程式碼實現
void Optimal_BST()
{
for(i=1;i<=n+1;i++)
{
c[i][i-1]=0.0;
w[i][i-1]=q[i-1];
}
for(int t=1;t<=n;t++)//t為關鍵字的規模
//從下標為i開始的關鍵字到下標為j的關鍵字
for(i=1;i<=n-t+1;i++)
{
j=i+t-1;
w[i][j]=w[i][j-1]+p[j]+q[j];
c[i][j]=c[i][i-1]+c[i+1][j];//初始化
s[i][j]=i;//初始化
//選取i+1到j之間的某個下標的關鍵字作為
//從i到j的根,如果組成的樹的期望值當前最小
//則k為從i到j的根節點
for(k=i+1;k<=j;k++)
{
double temp=c[i][k-1]+c[k+1][j];
if(temp<c[i][j]&&fabs(temp-c[i][j])>1E-6)
//C++中浮點數因為精度問題不可以直接比較
{
c[i][j]=temp;
s[i][j]=k;//k即為從下標i到j的根節點
}
}
c[i][j]+=w[i][j];
}
}
void Construct_Optimal_BST(int i,int j,bool flag)
{
if(flag==0)
{
cout<<"S"<<s[i][j]<<" 是根"<<endl;
flag=1;
}
int k=s[i][j];
//如果左子樹是葉子
if(k-1<i)
{
cout<<"e"<<k-1<<" is the left child of "<<"S"<<k<<endl;
}
//如果左子樹不是葉子
else
{
cout<<"S"<<s[i][k-1]<<" is the left child of "<<"S"<<k<<endl;
Construct_Optimal_BST(i,k-1,1);
}
//如果右子樹是葉子
if(k>=j)
{
cout<<"e"<<j<<" is the right child of "<<"S"<<k<<endl;
}
//如果右子樹不是葉子
else
{
cout<<"S"<<s[k+1][j]<<" is the right child of "<<"S"<<k<<endl;
Construct_Optimal_BST(k+1,j,1);
}
}
複雜度分析及改進
時間複雜度為O(n3),空間複雜度為O(n2)。
又可以用四邊形不等式優化(後續研究一下)
時間複雜度減少到O(n2)。
void Optimal_BST()
{
for(i=1;i<=n+1;i++)
{
c[i][i-1]=0.0;
w[i][i-1]=q[i-1];
}
for(int t=1;t<=n;t++)//t為關鍵字的規模
//從下標為i開始的關鍵字到下標為j的關鍵字
for(i=1;i<=n-t+1;i++)
{
j=i+t-1;
w[i][j]=w[i][j-1]+p[j]+q[j];
int i1=s[i][j-1]>i?s[i][j-1]:i;
int j1=s[i+1][j]<j?s[i+1][j]:j;
c[i][j]=c[i][i1-1]+c[i1+1][j];//初始化
s[i][j]=i1;//初始化
//選取i1+1到j1之間的某個下標的關鍵字
//作為從i到j的根,如果組成的樹的期望值當前
//最小,則k為從i到j的根節點
for(k=i1+1;k<=j1;k++)
{
double temp=c[i][k-1]+c[k+1][j];
if(temp<c[i][j]&&fabs(temp-c[i][j])>1E-6)
//C++中浮點數因為精度問題不可以直接比較
{
c[i][j]=temp;
s[i][j]=k;//k即為從下標i到j的根節點
}
}
c[i][j]+=w[i][j];
}
}
void Construct_Optimal_BST(int i,int j,bool flag)
{
if(flag==0)
{
cout<<"S"<<s[i][j]<<" 是根"<<endl;
flag=1;
}
int k=s[i][j];
//如果左子樹是葉子
if(k-1<i)
{
cout<<"e"<<k-1<<" is the left child of "<<"S"<<k<<endl;
}
//如果左子樹不是葉子
else
{
cout<<"S"<<s[i][k-1]<<" is the left child of "<<"S"<<k<<endl;
Construct_Optimal_BST(i,k-1,1);
}
//如果右子樹是葉子
if(k>=j)
{
cout<<"e"<<j<<" is the right child of "<<"S"<<k<<endl;
}
//如果右子樹不是葉子
else
{
cout<<"S"<<s[k+1][j]<<" is the right child of "<<"S"<<k<<endl;
Construct_Optimal_BST(k+1,j,1);
}
}
動態規劃演算法總結
動態規劃關鍵總結如下:
-
最優子結構判定
- 做出一個選擇。
- 假定已經知道了哪種選擇是最優的。
- 最優的會產生哪些子問題。
- 證明原問題的最優解包含其子問題的最優解。
-
如何得到最優解遞迴式
- 分析原問題最優解和子問題最優解的關係。
- 考察有多少種選擇。
- 得到最優解遞迴式。
相關文章
- 動態規劃學習筆記動態規劃筆記
- Leetcode 題解演算法之動態規劃LeetCode演算法動態規劃
- 動態規劃之 KMP 演算法詳解動態規劃KMP演算法
- 演算法-動態規劃演算法動態規劃
- 演算法_動態規劃演算法動態規劃
- 動態規劃演算法動態規劃演算法
- 演算法系列-動態規劃(1):初識動態規劃演算法動態規劃
- 前端演算法 - 動態規劃前端演算法動態規劃
- 動態規劃之數的劃分動態規劃
- 初級演算法-動態規劃演算法動態規劃
- 【每日演算法】動態規劃四演算法動態規劃
- 《演算法筆記》12. 用暴力遞迴解法推匯出動態規劃演算法筆記遞迴動態規劃
- 常用演算法思想之動態規劃的字尾思想演算法動態規劃
- 做題記錄 --- 動態規劃動態規劃
- 演算法(七):圖解動態規劃演算法圖解動態規劃
- 演算法-動態規劃-完全揹包演算法動態規劃
- 動態規劃動態規劃
- 動態規劃之股票問題123動態規劃
- 演算法---貪心演算法和動態規劃演算法動態規劃
- AI學習筆記——強化學習之動態規劃(Dynamic Programming)解決MDP(1)AI筆記強化學習動態規劃
- 矩陣連乘(動態規劃演算法)矩陣動態規劃演算法
- 動態規劃演算法原理與實踐動態規劃演算法
- 動態規劃演算法(DP)學習<1>動態規劃演算法
- 演算法系列-動態規劃(4):買賣股票的最佳時機演算法動態規劃
- [leetcode] 動態規劃(Ⅰ)LeetCode動態規劃
- 動態規劃法動態規劃
- 模板 - 動態規劃動態規劃
- 動態規劃初步動態規劃
- 動態規劃分析動態規劃
- 動態規劃(DP)動態規劃
- CSP之壓縮編碼(動態規劃)動態規劃
- LeetCode入門指南 之 動態規劃思想LeetCode動態規劃
- 動態規劃系列之九找零錢動態規劃
- Java演算法之動態規劃詳解-買賣股票最佳時機Java演算法動態規劃
- Python <演算法思想集結>之抽絲剝繭聊動態規劃Python演算法動態規劃
- [leetcode初級演算法]動態規劃總結LeetCode演算法動態規劃
- 演算法基礎--遞迴和動態規劃演算法遞迴動態規劃
- 乾貨:圖解演算法——動態規劃系列圖解演算法動態規劃