关于什么是PRD算法这里就不赘述了,本文只涉及PRD的实现与分析。
PRD公式
P(N) = C*N
C是概率增量
N是自上次暴击之后的第几次攻击(每次暴击后重置为1,一个意思)
P(N)是当前攻击的暴击概率
我们希望给一个期望暴击概率P,然后获取对应的概率增量C。
实现
step1. 根据 C 求平均暴击概率Ptested
根据我们打机的经验,如果好几🔪都不暴击,那么暴击概率会逐步提高,直到某一🔪必定暴击。
也就是说存在一个
N
m
a
x
Nmax
Nmax,当
N
>
=
N
m
a
x
N>=Nmax
N>=Nmax时,
P
(
N
m
a
x
)
=
1
P(Nmax)=1
P(Nmax)=1。
可得出
N
m
a
x
=
C
e
i
l
(
1
/
C
)
Nmax = Ceil(1/C)
Nmax=Ceil(1/C)。
现在假设攻击Nmax次,其中只有1次暴击。设n表示第几次攻击触发暴击,可得概率分布如下:
n | 1 | 2 | 3 | … | n |
---|---|---|---|---|---|
P | C C C | 2 C ( 1 − C ) 2C(1-C) 2C(1−C) | 3 C ( 1 − 3 C + 2 C 2 ) 3C(1-3C+2C^2) 3C(1−3C+2C2) | M i n ( 1 , n C ) ( 1 − p 1 − . . . − p n − 1 ) = p n Min(1,nC)(1-p1-...-p_{n-1})=pn Min(1,nC)(1−p1−...−pn−1)=pn |
第n次触发暴击的概率等于 P(N)乘前n-1次攻击不发生暴击的概率。
也就是说,
p
1
=
P
(
1
)
,
p
2
=
P
(
1
)
(
1
−
p
1
)
p1 = P(1), p2=P(1)(1-p1)
p1=P(1),p2=P(1)(1−p1),依次类推。
我们都知道,离散型随机变量的期望公式(难绷,我忘了)E(n) = 1*p1+2*p2+...+n*pn
。对E(n)取倒数得到平均暴击概率,具体见下:
public double PFromC(double c)
{
double dCurrP = 0d;//第n次攻击发生暴击的概率等于 前n-1次攻击不暴击与P(n)的乘积
double dPreSuccessP = 0d; //前n-1次攻击中发生暴击的概率
double dPE = 0; //触发暴击所需的平均攻击次数
int nMaxFail = (int)Math.Ceiling(1d / c); //Nmax
for (int i = 1; i <= nMaxFail; i++)
{
dCurrP = Math.Min(1d, i * c) * (1 - dPreSuccessP);
dPreSuccessP += dCurrP;
dPE += i * dCurrP;
}
return 1d / dPE; //返回平均暴击概率Ptested
}
step2. 二分法求C
public double CFromP(double p)
{
double dUp = p; //C在0-p范围内,C<p降低连续暴击的可能性
double dLow = 0d;
double dMid = p;
double dPLast = 1d; //保证逻辑至少执行依次
while (true)
{
dMid = (dUp + dLow) / 2d;//取当前最大暴击概率范围内的中值为增量C
double dPtested = PFromC(dMid);//测试当前
if (Math.Abs(dPtested - dPLast) <= 0.00005d) break;// 如果平均值变化不大,说明二分的区域已经很小了,此时可以退出循环。
if (dPtested > p) dUp = dMid; //当前平均值大于期望值,向右查找
else dLow = dMid;
dPLast = dPtested; //保存每次二分求得的平均值,见上break处
}
return dMid;
}
完整代码
public class PRD
{
//期望概率
public double p;
//概率增量
public double c;
public double PFromC(double c)
{
double dCurrP = 0d;//第n次攻击发生暴击的概率等于 前n-1次攻击不暴击与P(n)的乘积
double dPreSuccessP = 0d; //前n-1次攻击中发生暴击的概率
double dPE = 0; //触发暴击所需的平均攻击次数
int nMaxFail = (int)Math.Ceiling(1d / c); //Nmax
for (int i = 1; i <= nMaxFail; i++)
{
dCurrP = Math.Min(1d, i * c) * (1 - dPreSuccessP);
dPreSuccessP += dCurrP;
dPE += i * dCurrP;
}
return 1d / dPE; //返回平均暴击概率Ptested
}
public double CFromP(double p)
{
double dUp = p; //C在0-p范围内,C<p降低连续暴击的可能性
double dLow = 0d;
double dMid = p;
double dPLast = 1d; //保证逻辑至少执行依次
while (true)
{
dMid = (dUp + dLow) / 2d;//取当前最大暴击概率范围内的中值为增量C
double dPtested = PFromC(dMid);//测试当前
if (Math.Abs(dPtested - dPLast) <= 0.00005d) break;// 如果平均值变化不大,说明二分的区域已经很小了,此时可以退出循环。
if (dPtested > p) dUp = dMid; //当前平均值大于期望值,向右查找
else dLow = dMid;
dPLast = dPtested; //保存每次二分求得的平均值,见上break处
}
return dMid;
}
//当前攻击的次数
private int attackCount = 1;
//返回是否暴击,如果没暴击则攻击次数加1,如果暴击了,则攻击次数重置为1。
public bool IsCriticalHit()
{
if (new Random().Next(0, 101) <= (int)(c * 100d) * attackCount)
{
attackCount = 1;
return true;
}
else
{
attackCount++;
return false;
}
}
}
class Program2_3
{
public static void Main2_3()
{
PRD prd = new PRD();
//设置暴击率为0.5
prd.p = 0.5d;
Console.WriteLine("暴击率为" + (int)(prd.p * 100d) + "%");
//计算概率增量
prd.c = prd.CFromP(prd.p);
//记录最大连续不暴击次数和最大连续暴击次数
int maxCriticalCount = 0, maxUnCriticalCount = 0, count = 0;
int criticalCounter = 0, uncriticalCounter = 0;
for (int j = 0; j < 100000; j++)
{
if (prd.IsCriticalHit())
{
maxUnCriticalCount = Math.Max(uncriticalCounter, maxUnCriticalCount);
uncriticalCounter = 0;
criticalCounter++;
}
else
{
maxCriticalCount = Math.Max(maxCriticalCount, criticalCounter);
criticalCounter = 0;
uncriticalCounter++;
}
}
Console.WriteLine(maxCriticalCount + " " + maxUnCriticalCount);
}
}