游戏中的随机——“动态平衡概率”算法

前言

众所周知计算机模拟的随机是伪随机,但在结果看来依然和现实中的随机差别不大。
例如掷硬币,连续掷很多很多次之后,总有连续七八十来次同一个面朝上的情况出现,计算机中一般的随机函数也能很好模拟这一点。

但在游戏中,假如有一个50%概率会出现的情况,经常连续七八十来次不出现,这样其实非常影响游戏体验。

那么为了增加这部分游戏体验,我们如何避免上述情况发生,使某个概率能在总体上较为均匀地分布呢?

例如现在有这样的需求:

A. 暴击率总体为20%
B. 要求每十次攻击,至少有一次暴击
C. 要求暴击的总体分布较为均匀

算法预览

经过一段时间的深思熟虑,笔者终于构建了一种名为“动态平衡概率”的算法。
虽然它还有一些局限性,但已经达到了基本可用的状态。

先上代码,为了方便演示图表,这里就用 python 了:

import matplotlib.pyplot as plt
import random# 初始化变量
InitCritPercent = 0.2       # 初始暴击率
dynamicCritPercent = 0.2    # 动态暴击率
currentCritPercent = 0      # 当前暴击概率
deltaCritPercent = 0        # 当前暴击率与初始暴击率的差值(用来表示变化)
attackTotalCount = 0        # 总攻击次数
critTotalCount = 0          # 总暴击次数
noCritStreakCount = 0       # 连续未暴击次数# 给 plot 准备的列表
currentCritPercentList = []
deltaCritPercentList = []
dynamicCritPercentList = []
noCritStreakCountList = []
isCriticalList = []# 获取最佳的 N
def find_optimal_N(p):one_minus_p = 1 - pfor i in range(1, 501):if one_minus_p ** i <= 0.05:return ireturn 500  # 如果未找到合适的 N,则默认返回 500# 测试 10000 次
for i in range(10000):# 核心代码 ↓attackTotalCount += 1isCritical = False# 检查当前攻击数是否大于 0if attackTotalCount > 0:# 计算当前暴击概率currentCritPercent = critTotalCount / attackTotalCount# 计算当前暴击概率与初始暴击率的差值deltaCritPercent = abs(InitCritPercent - currentCritPercent)# 计算动态暴击率dynamicCritPercent = (attackTotalCount * (InitCritPercent - currentCritPercent) + currentCritPercent) * pow(deltaCritPercent, 0.5)# 检查是否连续 N - 1 次未暴击if noCritStreakCount < find_optimal_N(InitCritPercent) - 1:percent = random.random()if percent <= dynamicCritPercent:isCritical = TruenoCritStreakCount = 0else:noCritStreakCount += 1else:isCritical = TruenoCritStreakCount = 0if isCritical:critTotalCount += 1# 核心代码 ↑# 将数据添加到列表中currentCritPercentList.append(currentCritPercent)deltaCritPercentList.append(deltaCritPercent)dynamicCritPercentList.append(dynamicCritPercent)noCritStreakCountList.append(noCritStreakCount)isCriticalList.append(int(isCritical))# 创建多表格
fig, axs = plt.subplots(2)# 每 100 条数据标注一下
for i in range(0, len(currentCritPercentList), 100):axs[0].annotate(f"{currentCritPercentList[i]:.3f}", (i, currentCritPercentList[i]))# 画出暴击概率数据表格
axs[0].plot(currentCritPercentList, label='Current Crit Percent', color='r')
axs[0].plot(deltaCritPercentList, label='Delta Crit Percent', color='g')
axs[0].plot(dynamicCritPercentList, label='Dynamic Crit Percent', color='b')
axs[0].set_xlabel('Total Attacks')
axs[0].set_ylabel('Probability')
axs[0].legend()# 画出连续未暴击次数的表格
axs[1].plot(noCritStreakCountList, label='No-Crit Streak', color='m')
axs[1].plot(isCriticalList, label='Is Critical', color='c')
axs[1].set_xlabel('Total Attacks')
axs[1].set_ylabel('No-Crit Streak / Is Critical')
axs[1].legend()plt.show()

