背包问题的空间优化

01背包

题目描述

\(n\) 个重量和价值分别为 \(w_i\)\(v_i\) 的物品。从这些物品中挑选出总重量不超过 \(W\) 的物品,求所有挑选方案中价值总和的最大值。
数据范围
\(1\le n\le100\)
\(1\le w_i,v_i\le100\)
\(1\le W\le10000\)


优化前

  • 递推式:

\[\begin{split} dp[0][j]&=0 \\ dp[i + 1][j]&=\begin{cases} dp[i][j]&,j<w_i \\ max\{dp[i][j],dp[i][j-w_i]+v_i\}&,j\ge w_i \end{cases} \end{split} \]

  • 代码:
// 输入
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]);
}

优化一:二维数组

\(dp[i + 1][j]=max\{dp[i][j],dp[i][j-w_i]+v_i\}\),我们可以发现,虽然 \(dp[MAX][MAX]\)\(MAX\) 行,但是每次只访问 \(i\)\(i+1\)行。因此,我们只要用一个 \(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=\begin{cases} 1&, i=2\ast k+1\\ 0&,i=2\ast k \end{cases} k=0,1,2... \]


优化二:一维数组

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

\[\begin{split} dp[0][j]&=0 \\ dp[i + 1][j]&=\begin{cases} dp[i][j]&,j<w_i \\ max\{dp[i][j],dp[i][j-w_i]+v_i\}&,j\ge w_i \end{cases} \end{split} \]

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

原来的代码是

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

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

\[dp[j]=max\{dp[j],dp[j-w_i]+v_i\},j\ge w_i \]

// 输入
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\) 个重量和价值分别为 \(w_i\)\(v_i\) 的物品。从这些物品中挑选出总重量不超过 \(W\) 的物品,求所有挑选方案中价值总和的最大值。在这里,每种物品可以挑选任意多件。
数据范围
\(1\le n\le100\)
\(1\le w_i,v_i\le100\)
\(1\le W\le10000\)


优化前

  • 递推式

\[\begin{split} dp[0][j]&=0 \\ dp[i + 1][j]&=\begin{cases} dp[i][j]&,j<w_i \\ max\{dp[i][j],dp[i+1][j-w_i]+v_i\}&,j\ge w_i \end{cases} \end{split} \]

  • 代码
// 输入
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\) 时,与01背包中的分析一样。
    图3
  • \(j\ge w_i\) 时,与01背包唯一的不同是,其中一个数据由 \(dp[i][j-w_i]\) 变成了 \(dp[i+1][j-w_i]\)。虽然都是访问计算点左半部分的数据,但是,需要注意的是,访问的是新数据(\(i+1\)),不是老数据(\(i\))。因此,第二个循环必须是从左往右的。
    图4

\[dp[j]=max\{dp[j],dp[j-w_i]+v_i\},j\ge w_i \]

// 输入
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++) \\ 从左往右

posted @   ltign  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示