[杂记] 01背包记录路径

[杂记] 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)\)的做法,欢迎交流。

posted @ 2022-12-06 14:28  CQzhangyu  阅读(384)  评论(0编辑  收藏  举报