给定参数的运行结果如下图所示(这里的“要求N次攻击,至少有一次暴击”中的N,根据算法取了14)
反向 目标0.2 次数14 倍率差值开平方 无限制1
0 ~ 2000 次 如下
请添加图片描述
8000 ~ 10000 次 如下
请添加图片描述
可以看出,总体暴击率会在大概300次内稳定下来,并且逐渐逼近 0.2;
在攻击次数足够多时,“动态暴击率”的浮动也会趋于稳定。

这是一种通过调整每次攻击的暴击率,来达到动态平衡效果的算法;
也可以说,这是一种动态调整每次概率,以达到目标数学期望的算法。

核心思路

以“暴击率”为例,以下是这种“动态平衡概率”算法的核心思路:

基本参数:
初始概率(目标概率) : P 动态概率 : d y n a m i c P 当前概率 : c u r r e n t P 概率差值 : d e l t a P 攻击次数 : a t t a c k N 暴击次数 : c r i t N 连续未暴击次数 : n o C r i t S t r e a k \begin{align*} \text{初始概率(目标概率)} & :P \\ \text{动态概率} & :dynamicP \\ \text{当前概率} & :currentP \\ \text{概率差值} & :deltaP \\ \text{攻击次数} & :attackN \\ \text{暴击次数} & :critN \\ \text{连续未暴击次数} & :noCritStreak \\ \end{align*} 初始概率(目标概率)动态概率当前概率概率差值攻击次数暴击次数连续未暴击次数PdynamicPcurrentPdeltaPattackNcritNnoCritStreak

核心运算逻辑:
c u r r e n t P = c r i t N a t t a c k N d e l t a P = ∣ P − c u r r e n t P ∣ d y n a m i c P = ( a t t a c k N ⋅ ( P − c u r r e n t P ) + c u r r e n t P ) ⋅ d e l t a P \begin{align*} currentP &= \frac{critN}{attackN} \\ deltaP &= |P - currentP| \\ dynamicP &= \left( attackN · (P - currentP) + currentP \right) · \sqrt{deltaP} \\ \end{align*} currentPdeltaPdynamicP=attackNcritN=PcurrentP=(attackN(PcurrentP)+currentP)deltaP

暴击判断逻辑:
找到一个最佳的N, 用于判断连续 N - 1 次未暴击 : Find_Optimal_N ( p ) : ( 1 − p ) N ≤ 0.05 随机数生成和暴击判断 : 如果  n o C r i t S t r e a k < N − 1 ,则生成一个随机数  p e r c e n t ; ﹂如果  p e r c e n t ≤ d y n a m i c P ,则判定为暴击,相关参数 + 1 ﹂否则 未暴击,相关参数 + 1 否则 必然暴击,相关参数 + 1 \begin{align*} \\ \text{找到一个最佳的N,} \\ \text{用于判断连续 N - 1 次未暴击} & : \\ \text{Find\_Optimal\_N}(p) & : (1 - p) ^ N \leq 0.05 \\ \\ \text{随机数生成和暴击判断} & : \\ & \text{如果 \(noCritStreak\) \( < N - 1 \),则生成一个随机数 \(percent\);} \\ & \text{ ﹂如果 \(percent\) \( \leq \) \(dynamicP\),则判定为暴击,相关参数 + 1} \\ & \text{ ﹂否则 未暴击,相关参数 + 1} \\ & \text{否则 必然暴击,相关参数 + 1} \\ \end{align*} 找到一个最佳的N用于判断连续 N - 1 次未暴击Find_Optimal_N(p)随机数生成和暴击判断::(1p)N0.05:如果 noCritStreak <N1,则生成一个随机数 percent ﹂如果 percent  dynamicP,则判定为暴击,相关参数 + 1 ﹂否则 未暴击,相关参数 + 1否则 必然暴击,相关参数 + 1

本文到这里其实就结束了,这套算法虽然简单,但是笔者发现它的过程还是挺有意思的。
感兴趣的朋友可以继续往下看,文末还有一些优化思路…

发现

还是前文中的需求:

A. 暴击率总体为20%
B. 要求每十次攻击,至少有一次暴击
C. 要求暴击的总体分布较为均匀

假如每次暴击的概率都是0.2,并且每十次攻击至少一次暴击,这样相当于增加了总体最终的暴击数,也就是变相增加了暴击率,确实需要通过某种方式将最终结果调整到0.2.

目前笔者想到的实现方式大致分为两种:

