期望类题目如何DP(abc314-e题解)

题意:有 N N N 个转盘,第 i i i 个转盘上有 P i P_i Pi 个数 S i , 1 S_{i,1} Si,1 S i , P i S_{i,P_i} Si,Pi,每玩一次转盘会从中随机选取一个数作为这次的得分,每玩一次这个转盘花费 C i C_i Ci 元。

求总得分为 M M M 时,花费钱数的期望。


赛时做了一个小时只写出一个错误的贪心,我们不妨从这个失败的贪心出发,聊聊期望类题目的正解做法。


错误思路:

我们不妨考虑如下的贪心策略:

计算出每个转盘上所有数的平均值作为玩一次这个转盘得数的期望,再除以每个转盘玩一次的价格,找到其中的最大值,用 M M M 除以这个最大值得到答案。

部分代码:

	for(int i = 1; i <= n; i = i + 1)
	{
		cin >> c[i] >> p[i];
		for(int j = 1; j <= p[i]; j = j + 1)
		{
			cin >> s;
			if(s > m)
				s = m;	// 赛时写的一个毫无卵用的优化
			sum[i] += s;
		}
		ave[i] = sum[i] / double(p[i]);
		ave[i] /= c[i];
	}
	sort(ave + 1, ave + 1 + n, cmp);
	cout << m / ave[1];

这应该算是一个很多人都可能会考虑到的贪心策略了,但是其正确性究竟怎样呢?

考虑这样的数据:

N = 2
M = 100
第一个轮盘玩一次 100 元
0 0 0 0 100
第二个轮盘玩一次 100 元
0 0 0 0 0 0 0 1000

可以计算出,下面的轮盘玩一次平均获得 125 125 125 元,而上面的轮盘每玩一次平均仅获得 20 20 20 元,但是显然更优解是选择玩上面的轮盘。

为什么?

记不记得我们初中时候学过:平均值很多时候不能反映一组数据的真实情况,因为其很容易被极端值影响。

那有人说了:去掉一个最大值,去掉一个最小值,你看行不行?

当然不行啊!一堆 0 0 0 中间插了一个 100 100 100,你把最大值 100 100 100 干掉了,你看看这个盘子里还剩个啥。

所以这个题贪心来做很难找到一种合适的贪心策略,这个时候,我们就回忆起了——

暴力搜索

DP!


正解:我们设 d p i dp_i dpi 表示获得 i i i 得分所用的钱的期望值。

不难想到,由于最后我们希望得到一个最优解,一定要最小化这个期望值,所以我们初始化时全部设成一个大数。

那么接下来考虑如何转移:

不难想起背包问题中我们的转移方程:

d p i = max ⁡ ( d p i , d p i − v + w ) dp_i = \max(dp_i, dp_{i-v}+w) dpi=max(dpi,dpiv+w)

(完全背包问题的转移方程)

那么这道题其实也很类似,只不过在做常规背包问题的时候我们是有选取某件物品的资格的,但是现在我们不能选特定的物品了,只能听天由命,让概率说话。

所以针对一个轮盘上的某个数 p k p_k pk,如果我们选到了它,就会让此时的期望值变成 d p max ⁡ ( 0 , i − p k ) dp_{\max(0,i - p_k)} dpmax(0,ipk)

但是需要注意,如果 p k = 0 p_k = 0 pk=0,那么期望值转移回了 d p i dp_i dpi 自己,这一步做得毫无意义,所以我们需要特判一下 p k = 0 p_k = 0 pk=0 的情况。

于是,我们只需要实现定义一个变量 d d d 记录一下这个过程中我们的期望值的和,之后我们再除以有意义情况的个数,就得到了每次选到有意义情况时的期望。

以下,为了称呼方便,我们把 s j , k = 0 s_{j,k} = 0 sj,k=0 称为无意义情况,反之称为有意义情况。

