题目描述:
样例输入/输出:
样例1:
样例2:
难点分析:
拿到这道题的时候,笔者觉得有一个难点。押注可以为小数 !
所以我们有难点1:连续性问题意味着情况无穷多,我们无法处理。
基于此,我们需要思考如何将问题离散化。
应对策略1:连续问题离散化
从简单简单开始思考如何离散化。以m=1(参与一轮押注)为一个例子。
如果我们手头的钱 x < 500000,那么就算我们赢得胜利,我们也不能达到目标,所以我们的胜率为0。
如果我们手头的钱 500000 <= x < 1000000;那么我们不妨全压上!因为赢了我们拿一百万回家,输了就是还给节目组。所以此时我们的获胜概率为p。
如果我们手头的钱 1000000 <= x,我们就不需要参加之后的押注,或者押注为0.因为这样子我们一定能拿到1000000。所以此时概率为1.
所以我们可以得到下面的示意图:
我们再来看以m = 2为一个例子看看:
如果我们2场押注全胜!那么我们至少需要250000。所以当钱 250000 <= x < 500000时,我们的胜率为p^2。
反之,如果我们的钱 x < 250000时,我们毫无胜算,即胜率为0。
如果我们可以败一场,那么我们需要750000的钱
当 75000 <= x < 1000000时, 我们的胜率为p(1 - p) + p;
所以我们还能看到还有一个区域 500000 <= x < 75000;这个时候我们全压的胜率就是p;
有人可能就要杠了,为什么这个时候我们输一场?我只要500000保底金额。
好吧!好吧!那我们按照“杠”的思路来走一遍。如果我们输了且保底500000,那么获胜概率为
(1 - p) * p;如果我们赢了,那么我们毫无办法!这没用我还是要参加第二轮,也就是我们的胜率为p^2。两者相加你会发现概率仍然是p。所以这所谓的“容错”是假象!换种角度说,在倒数第二轮获胜了也无法决定胜局的时候,容错几乎是毫无意义。
至此我们可以画出一下的示意图:
观察图我们可以发现,当经过m轮时,会产生2^m + 1段的概率值。因为m轮,每轮会产生2种可能,随后故有2^m,此后有两种理解。理解一:因为会有全败的情况将其剔除,我们加入两种情况,不参加和参加了也是无果,即胜率为1和0的情况;理解二:全败是毫无意义的游戏,它可以代表概率为0的情况,之后加入不参加的情况。所以一共有 2^m + 1段。
现在我们将连续的情况,处理成有穷段。
这个时候我们只要算出我们的概率,在对应段落落座即可。这个时候我们的第一想法就是穷竭搜索。我们每次投资最小单元k,其中k = 1000000 / n,n = 2 ^ m;
接下来,我们需要写出状态的转移方程!也就是结构化思考后计算方程。
如何思考? 其实,我们知道一个结果,就是第m的情况数是第m-1轮的两倍。
也就是说,我们在计算第m轮的时候,借用第m-1的情况即可。
因为我们需要发生进段的过程,所以最小投资单元为k。通过j * k的枚举即可。其中如果输了要在之后的过程种赢回来,那么结果是 (1 - p) * dp[i-1][i - j];如果我们赢了,在之后的过程中达到目标则为p * dp[i-1][i + j];去最佳方案后则为 t = max{t,(1 - p) * dp[i-1][i - j] + p * dp[i-1][i + j] };
如果i - j在范围内,但i + j在范围外,那么就有(1 - p) * dp[i-1][i - j] + p;我们可以知道,我们的初始钱越多,获胜概率越大!所以dp的数据理应是单调的!所以我们dp[i-1][i - j1] > dp[i-1][i - j]。其中i + j1 = n;这就表明了,当i + j 溢出 n时接下来的所有计算时无效的。
当 i - j 越界,意味着我们没有更多的本金去支持我们做出决策,所以从此往后概率为0;
代码实现:
DFS
#include <iostream>
#include <algorithm>
#define ll long long
const int MAX_M = 15;
//滚动数组 -- dp[0][i]表示第i种情况的成功概率,dp[1][i]表示下一行第i中情况的成功概率
double dp[2][1 << MAX_M + 1];
void dfs(int round, int m, double p, double *crt, double *nxt) {
if (round == m) return;
int n = 1 << m;
for (int i = 0; i <= n; ++i)
{
int jub = std::min(i, n - i);
double t = 0.0;
//投资策略每次投入j * k(k为1000000/n,最小投资额),赢则进j段,败则退j段。
for (int j = 0; j <= jub; ++j)
{
//选出最佳的投入方案。
t = std::max(t, (1 - p) * crt[i - j] + p * crt[i + j]);
}
nxt[i] = t;
}
dfs(round + 1, m, p, nxt, crt);
return;
}
int main() {
int n, m;
double p, x;
scanf("%d %lf %lf",&m, &p, &x);
n = 1 << m;
double* crt = dp[0], * nxt = dp[1];
memset(crt, 0, sizeof(double) * (n + 1));
crt[n] = 1.0;
int i = (ll)x * n / 1000000;
dfs(0, m, p, crt, nxt);
//crt,nxt是值传入、受保护。所以最后nxt保存奇数行,crt保存偶数行
if (m % 2 == 1) printf("%lf", nxt[i]);
else printf("%lf", crt[i]);
return 0;
}
递推DP:
#include <iostream>
#include <algorithm>
#define ll long long
const int MAX_M = 15;
//滚动数组 -- dp[0][i]表示第i种情况的成功概率,dp[1][i]表示下一行第i中情况的成功概率
double dp[2][1 << MAX_M + 1];
//交换指针p1、p2
void swap(double*& p1, double*& p2) {
double * temp = p1;
p1 = p2;
p2 = temp;
return;
}
int main() {
int n, m;
double p, x;
scanf("%d %lf %lf",&m, &p, &x);
n = 1 << m;
double* crt = dp[0], * nxt = dp[1];
memset(crt, 0, sizeof(double) * (n + 1));
crt[n] = 1.0;
//参加到第 round + 1 轮的情况
for (int round = 0; round < m; ++round)
{
//第 i 种情况的成功概率
for (int i = 0; i <= n; ++i)
{
//有效计算范围
int jub = std::min(i, n - i);
double t = 0.0;
//投资策略每次投入j * k(k为1000000/n,最小投资额),赢则进j段,败则退j段。
for (int j = 0; j <= jub; ++j)
{
//选出最佳的投入方案。
t = std::max(t, (1 - p) * crt[i - j] + p * crt[i + j]);
}
nxt[i] = t;
}
swap(crt, nxt);
}
int i = (ll)x * n / 1000000;
//crt始终表示我们正在处理的行数据
printf("%lf", crt[i]);
return 0;
}