一种是“动态概率”,我们可以随着实际已出现的概率,动态地调整下一次的概率,并保证在最终结果上符合我们的目标概率。
另一种是提前将“随机种子”做好。在制作“种子”时使用连续分段的、适当长度的数组,每段数组中目标出现的概率基本相同,且总体概率符合我们的目标概率。再人为打乱每段数组,最后将他们拼接起来。但是这种方式还有个问题,就是打乱数组之后可能会出现两个数组中的一个暴击在头一个在尾,两次暴击又会间隔较远的情况,无法完全保证 B 条件成立。

本文先尝试第一种方式————“动态概率”

以前面的需求为例,假如每次暴击的概率都是0.2,并且每十次攻击至少一次暴击,先这样在Unity中看一下最终的暴击率会高出多少

using UnityEngine;public class CriticalHit : MonoBehaviour
{// 初始暴击率public float InitCritPercent = 0.2f;// 当前暴击概率private float currentCritPercent;// 当前总攻击次数private int attackTotalCount = 0;// 当前总暴击过的次数private int critTotalCount = 0;// 连续未出现暴击的次数private int noCritStreakCount = 0;private void Start(){currentCritPercent = InitCritPercent;}private void Update(){// 监听鼠标左键输入if (Input.GetMouseButtonDown(0)){// 测试一次PerformAttack();Debug.Log("当前暴击率:" + currentCritPercent);}if (Input.GetKeyDown(KeyCode.Space)){// 测试一万次for (int i = 0; i < 10000; i++) PerformAttack();}}private void PerformAttack(){attackTotalCount++;bool isCritical = false;if (attackTotalCount > 0){// 计算当前暴击概率 = 总暴击数 / 总攻击数currentCritPercent = (float)critTotalCount / attackTotalCount;}// 检查是否需要强制暴击if (noCritStreakCount < 9){float percent = Random.Range(0f, 1f);if (percent < InitCritPercent){isCritical = true;noCritStreakCount = 0; // 重置计数器}else{noCritStreakCount++;}}else{isCritical = true;noCritStreakCount = 0; // 重置计数器}if (isCritical) critTotalCount++;// 执行攻击,如果 isCritical 为 true,则为暴击if (isCritical)Debug.Log("Critical Hit!");elseDebug.Log("Normal Hit.");}
}

将这个脚本挂到场景中的空物体上,运行游戏,然后按空格键先测试一万次,再点击鼠标左键显示当前的暴击率
用上述方式测试几次,会发现最终的暴击率大概在 22.5% 左右,打印结果如下图所示
在Unity中测试1请添加图片描述

那么这多出来的 2.5% 为什么会是 2.5% 呢,它具体是怎么来的呢,如何避免它产生呢?

带着这样的疑惑,笔者开始尝试进行分析…

排除误差的可能

首先我们要排除这 2.5% 是误差的可能。

假设暴击率为 0.2,不考虑其他的设定和限制,每次测试十万次、共测试三次。
那么正常情况下的输出结果如下图所示
排除误差1
请添加图片描述
误差在 0.2% 左右,这与 2.5% 差别还是很大的,所以基本排除这是误差导致的情况。

探索

为了进一步优化算法,笔者决定结合已有的数据和个人直觉进行改进。

笔者用Python重新编写了一版代码,这样我们不仅可以方便地输出图表进行可视化分析,还能在这个基础上进行后续的代码修改和优化。

import matplotlib.pyplot as plt
import random# 初始化变量
InitCritPercent = 0.2   # 初始暴击率
attackTotalCount = 0    # 总攻击次数
critTotalCount = 0      # 总暴击次数
noCritStreakCount = 0   # 连续未暴击次数# 给 plot 准备的列表
currentCritPercentList = []
noCritStreakCountList = []
isCriticalList = []# 测试 10000 次
for i in range(10000):attackTotalCount += 1isCritical = False# 检查是否连续 9 次未暴击if noCritStreakCount < 9:percent = random.random()if percent <= InitCritPercent:isCritical = TruenoCritStreakCount = 0else:noCritStreakCount += 1else:isCritical = TruenoCritStreakCount = 0if isCritical:critTotalCount += 1# 计算当前暴击概率currentCritPercent = critTotalCount / attackTotalCount# 添加数据到列表中currentCritPercentList.append(currentCritPercent)noCritStreakCountList.append(noCritStreakCount)isCriticalList.append(int(isCritical))# 创建多表格
fig, axs = plt.subplots(2)# 画出暴击概率数据表格
axs[0].plot(currentCritPercentList, label='Current Crit Percent', color='r')
axs[0].set_xlabel('Total Attacks')
axs[0].set_ylabel('Probability')
axs[0].legend()# 每 100 条数据标注一下
for i in range(0, len(currentCritPercentList), 100):axs[0].annotate(f"{currentCritPercentList[i]:.5f}", (i, currentCritPercentList[i]))# 画出连续未暴击次数的表格
axs[1].plot(noCritStreakCountList, label='No-Crit Streak', color='m')
axs[1].plot(isCriticalList, label='Is Critical', color='c')
axs[1].set_xlabel('Total Attacks')
axs[1].set_ylabel('No-Crit Streak / Is Critical')
axs[1].legend()plt.show()

