前言
何謂 DP 的空間優化呢?直接表述顯得抽象,不如從一個典型例題說起。
題目描述
有一個箱子容量爲V(正整數,0≤V≤20000),同時有n個物品(0<n≤30,每個物品有一個體積(正整數)。
要求n個物品中,任取若干個裝入箱內,使箱子的剩餘空間爲最小。
輸入格式
1個整數,表示箱子容量
1個整數,表示有n個物品
接下來n行,分別表示這n個物品的各自體積
輸出格式
1個整數,表示箱子剩餘空間。
輸入輸出樣例
輸入 #1
24
6
8
3
12
7
9
7
輸出 #1
0
題解
題目中要求求剩餘最小空間,那麼其實就是求在不超過規定空間的條件下存放的最大空間,之後用 v 減去最大空間就是剩餘的最小空間。
題目明顯是 01 揹包問題,其中一個物品的大小既是限制也是經典 01 揹包問題中的“價值”。
那麼容易設出 DP 狀態即 dp[i][j] 代表在不超過 j 空間的條件下,從前 i 個物品中選出若干個放入箱子,使得體積之和最大的體積值。轉移方程也容易寫,即 dp[i][j] = max(dp[i-1][j-ds[i]] + ds[i],dp[i-1][j])。(其中 ds[i] 代表第 i 件物品的重量)
代碼如下:
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int MAX_N = 30 + 10;
const int MAX_V = 20000 + 10;
int v,n;
int ds[MAX_N];
int dp[MAX_N][MAX_V]; //代表使用 i 以及之前的箱子的最小剩餘空間
void init()
{
scanf("%d\n%d",&v,&n);
for(int i = 1;i <= n;i++)
scanf("%d",&ds[i]);
}
//用遞推 dp 得出答案,dp[n] 即是答案
void get_ans()
{
//設置邊界
for(int i = 0;i <= n;i++) dp[i][0] = 0;
for(int j = 0;j <= v;j++) dp[0][j] = 0;
//開始遞推 dp
for(int i = 1;i <= n;i++){
for(int j = 1;j <= v;j++){
dp[i][j] = dp[i-1][j];
if(j - ds[i] >= 0)
dp[i][j] = max(dp[i-1][j-ds[i]] + ds[i],dp[i][j]);
}
}
}
int main()
{
init();
get_ans();
printf("%d",v - dp[n][v]);
return 0;
}
因爲我也是看到別人的代碼之後才首先有了的疑惑,所以先貼一下別人的代碼:
#include<cstdio>
using namespace std;
int v,n;
int f[20010];
int w[40];
int main(){
int i,j;
scanf("%d%d",&v,&n);
for(i=1;i<=n;i++){
scanf("%d",&w[i]);
}
for(i=1;i<=n;i++){
for(j=v;j>=w[i];j--){
if(f[j]<f[j-w[i]]+w[i]){
f[j]=f[j-w[i]]+w[i];
}
}
}
printf("%d\n",v-f[v]);
}
從代碼中我們可以看到,雖然他(之後都用他代表上面這個代碼的作者)也用了二重循環,但是他只用了一個一維數組,這樣做的可行性就是 DP 的空間優化。
接下來,我就來結合第二種答案解釋一下 DP 的空間優化。
首先代碼中的 dp 的狀態是用 f 數組裝的,並且可以發現它沒有設置 物品狀態,這是因爲我們的轉移方程在關於物品 i 的轉移上只與前一個 i 有關,這就爲空間優化提供了理論前提。
單看 f[j] 所代表的含義,應該是在剩餘空間爲 j 的情況下,最多能放多少空間。那麼,如何讓 f[j] 再隱含地加上一個使用題目給的 n 個物品的條件呢?其中的奧祕就在於下面這個循環
for(i=1;i<=n;i++){
for(j=v;j>=w[i];j--){
if(f[j]<f[j-w[i]]+w[i]){
f[j]=f[j-w[i]]+w[i];
}
}
}
總的來說,每個物品都有選與不選兩種情況,上面的 i 就代表現在討論該物品,而循環體中的內容就是在討論當前剩餘空間是選這個物品更好還是不選更好。
那麼我們使 i 從 1 到 n 遍歷,假如 i 遍歷到 3,那麼此時在剩餘空間爲 j 的情況下討論選不選 3 的背景是,f[j] 已經確定了有兩個物品時最優的選擇(因爲 i 先確定了物品 1 和物品 2 選還是不選),所以當物品 3 討論完之後 f[j] 就可以代表當只有前 3 個物品時的最優決策。
由此可知,在循環體中的 f[j] 就可以代表有前 i 個物品時,剩餘 j 空間的最大體積值。那麼使用給定的 n 個物品,在 v 空間下最大的體積值顯然就是當 i 遍歷完 n 之後的 f[m]。那麼題目答案顯然就是 v - f[v]。
最後一個問題,爲什麼第二重循環 j 是從 v 開始倒着遍歷呢?其實這個要更容易理解些,因爲我們更新 f[j] 是通過 j 取更小時的 f[j] 的狀態來確定的,所以如果我們從正着遍歷 j 的話,就有可能出現這種情況:當前選中的物品重量爲 2,然後從前往後更新,先更新了 f[2],也就意味着 f[2] 已經是考慮了當前物品了,那麼當我們更新 f[4] 是,由於 f[4] 會受 f[2] 的影響,而此時 f[2] 已經不是前 2 個(不包括第二個)時的狀態了,那麼就與我們上面討論的 f[j] 的意義以及狀態轉移不符了。