01揹包
思路剖析
1.當我們第一次看到題目的時候,很容易想到運用貪心的思想,每次拿價效比最大的物品。
容易舉出反例:
3 7
6 10 //1.666...
5 8 // 1.6
2 3 //1.5
2.接著我們很自然的就可以想到,既然運用貪心的思想不行,那麼就列舉所有選取物品的情況,也就是組合。
於是我們就可以定義狀態
\(dfs(i,v,ans)\) 表示我搜尋到了第 \(i\) 個物品,此時已經選的物品的總體積為 \(v\) , 所得到的價值為 \(ans\) 。
當我們將 \(N\) 個物品全都搜完後,若 \(v \le M\) ,則更新我們的答案。
在這個過程中,我們可以發現 , 對於一個狀態 \(i,v,ans\) ,它可以從 \(i-1,v,ans\) 或 \(i-1,v-v_i,ans-w_i\) 轉移過來(分別對應了我選不選第 \(i\) 個物品的情況),但由於他所剩的物品個數一樣,已經選的體積一樣,所以儘管從兩個不同的狀態轉移過來,所得的價值不同,但是之後能夠遍歷到的情況都是相同的。
顯然有所得價值更大的狀態,得到的結果優於價值更小的狀態。
這就是最優子結構性質,正因為這個過程具有最優子結構性質,我們在進行下一階段計算的時候,就免去了絕對不可能是最優的答案的多餘的計算,所以我們可以通過他將搜尋轉化為DP,當然,這過程中同時具無後效性原則和重疊子問題。
簡單解釋一下重疊子問題優化的部分。當我已經知道狀態 \(dp_{i,v}\) 所能獲得的最大價值時,我將它記錄下來,之後再遍歷到這個狀態,我直接就能返回我的最優解了,而不用繼續搜下去,重新求解。這就要求了我們DP的問題具有該性質,否則就等同於沒有記錄,也就沒有優化一說了。
無後效性則保證了我求出來的最優解不會被後面所影響,一定是最優的。
3.簡單總結一下
狀態: \(dp_{i,j}\) 表示以在前 \(i\) 個物品中,選取體積為 \(j\) 的物品,得到的最大價值。
狀態轉移方程:
\(dp[i][j] = \max(dp[i-1][j],dp[i-1][j-v_i]+w_i)\)
code
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
int n,m,v[3500],w[3500],dp[12885];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d%d",&v[i],&w[i]);
memset(dp,0xcf,sizeof(dp));//賦值為-INF,表示該狀態不可達,這裡要特別注意,不能賦值為0,如果賦值為0的話,則表示容積為j的揹包所能獲得的最大價值,與我們所定義的狀態相悖。
dp[0]=0;
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
}
}
int ans=0;
for(int i=1;i<=m;i++) ans=max(ans,dp[i]);//找到最佳答案。
cout<<ans<<endl;
return 0;
}
唔,當然,如若是01揹包的話,用 \(dp_{i,j}\) 表示前 \(i\) 個物品中,用容積為 \(j\) 的揹包所能獲得的最大價值也是ok的,但如果題目要我們恰好要把容積為 \(M\) 的揹包裝滿,這個狀態所得到的答案就不一定正確了。
我給出的程式碼是已經優化過的版本,具體過程相信很多大佬都比我講得好,這裡主要分享我的一個思路,希望可以幫助自己鞏固複習,也可以幫到大家理解。
$ \text{-END-}$
\(2022.7.6\)