从输出的图表中不难看出,整体的暴击率确实变高了,如下图所示

前 2000 次 如下
无动态概率调整1
8000 ~ 10000 次 如下
请添加图片描述

如要将最终的暴击概率调整回 0.2,那就应该降低“当前暴击概率”,将 B 条件所增加的那部分修正回来。

“递增修正”

将前文的python代码添加几个变量,用来检测当前暴击概率的变化,当前暴击概率高于初始暴击率的时候,就降低动态暴击率,直到将当前暴击率拉回到正常水平;反之亦然。

import matplotlib.pyplot as plt
import random# 初始化变量
InitCritPercent = 0.2       # 初始暴击率
currentCritPercent = 0      # 当前暴击概率
deltaCritPercent = 0        # 当前暴击率与初始暴击率的差值(用来表示变化)
dynamicCritPercent = 0.2    # 动态暴击率
attackTotalCount = 0        # 总攻击次数
critTotalCount = 0          # 总暴击次数
noCritStreakCount = 0       # 连续未暴击次数# 给 plot 准备的列表
currentCritPercentList = []
deltaCritPercentList = []
dynamicCritPercentList = []
noCritStreakCountList = []
isCriticalList = []# 测试 10000 次
for i in range(10000):attackTotalCount += 1isCritical = False# 检查是否连续 9 次未暴击if attackTotalCount > 0:# 计算当前暴击概率currentCritPercent = critTotalCount / attackTotalCount# 计算当前暴击概率与初始暴击率的差值deltaCritPercent = abs(InitCritPercent - currentCritPercent)# 计算动态暴击率if(currentCritPercent > InitCritPercent):dynamicCritPercent -= deltaCritPercentif(currentCritPercent < InitCritPercent):dynamicCritPercent += deltaCritPercent# 检查是否连续 9 次未暴击if noCritStreakCount < 9:percent = random.random()if percent <= dynamicCritPercent:isCritical = TruenoCritStreakCount = 0else:noCritStreakCount += 1else:isCritical = TruenoCritStreakCount = 0if isCritical:critTotalCount += 1# 将数据添加到列表中currentCritPercentList.append(currentCritPercent)deltaCritPercentList.append(deltaCritPercent)dynamicCritPercentList.append(dynamicCritPercent)noCritStreakCountList.append(noCritStreakCount)isCriticalList.append(int(isCritical))# 创建多表格
fig, axs = plt.subplots(2)# 每 100 条数据标注一下
for i in range(0, len(currentCritPercentList), 100):axs[0].annotate(f"{currentCritPercentList[i]:.3f}", (i, currentCritPercentList[i]))# 画出暴击概率数据表格
axs[0].plot(currentCritPercentList, label='Current Crit Percent', color='r')
axs[0].plot(deltaCritPercentList, label='Delta Crit Percent', color='g')
axs[0].plot(dynamicCritPercentList, label='Dynamic Crit Percent', color='b')
axs[0].set_xlabel('Total Attacks')
axs[0].set_ylabel('Probability')
axs[0].legend()# 画出连续未暴击次数的表格
axs[1].plot(noCritStreakCountList, label='No-Crit Streak', color='m')
axs[1].plot(isCriticalList, label='Is Critical', color='c')
axs[1].set_xlabel('Total Attacks')
axs[1].set_ylabel('No-Crit Streak / Is Critical')
axs[1].legend()plt.show()

输出结果如下图所示
累计 目标0.2 次数10 无限制1
前 2000 次 如下
请添加图片描述

