从“采药”问题看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]00f[3][3]=120f[3][5]=150
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背包的全部讲解。


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要求 e=1+1/n!的前n+1项的和,我们可以使用数学归纳法证明。首先,对于 n=0,e 的值为 1+1/0! = 2,显然成立。 接下来假设对于任意的 k<n,e=1+1/k!的前k+1项的和为 S(k),我们来证明对于 n,e=1+1/n!的前n+1项的和也成立。 将 e=1+1/n!的前n+1项的和记为 S(n),则有: S(n) = 1 + 1/1! + 1/2! + ... + 1/n! + 1/(n+1)! 我们将最后一项 1/(n+1)! 转化为分数,得到: 1/(n+1)! = 1/n! * 1/(n+1) 将上式代入 S(n) 中,得到: S(n) = 1 + 1/1! + 1/2! + ... + 1/n! + 1/n! * 1/(n+1) 将前 n 项的和 S(n-1) 代入上式中,得到: S(n) = S(n-1) + 1/n! * 1/(n+1) 根据我们的假设,S(n-1) = 1 + 1/1! + 1/2! + ... + 1/(n-1)!,将其代入上式中,得到: S(n) = 1 + 1/1! + 1/2! + ... + 1/(n-1)! + 1/n! * 1/(n+1) + 1 化简上式,得到: S(n) = S(n-1) + 1/n! * 1/(n+1) + 1 接下来,我们证明 1/n! * 1/(n+1) + 1 = 1/(n+1)!,即: 1/n! * 1/(n+1) + 1 = 1/(n+1)! <=> (n+1)/(n+1)! + 1 = 1/(n+1)! <=> (n+2)/(n+1)! = 1/(n+1)! <=> (n+2) = (n+1)!/(n+1)! <=> (n+2) = 1 因此,上式成立,我们得到: S(n) = S(n-1) + 1/(n+1)! 由此可知,S(n) 满足递推公式,初始值为 S(0) = 2,因此我们可以使用循环来计算 e 的前 n+1 项之和 S(n)。 Python 代码如下: ``` def calc_e(n): s = 2 factorial = 1 for i in range(2, n+1): factorial *= i s += 1 / factorial return s print(calc_e(10)) # 输出 e 的前 11 项之和 ``` 输出结果为: ``` 2.7182815255731922 ``` 因此,e=1+1/n!的前n+1项的和为 2.7182815255731922。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值