这一部分代码如下:

			d = 0;
			int z = 0;
			for(int k = 1; k <= p[j]; k = k + 1)
			{
				// 为了称呼方便,我们把 s[j][k] == 0 称为无意义情况,反之称为有意义情况 
				if(s[j][k] == 0)
					z ++;	// z 表示 0 的个数 
				else d += dp[max(0, i - s[j][k])];	// 此时 d 表示有意义情况总共的期望值 
			}
			d /= (p[j] - z);	// 此时 d 表示有意义情况平均的期望值 

然而,并不是每次操作都能选到有意义的情况,因为无意义情况个数为 z z z,所以有意义情况个数位 p j − z p_j - z pjz,总情况数为 p j p_j pj,则不难理解选取到有意义情况的概率为 p j − z p j \dfrac{p_j-z}{p_j} pjpjz

而每玩一次都需要 C i C_i Ci 元,因此选到有意义的情况的代价应该是 C i × p j p j − z C_i \times \dfrac{p_j}{p_j-z} Ci×pjzpj

那么我们就得到了状态转移的完整代码:

	for(int i = 1; i <= m; i = i + 1)
	{
		double d;
		dp[i] = 1145141919810;
		for(int j = 1; j <= n; j = j + 1)
		{
			d = 0;
			int z = 0;
			for(int k = 1; k <= p[j]; k = k + 1)
			{
				// 为了称呼方便,我们把 s[j][k] == 0 称为无意义情况,反之称为有意义情况 
				if(s[j][k] == 0)
					z ++;	// z 表示 0 的个数 
				else d += dp[max(0, i - s[j][k])];	// 此时 d 表示有意义情况总共的期望值 
			}
			d /= (p[j] - z);	// 此时 d 表示有意义情况平均的期望值 
			d += c[j] * p[j] / double(p[j] - z);	// 同时,选到有意义情况需要付出一定代价
			// 每玩一次需要 c[j] 元,其中选到有意义情况的机会是 (p[j] - z) / p[j] 
			// 所以平均每次选到有意义情况的代价为 c[j] * p[j] / (p[j] - z) 
			dp[i] = min(dp[i], d);
			// 取每一个轮盘中的最小值 
		}
	}

结合前面的输入输出,以下是完整 AC 代码:

#include<bits/stdc++.h>
using namespace std;
int n, m, c[114], p[114], s[114][114];
double dp[114];
int main()
{
//	freopen("a.txt", "r", stdin);
	cin >> n >> m;
	for(int i = 1; i <= n; i = i + 1)
	{
		cin >> c[i] >> p[i]; 
		for(int j = 1; j <= p[i]; j = j + 1)
			cin >> s[i][j];
	}
	for(int i = 1; i <= m; i = i + 1)
	{
		double d;
		dp[i] = 1145141919810;
		for(int j = 1; j <= n; j = j + 1)
		{
			d = 0;
			int z = 0;
			for(int k = 1; k <= p[j]; k = k + 1)
			{
				// 为了称呼方便,我们把 s[j][k] == 0 称为无意义情况,反之称为有意义情况 
				if(s[j][k] == 0)
					z ++;	// z 表示 0 的个数 
				else d += dp[max(0, i - s[j][k])];	// 此时 d 表示有意义情况总共的期望值 
			}
			d /= (p[j] - z);	// 此时 d 表示有意义情况平均的期望值 
			d += c[j] * p[j] / double(p[j] - z);	// 同时,选到有意义情况需要付出一定代价
			// 每玩一次需要 c[j] 元,其中选到有意义情况的机会是 p[j] / (p[j] - z) 
			// 所以平均每次选到有意义情况的代价为 c[j] * p[j] / (p[j] - z) 
			dp[i] = min(dp[i], d);
			// 取每一个轮盘中的最小值 
		}
	}
	cout << dp[m];
	return 0; 
}

完美 ~ 撒花✿✿ヽ(°▽°)ノ✿

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值