可以明显看出动态暴击率在大幅度地反复震荡,并且明显超出了 (0, 1) 的区间;
在震荡的高点时,会出现连续暴击的情况;在震荡的低点时,会出现连续地触发“保底”暴击;
这样虽然能将总体暴击概率稳定在 0.2 左右,但这显然不满足条件 C。

“递增修正”优化

显而易见,当动态暴击率超出 (0, 1) 区间时,就和 0、1 没有区别了
所以可以为它加个简单限幅,例如笔者将动态暴击率的幅度限制在(0.5倍初始暴击率,2倍初始暴击率)之间

# 同上文代码# 测试 10000 次
for i in range(10000):# 同上文代码if attackTotalCount > 0:# 同上文代码# 计算动态暴击率if(currentCritPercent > InitCritPercent):dynamicCritPercent = min(max(dynamicCritPercent - deltaCritPercent, InitCritPercent * 0.5), InitCritPercent * 2)if(currentCritPercent < InitCritPercent):dynamicCritPercent = min(max(dynamicCritPercent + deltaCritPercent, InitCritPercent * 0.5), InitCritPercent * 2)# 检查是否连续 9 次未暴击if noCritStreakCount < 9:# 同上文代码# 同上文代码# 同上文代码

输出结果如下图所示
累计 目标0.2 次数10 限制0.5-2倍1
前 2000 次 如下
请添加图片描述
8000 ~ 10000 次 如下
请添加图片描述

现在的算法已经基本可用了,但还需要多尝试才能找到合适的限幅范围。
当限幅范围过大时,概率的分布会变得不均匀;
限幅范围过小时,又会出现无法逼近目标概率(初始暴击率),比较麻烦。

“递增修正”测试

将上述优化过的算法应用到其他情景中,例如掷硬币,每5次投掷至少有一次正面
初始概率(目标概率) = 0.5

# 同上文代码
InitCritPercent = 0.5
dynamicCritPercent = 0.5
# 同上文代码# 测试 10000 次
for i in range(10000):# 同上文代码# 检查是否连续 4 次未掷出正面if noCritStreakCount < 4:# 同上文代码# 同上文代码# 同上文代码

输出结果如下图所示
累计 目标0.5 次数5 限制0.5-2倍1
前 2000 次 如下
请添加图片描述
8000 ~ 10000 次 如下
请添加图片描述

可以发现出现连续未正面的次数(连续未暴击次数),又在动态概率的波谷处出现“聚拢”现象,这很好理解:因为我们的限幅有些过大了。
总结下来,这种手动限定幅度的方式效率很低还容易出问题…

那么能不能让它根据自身目前状况,如目标概率、总攻击次数等参数,来动态调整 动态暴击率的增量呢?

“镜像修正”

基于以上思考,笔者希望每次攻击的“动态暴击率”是上次“当前暴击概率”关于“初始暴击率”的镜像,通过这种有针对性的“反向”操作,来将最终暴击率逼近目标值。
于是便有如下代码:

# 初始化变量
InitCritPercent = 0.2       # 初始暴击率
dynamicCritPercent = 0.2    # 动态暴击率
# 同上文代码# 测试 10000 次
for i in range(10000):# 同上文代码if attackTotalCount > 0:# 同上文代码# 计算动态暴击率dynamicCritPercent = attackTotalCount * InitCritPercent - (attackTotalCount - 1) * currentCritPercent# 检查是否连续 9 次未暴击if noCritStreakCount < 9:# 同上文代码# 同上文代码# 同上文代码

输出结果如下图所示
反向 目标0.2 次数10 无限制 无限制1
前 2000 次 如下
请添加图片描述
8000 ~ 10000 次 如下
请添加图片描述

虽然能将最终的暴击概率稳定在 0.2,但结果过于平均了!
可以说这种“修正”的操作过于灵敏,导致暴击的分布非常均匀,甚至没有出现连续 9 次以上的未暴击。但这仍不是我们想要的,需要继续优化。

“镜像修正”优化

笔者发现,这种“过于均匀”的分布情况也是因为每次修正幅度过大导致的。
现在要调整这个幅度会比“递增修正”的方法容易很多,只需要让“计算动态暴击率”的结果乘以一个较小的系数即可。

