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 f s ( i , j ) = dfs(i,j)= dfs(i,j)= 从编号为 i i i 的物品开始挑选出总重量不超过 j j j 的物品,所有挑选方案中价值总和的最大值。(编号从 0 开始,最后一个物品编号为 n − 1 n-1 n1

  • 核心思想:对于编号为 i i i 的物品,可以选择 不拿 或者
    不拿:相当于从下一个物品(编号为 i + 1 i+1 i+1)开始挑选总重量仍然不超过 j j j 的物品,即 d f s ( i + 1 , j ) dfs(i+1,j) dfs(i+1,j)
    :由于拿了重量为 w i w_i wi 的物品,则总重量由 j j j 变成了 j − w i j-w_i jwi。相当于从下一个物品(编号为 i + 1 i+1 i+1)开始挑选总重量不超过 j − w i j-w_i jwi 的物品,即 d f s ( i + 1 , j − w i ) dfs(i+1,j-w_i) dfs(i+1,jwi)
  • 方程:
    d f s ( n , j ) = 0 d f s ( i , j ) = { d f s ( i + 1 , j ) , j < w i m a x { d f s ( i + 1 , j ) , d f s ( i + 1 , j − w i ) + v i } , j ≥ w i \begin{split} dfs(n,j)&=0 \\ dfs(i,j)&=\begin{cases} dfs(i+1,j)&,jdfs(n,j)dfs(i,j)=0={dfs(i+1,j)max{dfs(i+1,j),dfs(i+1,jwi)+vi},j<wi,jwi
  • 代码
// 输入
int n, W;                // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX];    // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值

// dfs(i, j) -- 从编号为i开始挑选总重不超过j的物品,返回最大价值
int dfs(int i, int j)
{
	if (i == n)
		return 0;
	
	int ret;
	if (j < w[i])
		ret = dfs(i + 1, j);
	else
		ret = max(dfs(i + 1, j), dfs(i + 1, j - w[i]) + v[i]);
		
	return ret;
}

// 输出
void solve(void)
{
	printf("%d\n", dfs(0, W));
}

记忆化

我们用一组数据来看一下暴力搜索的过程。

n = 4, W = 5
(w, v) = {(2, 3), (1, 2), (3, 4), (2, 2)}

我们用二叉树的形式来描述该过程:
01背包问题_第1张图片
我们发现 d f s ( 3 , 2 ) dfs(3, 2) dfs(3,2) 执行了两次,这显然造成了浪费。如果能把第一次的结果记录下来,那么就避免了第二次的计算。这就是记忆化搜索,把搜索过程中的结果记录下来,避免重复的搜索。

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

// dfs(i, j) -- 从编号为i开始挑选总重不超过j的物品,返回最大价值
int dfs(int i, int j)
{
	if (i == n)
		return 0;
	
	// 使用已经计算过的结果
	if (dp[i][j] >= 0)
		return dp[i][j];
	
	int ret;
	if (j < w[i])
		ret = dfs(i + 1, j);
	else
		ret = max(dfs(i + 1, j), dfs(i + 1, j - w[i]) + v[i]);
		
	return ret;
}

// 输出
void solve(void)
{
	memset(dp, -1, sizeof(dp));    // 初始化记忆数组
	
	printf("%d\n", dfs(0, W));
}

不难看出,与暴力搜索相比,记忆化搜索在原来的基础上,只不过多了 记忆化数组的声明和初始化访问记忆化数组
值得注意的是,记忆化数组必须开得足够大。因为它是用来记录 d f s ( i , j ) dfs(i,j) dfs(i,j) 的结果的,所以必须使 d p [ i ] [ j ] dp[i][j] dp[i][j] 总是合法,不会越界访问


动态规划

对于动态规划来说,最重要的就是 递推关系式。一般,我们可以先写出搜索算法,再得到递推式;我们也可以直接得出递推式。

由搜索递归函数得到递归式

由上文 暴力搜索 提到的函数递归式,我们可以写出动态规划的递推式。
d p [ n ] [ j ] = 0 d p [ i ] [ j ] = { d p [ i + 1 ] [ j ] , j < w i m a x { d p [ i + 1 ] [ j ] , d p [ i + 1 ] [ j − w i ] + v i } , j ≥ w i \begin{split} dp[n][j]&=0 \\ dp[i][j]&=\begin{cases} dp[i+1][j]&,jdp[n][j]dp[i][j]=0={dp[i+1][j]max{dp[i+1][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 = n - 1; i >= 0; i--)
	{
		for (int j = 0; j <= W; j++)
		{
			if (j < w[i])
				dp[i][j] = dp[i + 1][j];
			else
				dp[i][j] = max(dp[i + 1][j], dp[i + 1][j - w[i]] + v[i]);
		}
	}

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

直接写出递归式

定义 d p [ i ] [ j ] = dp[i][j]= dp[i][j]= 从前 i i i 个中选出总重量不超过 j j j 的物品。(因为编号从 0 0 0 开始,所以前 i + 1 i+1 i+1个物品,最后一个物品的编号为 i i i
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]);
}

你可能感兴趣的:(algorithms,算法)