DP的空间优化

前言

何谓 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

题解

  • 普通 DP

题目中要求求剩余最小空间,那么其实就是求在不超过规定空间的条件下存放的最大空间,之后用 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;
}
  • 空间优化后的 DP

因为我也是看到别人的代码之后才首先有了的疑惑,所以先贴一下别人的代码:

#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] 的意义以及状态转移不符了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章