这个系数需要与当前的状态有关,并且是一个越来越小的值。
而在攻击次数越来越多时,currentCritPercent 也会越来越逼近 InitCritPercent 的值,所以 deltaCritPercent 会随着攻击次数的增多越来越小;
(又因为 currentCritPercent 趋向于一个比 InitCritPercent 偏大的值,那么 deltaCritPercent 也会永不为 0)
这里我们就用 deltaCritPercent 来作为系数,目前来看刚好合适。

# 同上文代码# 计算动态暴击率dynamicCritPercent = (attackTotalCount * (InitCritPercent - currentCritPercent) + currentCritPercent) * deltaCritPercent# 同上文代码

输出结果如下图所示
反向 目标0.2 次数10 倍率差值 无限制1
前 2000 次 如下
请添加图片描述
8000 ~ 10000 次 如下
请添加图片描述

由于对每次的 dynamicCritPercent 的幅度都做了差不多的限制,可以看到图二中,在前 1000 次左右攻击时,currentCritPercent 逼近目标值的速度很慢。
啧,还差一点…

继续优化!既然 deltaCritPercent 会随着攻击次数增多变得越来越小,那么我们不妨直接将它放大。

# 同上文代码# 计算动态暴击率dynamicCritPercent = (attackTotalCount * (InitCritPercent - currentCritPercent) + currentCritPercent) * pow(deltaCritPercent, 0.5)# 同上文代码

输出结果如下图所示
反向 目标0.2 次数10 倍率差值开平方 无限制1
前 2000 次 如下
请添加图片描述
8000 ~ 10000 次 如下
请添加图片描述

以上结果已经基本符合预期。

“镜像修正”测试

掷硬币

下面还是用硬币的例子:掷硬币,每5次投掷至少有一次正面
初始概率(目标概率) = 0.5

# 同上文代码
InitCritPercent = 0.5
dynamicCritPercent = 0.5
# 同上文代码# 测试 10000 次
for i in range(10000):# 同上文代码# 检查是否连续 4 次未掷出正面if noCritStreakCount < 4:# 同上文代码# 同上文代码# 同上文代码

输出结果如下图所示
反向 目标0.5 次数5 倍率差值开平方 无限制1
前 2000 次 如下
请添加图片描述
8000 ~ 10000 次 如下
请添加图片描述

也基本符合预期。

掷骰子

再以掷骰子为例:每掷出 15 次至少有一次是 点数 1。

# 同上文代码
InitCritPercent = 0.166667
dynamicCritPercent = 0.166667
# 同上文代码# 测试 10000 次
for i in range(10000):# 同上文代码# 检查是否连续 14 次未掷出正面if noCritStreakCount < 14:# 同上文代码# 同上文代码# 同上文代码

输出结果如下图所示
反向 目标0.166667 次数15 倍率差值开平方 无限制1
前 2000 次 如下
请添加图片描述
8000 ~ 10000 次 如下
请添加图片描述

稳定发挥。

优化

目前“镜像修正”算法已经基本可用了,但是虽然叫“镜像”,却已经没有了镜像当初的样子。

不如就直接改名叫“动态平衡概率”算法好了…

算法优化

细心的朋友应该会发现,这套算法在一开始的概率会低于目标概率一些,并且逼近的速度还是慢了些。后期稳定性也没有想象中的高。

笔者目前能想到的继续优化的方式有三种:

1.分段修改 deltaCritPercent 的开根,类似LOD模型替换的感觉;
2.用 log 函数做系数,然后当次数达到一定值时直接 * deltaCritPercent 就可以了;
3.按目标概率的比例,给“总攻击次数”和“总暴击次数”设置较大的初始值。这样不用给 deltaCritPercent 开平方,就能得到一个较为满意的结果,也会相对高效一些。

笔者还没来得及测试性能,如果后续有相关优化会修改本文章,或者发一篇新文章。

关于判断次数

我们感觉到的小概率事件发生的概率通常在 5% 或 1% 以下,通过这两个标准,我们可以很轻松地得出“目标概率为 X 时,操作 N 次至少出现一次目标事件”中的N:

def find_optimal_N(p):# 从 1 到 500for i in range(1, 501):if(1 - p) ** i <= 0.05:return iprint(find_optimal_N(0.2))
print(find_optimal_N(0.5))
print(find_optimal_N(0.166667))# 输出结果为:
# 14
# 5
# 17

所以当目标概率为 0.2、0.5、0.166667 时,N 比较合适的值为 14、5、17。
当目标概率小于 0.05 时,可以让if(1 - p) ** i <= 0.01:,或者更小。

