背包问题的空间优化

目录

  • 01背包
    • 题目描述
    • 优化前
    • 优化一:二维数组
    • 优化二:一维数组
  • 完全背包
    • 题目描述
    • 优化前
    • 优化一:二维数组
    • 优化二:一维数组

01背包

题目描述

n n n 个重量和价值分别为 w i w_i wi v i v_i vi 的物品。从这些物品中挑选出总重量不超过 W W W 的物品,求所有挑选方案中价值总和的最大值。
数据范围
1 ≤ n ≤ 100 1\le n\le100 1n100
1 ≤ w i , v i ≤ 100 1\le w_i,v_i\le100 1wi,vi100
1 ≤ W ≤ 10000 1\le W\le10000 1W10000


优化前

  • 递推式:

d p [ 0 ] [ j ] = 0 d p [ i + 1 ] [ j ] = { d p [ i ] [ j ] , j < w i m a x { d p [ i ] [ j ] , d p [ i ] [ j − w i ] + v i } , j ≥ w i \begin{split} dp[0][j]&=0 \\ dp[i + 1][j]&=\begin{cases} dp[i][j]&,jdp[0][j]dp[i+1][j]=0={dp[i][j]max{dp[i][j],dp[i][jwi]+vi},j<wi,jwi

  • 代码:
// 输入
int n, W;                    // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX];        // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX][MAX]             // dp数组,与记忆化数组一样,必须足够大

void solve(void)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j <= W; j++)
		{
			if (j < w[i])
				dp[i + 1][j] = dp[i][j];
			else
				dp[i + 1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]);
		}
	}

	printf("%d\n", dp[n][W]);
}

优化一:二维数组

d p [ i + 1 ] [ j ] = m a x { d p [ i ] [ j ] , d p [ i ] [ j − w i ] + v i } dp[i + 1][j]=max\{dp[i][j],dp[i][j-w_i]+v_i\} dp[i+1][j]=max{dp[i][j],dp[i][jwi]+vi},我们可以发现,虽然 d p [ M A X ] [ M A X ] dp[MAX][MAX] dp[MAX][MAX] M A X MAX MAX 行,但是每次只访问 i i i i + 1 i+1 i+1行。因此,我们只要用一个 d p [ 2 ] [ M A X ] dp[2][MAX] dp[2][MAX] 就能实现原来的功能。

// 输入
int n, W;                    // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX];        // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[2][MAX]               // dp数组,与记忆化数组一样,必须足够大

void solve(void)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j <= W; j++)
		{
			if (j < w[i])
				dp[(i + 1) & 1][j] = dp[i & 1][j];
			else
				dp[(i + 1) & 1][j] = max(dp[i & 1][j], dp[i & 1][j - w[i]] + v[i]);
		}
	}

	printf("%d\n", dp[n & 1][W]);
}
  • & 按位与是一个双目操作符,对数字的二进制位进行操作。规则是:两个数字相对应的二进制位相同,运算结果为1;不同,则为0。
  • 由十进制与二进制的转换规则可知,从右往左看,偶数的第一个二进制为0;奇数的第一个二进制位为1。

所以,我们可以得到:
i & 1 = { 1 , i = 2 ∗ k + 1 0 , i = 2 ∗ k k = 0 , 1 , 2... i\&1=\begin{cases} 1&, i=2\ast k+1\\ 0&,i=2\ast k \end{cases} k=0,1,2... i&1={10,i=2k+1,i=2kk=0,1,2...


优化二:一维数组

动态规划的核心是先记录,再访问。如果访问过后,数据就不要再需要了,那么覆盖这条记录也不会对结果造成影响。因此,我们只使用一个一维数组 d p [ M A X ] dp[MAX] dp[MAX] 也能达到目的。
我们再来看递推式:

d p [ 0 ] [ j ] = 0 d p [ i + 1 ] [ j ] = { d p [ i ] [ j ] , j < w i m a x { d p [ i ] [ j ] , d p [ i ] [ j − w i ] + v i } , j ≥ w i \begin{split} dp[0][j]&=0 \\ dp[i + 1][j]&=\begin{cases} dp[i][j]&,jdp[0][j]dp[i+1][j]=0={dp[i][j]max{dp[i][j],dp[i][jwi]+vi},j<wi,jwi

  • j < w i jj<wi 时,从二维的视角来看,数据从 d p [ i ] [ j ] dp[i][j] dp[i][j] 拷贝到 d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j];切换到一维,数据其实没有发生变化,也就是说不需要对原来的数据进行操作。
    背包问题的空间优化_第1张图片
  • j ≥ w i j\ge w_i jwi 时,从二维的视角来看,计算 d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j] 需要访问 d p [ i ] [ j ] dp[i][j] dp[i][j] d p [ i ] [ j − w i ] dp[i][j-w_i] dp[i][jwi]。再到一维,就可以发现,需要的数据是总是在该位置的左半部分(要计算第 j j j 列,需要访问第 j j j 列和第 j − w i j-w_i jwi 列的数据)。也就是说,右半部分的数据是可以覆盖的)。
    背包问题的空间优化_第2张图片

原来的代码是

for (int j = 0; j <= W; j++)

因为是从左向右的循环,所以肯定影响后来的计算。若改为从右往左计算,则能避免这样的问题。

d p [ j ] = m a x { d p [ j ] , d p [ j − w i ] + v i } , j ≥ w i dp[j]=max\{dp[j],dp[j-w_i]+v_i\},j\ge w_i dp[j]=max{dp[j],dp[jwi]+vi},jwi

// 输入
int n, W;                    // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX];        // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX]                  // dp数组,与记忆化数组一样,必须足够大

void solve(void)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = W; j >= w[i]; j--)
		{
			dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
		}
	}

	printf("%d\n", dp[W]);
}

