一、问题描述
小U和小S正在玩一个有趣的骰子游戏。每个骰子都有固定数量的面数k,每一面的点数分别是1到k。小U拥有n个骰子,每个骰子i的面数是 a_i,摇到每一面的概率均为 1/a_i。小S则有m个骰子,每个骰子j的面数是 b_j,摇到每一面的概率均为 1/b_j。
两人分别同时摇各自的骰子,并将摇出的点数相加,得分较高的一方获胜,得分相同则为平局。游戏只进行一次,没有重赛。现在小U想知道他获胜的概率是多少。你能帮他计算吗?(答案保留三位小数)
输入:
n:小U的骰子数量m:小S的骰子数量arrayN:小U的每个骰子面数arrayM:小S的每个骰子面数
约束条件:
n,m,k为整数,数据范围 1 ≤n,m,k≤ 25
二、整体解题分析
这段Java代码定义了一个名为 solution 的方法,该方法接受两个整数 n 和 m,以及两个整数数组 arrayN 和 arrayM 作为参数,并返回一个双精度浮点数。从代码的逻辑和结构来看,这个方法主要用于计算某种与两个骰子(由数组表示)相关的获胜概率,在主函数中给出了一个测试用例来调用 solution 方法。
三、编写过程与思路分析
- 概率计算的总体框架
public static double solution(int n, int m, int[] arrayN, int[] arrayM) {
double[] probA = calculateProbabilities(arrayN);
double[] probB = calculateProbabilities(arrayM);
double winProbability = 0.0;
for (int i = 0; i < probA.length; i++) {
for (int j = 0; j < probB.length; j++) {
if (i > j) {
winProbability += probA[i] * probB[j];
}
}
}
// 保留三位小数
return Math.round(winProbability * 1000.0) / 1000.0;
}
(1)计算单个骰子的概率分布:首先,调用 calculateProbabilities 方法分别对两个输入数组 arrayN 和 arrayM 计算概率分布,得到两个概率数组 probA 和 probB。这一步是为后续计算获胜概率做准备,通过分别计算每个骰子可能出现的各种点数之和的概率,构建了计算获胜概率的基础。
(2)计算获胜概率:定义一个变量 winProbability 初始化为0.0,然后使用嵌套的 for 循环遍历 probA 和 probB 数组。这里的逻辑是,如果 probA 对应的点数之和(索引 i)大于 probB 对应的点数之和(索引 j),那么就将这两个概率相乘并累加到 winProbability 中。这个逻辑基于一种特定的获胜规则,即当第一个骰子的点数之和大于第二个骰子的点数之和时视为获胜。
(3)结果格式化:最后,通过 Math.round(winProbability * 1000.0) / 1000.0 将计算得到的获胜概率保留三位小数并返回。这一步是为了满足特定的精度要求,确保结果的准确性和可读性。
- 单个骰子概率计算方法
private static double[] calculateProbabilities(int[] dice) {
int maxSum = Arrays.stream(dice).sum();
double[] dp = new double[maxSum + 1];
dp[0] = 1.0;
for (int die : dice) {
double[] newDp = new double[maxSum + 1];
for (int sum = 0; sum <= maxSum; sum++) {
if (dp[sum] > 0) {
for (int face = 1; face <= die; face++) {
newDp[sum + face] += dp[sum] / die;
}
}
}
dp = newDp;
}
return dp;
}
(1)确定最大点数和:首先,使用 Arrays.stream(dice).sum() 计算输入数组 dice 中所有元素的总和,这个总和表示骰子可能出现的最大点数之和。这是确定概率数组大小的依据,因为概率数组需要涵盖从0到最大点数之和的所有可能结果。 (2)初始化动态规划数组:创建一个名为 dp 的双精度浮点数数组,大小为 maxSum + 1,并将 dp[0] 初始化为1.0。这表示初始状态下,点数之和为0的概率为1.0,这是一种合理的起始状态设定,因为在没有进行任何骰子投掷之前,点数之和必然为0。 (3)动态规划计算概率:通过外层的 for 循环遍历输入数组 dice 中的每个元素(每个骰子)。对于每个骰子,创建一个新的临时双精度浮点数数组 newDp,大小同样为 maxSum + 1。然后使用嵌套的循环,内层第一个循环遍历从0到 maxSum 的所有可能点数之和 sum,如果 dp[sum] 大于0,表示当前点数之和 sum 是一个可能的状态。接着内层第二个循环遍历当前骰子的每个面(从1到 die),将 dp[sum] / die 的值累加到 newDp[sum + face] 中。这个过程是动态规划的核心,通过逐步计算每个点数之和的概率,基于前面已经计算出的概率进行更新。最后,将 newDp 赋值给 dp,为下一轮循环做准备。 (4)返回概率数组:在完成所有骰子的概率计算后,返回最终的概率数组 dp,这个数组包含了骰子可能出现的各种点数之和的概率。
- 主函数中的测试逻辑
public static void main(String[] args) {
System.out.println(solution(1, 3, new int[]{8}, new int[]{2, 3, 4}));
}
在主函数中,调用 solution 方法并传入特定的参数进行测试。这里传入了 n = 1,m = 3,一个包含8的数组和一个包含2、3、4的数组。通过将 solution 方法的返回值直接输出到控制台,可以直观地看到计算结果,从而验证代码在这个特定测试用例下的正确性。
四、总结
这段代码通过动态规划的方法计算单个骰子的概率分布,然后基于特定的获胜规则计算两个骰子之间的获胜概率,并在主函数中提供了一个测试用例来验证方法的正确性。这种方式在处理与概率计算和组合情况相关的问题时非常有效,通过逐步构建概率分布来得到最终的结果。