结语

虽然本算法目前还有待优化,但已经足够应对一些游戏场景。
关于那多出的2.5%的问题,笔者会继续探索,直到找到满意的答案。

如果这篇文章能为你解决问题或带来新的启发,那我会感到非常荣幸!

对于已经在这个领域有丰富经验的大佬们,非常欢迎你们的建议或批评。这不仅能帮助我改进,也能让这篇文章更加完善,从而帮助到更多的人。

感谢你抽出宝贵的时间来阅读这篇文章,如果你觉得有用,也请不吝分享给更多需要的人。

再次感谢,期待我们在知识的海洋里再次相遇!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/158136.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用ChatGPT和MindShow一分钟生成PPT模板

对于最近学校组织的实习答辩&#xff0c;由于时间太短了&#xff0c;而且小编也特别的忙&#xff0c;于是就用ChatGPT结合MindShow一分钟快速生成PPT&#xff0c;确实很实用。只要你跟着小编后面&#xff0c;你也可以快速制作出这个PPT&#xff0c;下面小编就来详细介绍一下&am…

C语言中的文件操作函数

C语言中的文件操作函数_c语言文件操作函数_点子李的博客-CSDN博客C语言文件操作_c语言文件操作函数https://blog.csdn.net/qq_53332052/article/details/128470575?utm_mediumdistribute.pc_relevant.none-task-blog-2~default~baidujs_utm_term~default-1-128470575-blog-125…

Nginx - 反向代理与负载均衡

目录 一、Nginx 1.1、Nginx 下载 1.2、nginx 基础配置的认识 a&#xff09;第一部分&#xff1a;全局块 b&#xff09;第二部分&#xff1a;events 块 c&#xff09;第三部分&#xff1a;http 块 http 块中 内嵌的 server 块 1.3、一些常用配置 1.3.1、location 匹配级…

windows Vscode 连接 虚拟机,超详细,含免密免ip配置 以 linux 虚拟机为例

我们这里使用 ssh 进行连接&#xff0c;不了解 ssh 的也没关系&#xff0c;感兴趣的可以自己了解一下。 我的虚拟机是 Ubuntu20.04&#xff0c;如果出现与 Centos 不一样的操作可以自行替换。 &#xff08;应该不会有&#xff1f;&#xff1f;&#xff09; 一 . 登录虚拟机~&a…

2023年中国云计算软件市场规模、市场结构及市场份额情况分析[图]

云计算是分布式计算的一种&#xff0c;指的是通过网络“云”将巨大的数据计算处理程序分解成无数个小程序&#xff0c;然后&#xff0c;通过多部服务器组成的系统进行处理和分析这些小程序得到结果并返回给用户。云计算软件类型分为三类&#xff0c;即基础设施即服务、平台即服…

EMC Unity存储(VNXe) service Mode和Normal Mode的一些说明

本文介绍下EMC unity存储设备&#xff08;也包含VNXe存储设备&#xff09;的两种工作模式&#xff1a; Service mode&#xff1a;也叫做rescue mode&#xff0c;存储OS工作不正常或者有其他故障&#xff0c;就会进入这个模式&#xff0c;无法对外提供服务Normal mode&#xff…

深入JTS事务引擎:理论与实践相结合,掌握高效事务管理的秘诀

事务是可靠应用程序的构建块 如果您阅读过任何有关 J2EE 的介绍性文章或者书籍&#xff0c;那么就会发现&#xff0c;只有一小部分资料是专门针对 Java Transaction Service&#xff08;JTS&#xff09;或 Java Transaction API&#xff08;JTA&#xff09;的。这并不是因为 J…

基于SpringBoot的网上订餐系统

基于SpringBoot的网上订餐系统的设计与实现 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBootMyBatisVue工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 【主要功能】 角色&#xff1a;用户、管理员管理员&#xff1a;登录、个人中心、会员管理、…

网络工程师--网络安全与应用案例分析

前言 需要网络安全学习资料的点击链接&#xff1a;【282G】网络安全&黑客技术零基础到进阶全套学习大礼包&#xff0c;免费分享&#xff01; 案例一&#xff1a; 某单位现有网络拓扑结构如下图所示&#xff0c;实现用户上网功能&#xff0c;该网络使用的网络交换机均为三…

