题目描述与示例
题目描述
米小游都快保底了还没抽到希儿,好生气哦!只能打会活动再拿点水晶。
米小游和世界第一可爱的魔法少女 TeRiRi 正在打 BOSS,BOSS 的血量为h
,当 BOSS 血量小于等于0
时,BOSS 死亡。TeRiRi 有一套牌,在一轮中,她会按顺序一张一张的将卡牌打出,套牌中有两种卡牌:
- 时来运转 :获得
x
个幸运币。 - 幸运一掷 :造成
x
点伤害,并投掷所有幸运币 ,造成等于所有幸运币掷出的点数之和的伤害。
幸运币 可以等概率的投掷出1∼6
之间的点数。 (所以为什么不叫骰子呢?)
米小游想知道,TeRiRi 的套牌在一轮内击杀 BOSS 的概率。
输入描述
第一行输入两个整数n (1≤n≤100)
,h (1≤h≤10^9)
,分别表示卡牌张数和 BOSS 血量。
接下来n
行,每行首先输入两个整数t (1≤t≤2)
,x (1≤x≤10)
,t
为1
表示卡牌为时来运转,t
为2
表示卡牌为幸运一掷。
输出描述
输出一个实数表示答案,你的答案与标准答案的误差不超过10^−4
都被认为是正确答案。
示例一
输入
Plain
2 5
1 1
2 1
输出
Plain
0.5
说明
幸运币掷出4
及以上的概率为0.5
,再加上1
点固定伤害,即可击杀BOSS。
示例二
输入
Plain
3 1145
1 4
1 9
1 9
输出
Plain
0
说明
无论如何都无法击杀BOSS。
解题思路
对于固定顺序的套牌,投掷幸运币的数量是固定的。这里要注意的是,由于时来运转 之后必须接上幸运一掷 才能将幸运币打出造成伤害,所以如果最后的若干张连续的卡牌是时来运转,这些最后获得的幸运币也是无法造成伤害的。
我们将造成的伤害分为两部分,固定伤害和随机伤害 ,前者为打出y
个幸运币必定造成的z
点伤害,后者为y
个幸运币掷出点数和的伤害。
假设整套卡牌一共投掷了y
个幸运币,造成的固定伤害 为z
点,如果想要击杀BOSS,随机伤害必须至少达到h-z
点才可以。当然,如果h-z≤0
,则必定可以击杀BOSS。
问题就转换为,投掷出 y
个幸运币,点数总和超过 h-z
的概率是多少?
由于每一个幸运币都是独立 的,在掷出第i
个幸运币时,其结果是从掷出第i-1
个幸运币时得到的各种结果转移得到的,因此我们可以使用动态规划来解决该问题。我们考虑动态规划三部曲:
dp
数组的含义是什么?
dp
数组是一个长度为(y+1)×(h-z+1)
的二维矩阵,dp[i][j]
表示掷出第i
个幸运币时,有多大的概率 可以取得和为j
的结果,即造成和为j
的伤害。- 特别地,由于只需要判断伤害之和大于等于
h-z
的概率,而不用关心具体的分布,dp
数组内层的第h-z
个元素,即dp[i][h-z]
,表示求和大于等于h-z
的概率。
- 动态转移方程是什么?
- 由于幸运币掷出点数
1-6
是等概率的,故对于某一个特定的dp[i-1][j]
,在掷出第i
个幸运币时,dp[i-1][j]
的结果将等概率地转换到dp[i][j+1]
,dp[i][j+2]
,dp[i][j+3]
,dp[i][j+4]
,dp[i][j+5]
,dp[i][j+6]
,即每一个状态都可以取得1/6
的转移。 - 另外,如果
j+k
之后超过了h-z
,则将直接获得(7-k)/6 * dp[i-1][j]
的概率。
Python
for i in range(1, y+1):
for j in range(i-1, h-z+1):
for k in range(1, 7):
if j + k >= h - z:
dp[i][h-z] += (7-k)/6 * dp[i-1][j]
break
else:
dp[i][j+k] += 1/6 * dp[i-1][j]
dp
数组如何初始化?
- 考虑不投掷任何幸运币的情况,那么只有一种情况,也就是在投掷
0
个幸运币的时候获得求和为0
的概率为恒定1
。故初始化dp[0][0] = 1
Python
dp = [[0] * (h-z+1) for _ in range(y+1)]
dp[0][0] = 1
考虑完上述问题后,代码其实呼之欲出了。
代码
Python
Python
# 题目:【DP】米哈游2023秋招-米小游与魔法少女-奇运
# 作者:闭着眼睛学数理化
# 算法:DP
# 代码有看不懂的地方请直接在群上提问
y = 0 # 掷出幸运币的总个数
z = 0 # 全部造成的固定伤害
x_temp = 0 # 时来运转获得的幸运币
n, h = map(int, input().split())
for _ in range(n):
t, x = map(int, input().split())
# 时来运转
if t == 1:
x_temp += x
# 幸运一掷
else:
y += x_temp
x_temp = 0
z += x
# 如果固定伤害已经大于h,直接输出1
if h - z <= 0:
print(1)
# 否则才需要进行dp过程
else:
# 初始化dp数组
# dp[i][j]表示掷出了i个幸运币时,
# 有多大的概率可以取得和为j的结果,即造成和为j的伤害。
dp = [[0] * (h-z+1) for _ in range(y+1)]
dp[0][0] = 1
# 考虑每一个幸运币
for i in range(1, y+1):
# 对于每一个幸运币考虑打出i-1个硬币后的
# 每一种求和结果的概率
# 注意,由于已经掷出了i-1个幸运币
# 那么求和结果至少为i-1,因为每个幸运币点数至少为1点
# 因此j遍历时起点可以从i-1开始
for j in range(i-1, h-z+1):
# 如果求和j尚未在上一次投掷中取得,
# 则可以直接考虑下一个幸运币
if dp[i-1][j] == 0:
break
# 遍历掷出六种不同点数k的情况,
# 当前点数则可以取得j+k
for k in range(1, 7):
# 如果当前点数j+k超过了击杀所需点数
# 则更新dp[i][h-z]
# 为dp[i-1][j]对应的概率乘以(7-k)/6
if j + k >= h - z:
dp[i][h-z] += (7-k)/6 * dp[i-1][j]
break
# 如果当前点数j+k尚未超过击杀所需点数
# 则其概率由dp[i-1][j]六等分后转移得到
else:
dp[i][j+k] += 1/6 * dp[i-1][j]
# 输出最后一行的最后一个元素
# 表示打出第y个幸运币后,造成伤害大于等于h-z点的概率
print(dp[y][h-z])
Java
Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int y = 0; // 掷出幸运币的总个数
int z = 0; // 全部造成的固定伤害
int x_temp = 0; // 时来运转获得的幸运币
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int h = scanner.nextInt();
for (int i = 0; i < n; i++) {
int t = scanner.nextInt();
int x = scanner.nextInt();
// 时来运转
if (t == 1) {
x_temp += x;
}
// 幸运一掷
else {
y += x_temp;
x_temp = 0;
z += x;
}
}
// 如果固定伤害已经大于h,直接输出1
if (h - z < 0) {
System.out.println("1");
}
// 否则才需要进行dp过程
else {
// 初始化dp数组
// dp[i][j]表示掷出了i个幸运币时,
// 有多大的概率可以取得和为j的结果,即造成和为j的伤害。
double[][] dp = new double[y + 1][h - z + 1];
dp[0][0] = 1.0;
// 考虑每一个幸运币
for (int i = 1; i <= y; i++) {
// 对于每一个幸运币考虑打出i-1个硬币后的
// 每一种求和结果的概率
// 注意,由于已经掷出了i-1个幸运币
// 那么求和结果至少为i-1,因为每个幸运币点数至少为1点
// 因此j遍历时起点可以从i-1开始
for (int j = i - 1; j <= h - z; j++) {
// 如果求和j尚未在上一次投掷中取得,
// 则可以直接考虑下一个幸运币
if (dp[i - 1][j] == 0) {
break;
}
// 遍历掷出六种不同点数k的情况,
// 当前点数则可以取得j+k
for (int k = 1; k <= 6; k++) {
// 如果当前点数j+k超过了击杀所需点数
// 则更新dp[i][h-z]
// 为dp[i-1][j]对应的概率乘以(7-k)/6
if (j + k >= h - z) {
dp[i][h - z] += (7 - k) / 6.0 * dp[i - 1][j];
break;
}
// 如果当前点数j+k尚未超过击杀所需点数
// 则其概率由dp[i-1][j]六等分后转移得到
else {
dp[i][j + k] += 1.0 / 6.0 * dp[i - 1][j];
}
}
}
}
// 输出最后一行的最后一个元素
// 表示打出第n个幸运币后,造成伤害大于等于h-z点的概率
System.out.println(String.format("%.5f", dp[y][h - z]));
}
}
}
C++
C++
#include <iostream>
#include <vector>
#include <iomanip>
using namespace std;
int main() {
int y = 0; // 掷出幸运币的总个数
int z = 0; // 全部造成的固定伤害
int x_temp = 0; // 时来运转获得的幸运币
int n, h;
cin >> n >> h;
for (int i = 0; i < n; i++) {
int t, x;
cin >> t >> x;
// 时来运转
if (t == 1) {
x_temp += x;
}
// 幸运一掷
else {
y += x_temp;
x_temp = 0;
z += x;
}
}
// 如果固定伤害已经大于h,直接输出1
if (h - z < 0) {
cout << fixed << setprecision(10) << 1 << endl;
}
// 否则才需要进行dp过程
else {
// 初始化dp数组
// dp[i][j]表示掷出了i个幸运币时,
// 有多大的概率可以取得和为j的结果,即造成和为j的伤害。
vector<vector<double>> dp(y + 1, vector<double>(h - z + 1, 0));
dp[0][0] = 1.0;
// 考虑每一个幸运币
for (int i = 1; i <= y; i++) {
// 对于每一个幸运币考虑打出i-1个硬币后的
// 每一种求和结果的概率
// 注意,由于已经掷出了i-1个幸运币
// 那么求和结果至少为i-1,因为每个幸运币点数至少为1点
// 因此j遍历时起点可以从i-1开始
for (int j = i - 1; j <= h - z; j++) {
// 如果求和j尚未在上一次投掷中取得,
// 则可以直接考虑下一个幸运币
if (dp[i - 1][j] == 0) {
break;
}
// 遍历掷出六种不同点数k的情况,
// 当前点数则可以取得j+k
for (int k = 1; k <= 6; k++) {
// 如果当前点数j+k超过了击杀所需点数
// 则更新dp[i][h-z]
// 为dp[i-1][j]对应的概率乘以(7-k)/6
if (j + k >= h - z) {
dp[i][h - z] += (7 - k) / 6.0 * dp[i - 1][j];
break;
}
// 如果当前点数j+k尚未超过击杀所需点数
// 则其概率由dp[i-1][j]六等分后转移得到
else {
dp[i][j + k] += 1.0 / 6.0 * dp[i - 1][j];
}
}
}
}
// 输出最后一行的最后一个元素
// 表示打出第n个幸运币后,造成伤害大于等于h-z点的概率
cout << fixed << setprecision(5) << dp[y][h - z] << endl;
}
return 0;
}
时空复杂度
时间复杂度:O(yh)
。其中y
为投掷出的幸运币的总数,h
为BOSS总血量,dp
过程需要进行双重循环。
空间复杂度:O(yh)
。dp
数组所占空间。如果使用滚动dp,空间复杂度可以降低到O(h)
华为OD算法冲刺训练
华为OD算法冲刺训练目前开始常态化报名!目前已服务100+同学成功上岸!
课程讲师为全网50w+粉丝编程博主@吴师兄学算法 以及小红书头部编程博主@闭着眼睛学数理化
每期人数维持在20人内,保证能够最大限度地满足到每一个同学的需求,达到和1v1同样的学习效果!
30+天陪伴式学习,20+直播课时,300+动画图解视频,200+LeetCode经典题,100+华为OD真题,还有简历修改与模拟面试将为你解锁
可查看链接 OD算法冲刺训练课程表 & OD真题汇总(持续更新)
绿色聊天软件戳 od1336了解更多