从“采药”问题看0/1背包

    (0/1背包题目:对于每一件物品,只能取一次,而且有容量限制,每件物品有重量和价值,决策为取与不取)

    讲完了数塔问题,咱们再来看看一个炒鸡老炒鸡老的题目:NOIP2005 PJ组 第三题:“采药”。

    题目描述

    对于这道题目,我们先来想想搜索怎么写(因为DP和记搜很相似)。

    我们先定义f[i][j]表示剩余i时间,取了j株草药所能获得的最大价值。

    那么,冗余搜索是什么?

    让我们来思考:如果在取到第x株草药时花费了t时间,而且目前所获得的价值比f[t][x]所要小,那么接下来无论怎么操作它最后得出的ans一定比用f[t][x]所进行操作的ans小?

        答案就是如此(大家可以思考一下为什么)。

        接下来附上记搜代码:

void dfs(int t, int x, int val) // 当前的状态: 背包还剩t的空间, 现在采到第X棵草药, 目前的总价值为val
{
	if(val <= f[t][x]) return; // 记忆化: 如果当前的价值小于之前某一次, 那就直接退出

	f[t][x] = val; // 更新记忆化数组

	if(x == n) // 采到最后一棵了
	{
		if(val > ans) ans = val; // 与答案取最大值
		return;
	}

	dfs(t, x+1, val); // 不取这一棵
	if(t >= w[x]) dfs(t - w[x], x+1, val + v[x]); // 取这一棵, 前提是当前的空间足够
}

    写完了记搜,然后我们来看看DP的实现方式

    我们先来回顾一下动态规划的意义:只记录状态的最优值,并用最优值来推导其他的最优值。

   然后我们通过刚刚的记搜来设计我们的状态:

    f[i][j]表示:已经决定了前i株草药,用了j的时间,所能得到的最大价值。

    先来看顺推:

    “我这个状态的下一步去向何方”:决定下一个物品取还是不取。

        >不取:状态转移为f[i+1][j]

        >取:状态转移为f[i+1][j-w[i+1]](需要满足重量约束)

    再来看逆推:

    “我这个状态从何而来”:决定我这个物品取不取

        >不取:由f[i-1][j]推导而来

        >取:由f[i-1][j-w[i]]推导而来(需要满足重量约束)

先附上顺推代码:

for(int i = 0; i < n; ++ i)
		for(int j = 0; j <= t; ++ j)
		{
			// 顺推: 考虑当前状态, 已经采了i棵草药, 目前占据的空间大小为j

			// 第一种方法: 不采这一棵, 则下一步的状态则是(i+1, j)
			f[i+1][j] = max(f[i+1][j], f[i][j]);

			// 第二种方法: 采这一棵, 则需要满足空间足够大
			if(j + w[i] <= t) // 约束 : 空间大小足够
			// 下一步的状态则是(i+1, j+w[i]) 
			f[i+1][j+w[i]] = max(f[i+1][j+w[i]], f[i][j]+v[i]);
		}

	ans = f[n][t]; // 答案
	cout << ans << endl;

现在附上逆推代码:

	for(int i = 1; i <= n; ++ i)
		for(int j = 0; j <= t; ++ j)
		{
			// 逆推: 考虑是从什么状态到达我这里的(i, j)
			f[i][j] = f[i-1][j]; // 如果我这棵草药不取, 那么从状态(i-1, j)可以达到这个状态
			// 如果我取了这棵草药, 那个从状态(i-1, j-w[i]), 再加上这棵草药, 就可以达到这个状态
			if(j >= w[i]) f[i][j] = max(f[i][j], f[i-1][j-w[i]] + v[i]); // 但需要注意, 约束是需要满足的
		}

	ans = 0;
	for(int i = 0; i <= t; ++ i) ans = max(ans, f[n][i]);
	cout << ans << endl; // 输出答案

    接下来我们来考虑数组压缩:

    *数组压缩:即用一个一维数组来代替二维数组

    -观察方程,我们可以发现,f[i]仅仅是由f[i-1]决定的,也就是说,前面的大多数状态对后面均无影响。

    

f[1] ... ... ... ... ... ...
f[2] ... ... ... ... ... ...
f[3] 0 0 f[3][3]=12 0 f[3][5]=15 0
f[4] f[4][1]=? f[4][2]=? f[4][3]=? f[4][4]=? f[4][5]=? f[4][6]=?
f[5]            
f[6]            









如上表,f[4]中的所有状态只和f[3]中的两个状态f[3][3]和f[3][5]有关而f[3][1],f[3][2]等等对于f[4]中的任意一个值都没有影响,所以我们可以仅仅保留前一行的状态。

那如何压缩呢?我们用一种特别简单的方法:将j倒着枚举。

附上代码:

for(int i = 1; i <= n; ++ i)
		for(int j = t; j >= 0; -- j) // 在使用压缩状态的时候, 需要注意枚举的方向
		{ // 由于是01背包, 所以j要倒过来枚举 (注意每时每刻数组里存的是f[i-1][j]还是f[i][j])
			// f[i][j] = f[i-1][j]; -> f[j] = f[j]
			//if(j >= w[i]) f[i][j] = max(f[i][j], f[i-1][j-w[i]] + v[i]);
			if(j >= w[i]) f[j] = max(f[j], f[j - w[i]] + v[i]);
		}

上述就是采药的全部讲解,实际上采药是一道全裸的0/1背包题目,对于所有全裸的0/1背包,均可用上述的三种代码来实现。




结束了?

没有。

实际上,对于0/1背包的代码打法。还有另外一种:

for(int i = 1; i <= n; ++ i)
		for(int j = t; j >= w[i]; -- j) // 在使用压缩状态的时候, 需要注意枚举的方向
		{ 
                        f[j] = max(f[j], f[j - w[i]] + v[i]);
		}

有没有发现什么不一样?

没错,第二层for循环,把“0”改为w[i],可以省掉一个if判断,是不是很厉(méi)害(yòng)?

好了,以上就是采药及0/1背包的全部讲解。


猜你喜欢

转载自blog.csdn.net/SKDream_Khan/article/details/79346997