完全背包

题目描述

n n n 个重量和价值分别为 w i w_i wi v i v_i vi 的物品。从这些物品中挑选出总重量不超过 W W W 的物品,求所有挑选方案中价值总和的最大值。在这里,每种物品可以挑选任意多件。
数据范围
1 ≤ n ≤ 100 1\le n\le100 1n100
1 ≤ w i , v i ≤ 100 1\le w_i,v_i\le100 1wi,vi100
1 ≤ W ≤ 10000 1\le W\le10000 1W10000


优化前

  • 递推式

d p [ 0 ] [ j ] = 0 d p [ i + 1 ] [ j ] = { d p [ i ] [ j ] , j < w i m a x { d p [ i ] [ j ] , d p [ i + 1 ] [ j − w i ] + v i } , j ≥ w i \begin{split} dp[0][j]&=0 \\ dp[i + 1][j]&=\begin{cases} dp[i][j]&,jdp[0][j]dp[i+1][j]=0={dp[i][j]max{dp[i][j],dp[i+1][jwi]+vi},j<wi,jwi

  • 代码
// 输入
int n, W;                    // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX];        // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX][MAX]             // dp数组,与记忆化数组一样,必须足够大

void solve(void)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j <= W; j++)
		{
			if (j < w[i])
				dp[i + 1][j] = dp[i][j];
			else
				dp[i + 1][j] = max(dp[i][j], dp[i + 1][j - w[i]] + v[i]);   // 与01背包唯一的不同
		}
	}

	printf("%d\n", dp[n][W]);
}

优化一:二维数组

// 输入
int n, W;                    // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX];        // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[2][MAX]               // dp数组,与记忆化数组一样,必须足够大

void solve(void)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j <= W; j++)
		{
			if (j < w[i])
				dp[(i + 1) & 1][j] = dp[i & 1][j];
			else
				dp[(i + 1) & 1][j] = max(dp[i & 1][j], dp[(i + 1) & 1][j - w[i]] + v[i]);   // 与01背包唯一的不同
		}
	}

	printf("%d\n", dp[n & 1][W]);
}

优化二:一维数组

  • j < w i jj<wi 时,与01背包中的分析一样。
    背包问题的空间优化_第3张图片
  • j ≥ w i j\ge w_i jwi 时,与01背包唯一的不同是,其中一个数据由 d p [ i ] [ j − w i ] dp[i][j-w_i] dp[i][jwi] 变成了 d p [ i + 1 ] [ j − w i ] dp[i+1][j-w_i] dp[i+1][jwi]。虽然都是访问计算点左半部分的数据,但是,需要注意的是,访问的是新数据( i + 1 i+1 i+1),不是老数据( i i i)。因此,第二个循环必须是从左往右的。
    背包问题的空间优化_第4张图片

d p [ j ] = m a x { d p [ j ] , d p [ j − w i ] + v i } , j ≥ w i dp[j]=max\{dp[j],dp[j-w_i]+v_i\},j\ge w_i dp[j]=max{dp[j],dp[jwi]+vi},jwi

// 输入
int n, W;                    // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX];        // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX]                  // dp数组,与记忆化数组一样,必须足够大

void solve(void)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = w[i]; j <= W; j++)
		{
			dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
		}
	}

	printf("%d\n", dp[W]);
}

与01背包比较,通过该方法优化后,二者代码的区别仅在第二个循环的方向

  • 01背包:
for (int j = W; j >= w[i]; j--) \\ 从右往左
  • 完全背包:
for (int j = w[i]; j <= W; j++) \\ 从左往右

你可能感兴趣的:(algorithms,算法,动态规划)