边端小场景音视频流分发架构

备注&#xff1a;绿色线条&#xff0c;红色线条&#xff0c;蓝色线条&#xff0c;均是表示同一路流的不同的协议而已 1&#xff09;IPC本身的流媒体的能力有限&#xff0c;一般IPC支持的客户端数10~50个&#xff0c;媒体分发能力&#xff1a;10~20路&#xff0c;看设备品牌能力…

linux中搭建c语言环境并编译

安装gcc 安装 yum install gcc 检查 gcc --version 编译文件 1.编写test.c vim test.c #include <stdio.h>int main() {printf(" ***** \n");printf(" * o o * \n");printf("* ^ *\n");printf("* - *\n");printf…

小程序如何设置各种时间参数

在小程序管理员后台->基本设置处&#xff0c;可以设置各种时间。例如待支付提醒时间、待支付取消时间、自动发货时间、自动收货时间、自动评价时间等等。下面具体解释一下各个时间的意思。 1. 待支付提醒时间&#xff1a;在用户下单后&#xff0c;如果一段时间内没有完成支付…

RS485电路设计

引言 今天学习RS485电路的设计。 首先先来了解一下RS485电路是什么干什么。 RS485是一种串行通信协议&#xff0c;也是一种电气标准。它可以用于在远距离范围内传送数据&#xff0c;最长传输距离可以达到1200米&#xff0c;可以支持多个设备同时通信。RS485通常应用于工业自…

【漏洞复现】安全云平台存在任意文件下载getshell

漏洞描述 深圳市强鸿电子有限公司鸿运主动安全云平台存在任意文件下载漏洞,攻击者可通过此漏洞下载敏感文件信息。 免责声明 技术文章仅供参考,任何个人和组织使用网络应当遵守宪法法律,遵守公共秩序,尊重社会公德,不得利用网络从事危害国家安全、荣誉和利益,未经授权…

SQL 教程||SQL 简介

SQL 简介&#xff08;开个新坑&#xff09; SQL&#xff08;结构化查询语言&#xff09;是用于访问和操作数据库中的数据的标准数据库编程语言。 SQL是关系数据库系统的标准语言。所有关系数据库管理系统(RDMS)&#xff0c;如MySQL、MS Access、Oracle、Sybase、Informix、Po…

开源任务调度框架

本文主要介绍一下任务调度框架Flowjob的整体结构&#xff0c;以及整体的心路历程。 功能介绍 flowjob主要用于搭建统一的任务调度平台&#xff0c;方便各个业务方进行接入使用。 项目在设计的时候&#xff0c;考虑了扩展性、稳定性、伸缩性等相关问题&#xff0c;可以作为公司…

用超声波清洗机来洗眼镜好不好?眼镜超声波清洗机推荐

超声波清洗机不单单可以清洗眼镜了&#xff0c;已经衍生到可以清洗项链首饰化妆刷等物件了&#xff0c;要说利用超声波清洗机洗眼镜好不好的话其实毋庸置疑是不错的&#xff0c;毕竟现在眼镜店都会有一部超声波清洗机放在店里给顾客清洗&#xff0c;也是亲眼所见一副很久没清洗…

3.MySQL数据类型详解

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 目录 1.数据类型分类 2.数值类型 (1).tinyint&#xff0c;smallint类型等 (2)bit类型 (3)小数类型 1).float 2).decimal 3.字符串类型 (1)char (2)varchar (3)char和varchar比较 (4)日期和时间类型 (5)enum和se…

TensorFlow入门(二十五、单个神经元的扩展——Maxout网络)

Maxout网络的原理 Maxout是Goodfellow在2013年提出的一个新的激活函数,与其它的激活函数相比,Maxout是需要参数的,且参数可以通过网络的反向传播得到学习,因此它比其它激活函数有着更好的性能,理论上可以拟合任意凸函数,进而使得网络取得更好的性能。 Maxout网络主要是扩展单个…

React js原生 详解 HTML 拖放 API(鼠标拖放功能)

最近碰到了个需求&#xff0c;大概就是要通过可视化拖拽的方式配置一个冰柜&#xff0c;需要把预设好的冰柜内部架子模板一个个拖到冰箱内。一开始的想法是用鼠标事件&#xff08;mousedown、mouseup等&#xff09;那一套去实现&#xff0c;能实现但是过程过于复杂&#xff0c;…