[雜記] 01揹包記錄路徑
衆所周知,01揹包的時間複雜度是\(O(nm)\)(n爲物品數量,m爲揹包容量),空間複雜度是\(O(m)\)。如果還需要輸出最優解中的所有物品的話,時間複雜度不變,空間複雜度呢?
你的第一反應可能是:我很快就可以給出一個空間複雜度也是\(O(m)\)的算法啊?
但實際上這個算法是有問題的:
int n; // 物品數量
int m; // 揹包容量
int w[N]; // 物品重量
int v[N]; // 物品價值
int dp[M]; // dp數組
int pre[M]; // 路徑
void package01() {
for (int i = 1; i <= n; i ++) {
for (int j = m; j >= w[i]; j --) {
if (dp[j] < dp[j-w[i]] + v[i]) {
dp[j] = dp[j-w[i]] + v[i];
pre[j] = i;
}
}
}
int j = m;
while (pre[j] != 0) {
int last = pre[j];
printf("%d, ", last);
j -= w[last];
}
}
這樣寫有什麼問題呢?
當然有!
比如對於下面的樣例:
n = 3;
m = 4;
w[] = {1, 1, 2};
v[] = {1, 1, 4};
輸出結果就是
3, 3,
第三個物品被用了兩次。
這時你可能恍然大悟,因爲在第三個物品加入後,pre[2]被更新爲了3,所以就丟失了前兩個物品的路徑。
本質原因在於,一開始的01揹包本來就是二維數組\(dp[i][j]\),表示“只使用前i個物品,揹包容量爲j時的最大價值”,這時\(pre[i][j]\)的意義就是“只使用前i個物品,揹包容量爲j,達到最大價值時最後一個購買的物品”。轉移是:
if (dp[i-1][j] > dp[i-1][j-w[i]] + v[i]) {
dp[i][j] = dp[i-1][j-w[i]] + v[i];
pre[i][j] = i;
}
else {
dp[i][j] = dp[i-1][j];
pre[i][j] = pre[i-1][j];
}
路徑是:
int i = n, j = m;
while (pre[i][j] != 0) {
int last = pre[i][j];
printf("%d, ", last);
i = last - 1;
j -= w[last];
}
如果把pre壓縮爲1維,相當於最後只剩下\(pre[n][.]\),前面\(pre[<n][.]\)的路徑信息就丟失了,這樣就沒法輸出完整路徑了。
那有沒有時間複雜度還是\(O(nm)\),空間複雜度低於\(O(nm)\)的做法呢?
好吧我是沒想出來。
不過倒是有一種能讓空間降低32倍的做法:將pre變成一個01數組,\(pre[i][j]=1\)表示“只使用前i個物品,揹包容量爲j,達到最大價值時最後一個購買的物品就是i”。這樣也能輸出完整的路徑。
int i = n, j = m;
while (i != 0) {
while (!pre[i][j])
i --;
printf("%d, ", i);
j -= w[i];
i --;
}
如果有高人能給出空間複雜度低於\(O(nm)\)的做法,歡迎交流。