基于随机过程的抽卡概率精确计算(2)——终末地卡池
想看烟花awa
2026年05月18日 18:30
抽卡概率计算

接上文,这一篇介绍如何使用转移矩阵的方法计算终末地卡池的抽卡分布。主要方法和原理见前文。

实现方法——一般双保底池

卡池状态

复习一下上一篇文章中讨论的米池机制:由于其只有一种保底,我们仅需要记录距离上一次出目标结果的抽数(也称作垫池子的抽数),就可以确定下一次抽卡时的概率。注意:区别于同时需要记录抽卡结果状态—结果空间,我们将状态空间认为是“能够确定下一次抽卡概率”所需的最小空间,即下一抽的结果仅取决于状态空间。

由于终末地中拥有独立计数的双保底机制,这两种保底状态都会决定我们下一抽的概率分布,故必须用120*80的状态空间来记录卡池的状态。取S = {0..119} %5Ctimes {0..79} 作为状态空间,前一项表示大保底计数,后一项表示小保底计数。终末地的小保底机制为软保底:基础概率0.8%,第66抽开始每抽增加5%,第80抽为100%,我们记第j+1抽的概率为p(j)。为方便起见我们先假设大保底机制为:每120抽没有出限定就必出限定。由此,我们得到了该空间上的转移矩阵P:

%5Cbegin%7Bequation*%7D%0A%5Cbegin%7Bcases%7D%0Ap_%7B(i%2Cj)%2C(i%2B1%2Cj%2B1)%7D%20%3D%201-p(j)%20(%E4%B8%8D%E5%87%BA%E8%B4%A7)%5C%5C%0Ap_%7B(i%2Cj)%2C(i%2B1%2C0)%7D%20%3D%20%5Cfrac%7Bp(j)%7D%7B2%7D(%E6%AD%AA)%20%5C%5C%0Ap_%7B(i%2Cj)%2C(0%2C0)%7D%20%3D%20%5Cfrac%7Bp(j)%7D%7B2%7D%20(%E6%8A%BD%E4%B8%AD)%5C%5C%0Ap_%7B(119%2Cj)%2C(0%2C0)%7D%20%3D%201(%E5%A4%A7%E4%BF%9D%E5%BA%95%E5%96%B5)%20%5C%5C%0A%5Cend%7Bcases%7D%0A%20%5Cforall%20i%20%5Cin%20%5C%7B0..118%20%5C%7D%2Cj%20%5Cin%20%5C%7B0..79%20%5C%7D%0A%5Cend%7Bequation*%7D

用上一篇文章中提到的方法,将状态空间拉直为长9600的数列,同时得到一个9600阶的转移矩阵。这就是描述终末地抽卡概率的全部内容了。

结果空间

同样运用上一篇文章的方法,假设总金数阈值m(这个金指的是出限定角色),以n表示状态空间S的大小,我们得到结果空间%5Ctilde%7BS%7D%20%3D%20%20%5C%7B0..m%5C%7D%20%20%5Ctimes%20S与其上转移矩阵

%5Ctilde%7BP%7D%20%3D%20%5Cbegin%7Bpmatrix%7D%0AP_2%20%26%20P_1%20%26%20%26%20%5C%5C%0A%20%26%20P_2%20%26%20P_1%20%26%20%5C%5C%0A%26%20%26%20%20%5Cddots%20%5C%5C%0A%26%26%26%26P_2%20%26%20P_1%20%5C%5C%0A%26%26%26%26%26%20P%0A%0A%5Cend%7Bpmatrix%7D_%7Bn(m%2B1)*n(m%2B1)%7D

注意仅当状态空间S有且仅有第一个位置表示出金时可以使用上式。

给定结果求抽数分布

假设要求抽出m个我们将结果空间设置为S%20%3D%20%5C%7B0..m-1%5C%7D%20%5Ctimes%20S%20%2B%20%5C%7B1..120m%5C%7D。这是因为按照规则至多需要120m抽就能达成要求,最后{1..120m}部分用于记录达成要求花费的抽数。同上一篇,转移矩阵为

%5Ctilde%7BP%7D%20%3D%20%5Cbegin%7Bpmatrix%7D%0AP_2%20%26%20P_1%20%26%20%26%20%5C%5C%0A%20%26%20P_2%20%26%20P_1%20%26%20%5C%5C%0A%26%20%26%20%20%5Cddots%20%5C%5C%0A%26%26%26%26P_2%20%26%20P_1%20%5C%5C%0A%26%26%26%26%26%20P%0A%0A%5Cend%7Bpmatrix%7D_%7Bn(m%2B1)*n(m%2B1)%7D

关于终末地卡池的调整

给定抽数求结果

实际上,终末地的大保底机制并不是每120抽必出限定。只有第一次出up前有大保底,且无论如何每240抽会获得一个角色命座。由此,我们需要将状态空间从{0..119} %5Ctimes {0..79} 改为{0..(num-1)} %5Ctimes {0..79},其中num表示投入的抽数,第一项用于记录当前卡池所花费的总抽数,这样我们就可以知道什么时候会出现大保底。

不过在我们兴致勃勃将新的状态空间代入前面的公式之前,先等一下。我们使用{0..(num-1)}这个上限不定的空间去记录投入总抽数仅仅是为了判断大保底,但是最晚在120抽之后大保底就将不复存在,我们并不需要继续记录后续的抽数——我们只需要知道目前距离大保底还有多远,亦或者大保底已经不再生效。故我们可以取状态空间S = {0..120} %5Ctimes {0..79} ,若前一项为120,则表示此后大保底不会再生效。至于每240抽送的命座,我们只需要在卡池结束之后将%5Clfloor%20%5Cfrac%7Bnum%7D%7B240%7D%20%5Crfloor加到结果上就可以了。注意此时n=121。

此时转移矩阵变为:

%5Cbegin%7Bequation*%7D%0A%5Cbegin%7Bcases%7D%0Ap_%7B(i%2Cj)%2C(i%2B1%2Cj%2B1)%7D%20%3D%201-p(j)%20(%E4%B8%8D%E5%87%BA%E8%B4%A7)%5C%5C%0Ap_%7B(i%2Cj)%2C(i%2B1%2C0)%7D%20%3D%20%5Cfrac%7Bp(j)%7D%7B2%7D(%E6%AD%AA)%20%5C%5C%0Ap_%7B(i%2Cj)%2C(0%2C0)%7D%20%3D%20%5Cfrac%7Bp(j)%7D%7B2%7D%20(%E6%8A%BD%E4%B8%AD)%5C%5C%0Ap_%7B(119%2Cj)%2C(120%2C0)%7D%20%3D%201(%E5%A4%A7%E4%BF%9D%E5%BA%95%E5%96%B5)%20%5C%5C%0A%5Cend%7Bcases%7D%0A%20%5Cforall%20i%20%5Cin%20%5C%7B0..118%20%5C%7D%2Cj%20%5Cin%20%5C%7B0..79%20%5C%7D%20%5C%5C%0A%5Cbegin%7Bcases%7D%0Ap_%7B(120%2Cj)%2C(120%2Cj%2B1)%7D%20%3D%201-p(j)%20(%E4%B8%8D%E5%87%BA%E8%B4%A7)%5C%5C%0Ap_%7B(120%2Cj)%2C(120%2C0)%7D%20%3D%20%5Cfrac%7Bp(j)%7D%7B2%7D(%E6%AD%AA)%20%5C%5C%0Ap_%7B(120%2Cj)%2C(120%2C0)%7D%20%3D%20%5Cfrac%7Bp(j)%7D%7B2%7D%20(%E6%8A%BD%E4%B8%AD)%5C%5C%0A%5Cend%7Bcases%7D%0A%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5C%20%5Cforall%20j%20%5Cin%20%5C%7B0..79%20%5C%7D%20%5C%5C%0A%5Cend%7Bequation*%7D

在将结果带入结果空间的算法之前我们还有一个小问题需要处理:正常情况下我们要求S有且仅有第一个位置表示抽出目标时才可以使用这种算法。而我们目前的方法中表示抽出目标的位置则是(120,0),同时到达位置(120,0)仅能保证出金,但不保证获得需要的目标(注意上式6,7行完全一致,这也是由这个问题导致的)。由此我们需要做以下调整:

%E5%AF%B9%E8%BD%AC%E7%A7%BB%E7%9F%A9%E9%98%B5P_1%20%5C%5C%0A%5Cbegin%7Bequation*%7D%0A%5Cbegin%7Bcases%7D%0Ap_%7B(i%2Cj)%2C(120%2C0)%7D%20%3D%20%5Cfrac%7Bp(j)%7D%7B2%7D%20(%E6%8A%BD%E4%B8%AD)%5C%5C%0Ap_%7B(119%2Cj)%2C(120%2C0)%7D%20%3D%201(%E5%A4%A7%E4%BF%9D%E5%BA%95%E5%96%B5)%20%5C%5C%0Ap_%7B(120%2Cj)%2C(120%2C0)%7D%20%3D%20%5Cfrac%7Bp(j)%7D%7B2%7D%20(%E6%8A%BD%E4%B8%AD)%5C%5C%0A%5Cend%7Bcases%7D%0A%20%5C%20%5C%20%5C%20%5C%20%5C%20%5Cforall%20i%20%5Cin%20%5C%7B0..118%5C%7D%2Cj%20%5Cin%20%5C%7B0..79%20%5C%7D%20%5C%5C%0A%5Cend%7Bequation*%7D

%E5%AF%B9%E8%BD%AC%E7%A7%BB%E7%9F%A9%E9%98%B5P_2%20%5C%5C%0A%5Cbegin%7Bequation*%7D%0A%5Cbegin%7Bcases%7D%0Ap_%7B(i%2Cj)%2C(i%2B1%2Cj%2B1)%7D%20%3D%201-p(j)%20(%E4%B8%8D%E5%87%BA%E8%B4%A7)%5C%5C%0Ap_%7B(i%2Cj)%2C(i%2B1%2C0)%7D%20%3D%20%5Cfrac%7Bp(j)%7D%7B2%7D(%E6%AD%AA)%20%5C%5C%0Ap_%7B(120%2Cj)%2C(120%2Cj%2B1)%7D%20%3D%201-p(j)%20(%E4%B8%8D%E5%87%BA%E8%B4%A7)%5C%5C%0Ap_%7B(120%2Cj)%2C(120%2C0)%7D%20%3D%20%5Cfrac%7Bp(j)%7D%7B2%7D(%E6%AD%AA)%20%5C%5C%0A%5Cend%7Bcases%7D%0A%20%5Cforall%20i%20%5Cin%20%5C%7B0..118%20%5C%7D%2Cj%20%5Cin%20%5C%7B0..79%20%5C%7D%20%5C%5C%0A%5Cend%7Bequation*%7D

其物理含义为:P_2记录一次抽卡没有抽中的情况下以何种概率转移到其他状态,而P_1记录抽卡抽到目标后应该转移到什么状态,故P%20%3D%20P_1%20%2B%20P_2表示状态空间的转移矩阵。而%5Ctilde%7BP%7D则表示:如果抽到了目标,则按照P_1的方式转移状态,并且将出货计数+1;若没抽到目标,则按照P_2的方式转移状态,并且不改变出货计数。在%5Ctilde%7BP%7D的显式表达中也可以看出来,P_2位于对角线上意味着出货计数不变。而P_1位于对角线右侧表示在按规则改变状态的同时出货计数+1。

%5Ctilde%7BP%7D%20%3D%20%5Cbegin%7Bpmatrix%7D%0AP_2%20%26%20P_1%20%26%20%26%20%5C%5C%0A%20%26%20P_2%20%26%20P_1%20%26%20%5C%5C%0A%26%20%26%20%20%5Cddots%20%5C%5C%0A%26%26%26%26P_2%20%26%20P_1%20%5C%5C%0A%26%26%26%26%26%20P%0A%0A%5Cend%7Bpmatrix%7D_%7Bn(m%2B1)*n(m%2B1)%7D

再将这个结果带入状态—结果空间中的转移矩阵即可得到想要的结果。记得加上每240抽送的命座%5Clfloor%20%5Cfrac%7Bnum%7D%7B240%7D%20%5Crfloor。

给定抽数求结果

给定抽数求结果仍然可以使用上文求解的P_1,P_2。假设需要m个目标,则至多需要L = 240m-120抽达成目标,结果空间可以取S%20%3D%20%5C%7B0..m-1%5C%7D%20%5Ctimes%20S%20%2B%20%5C%7B1..L%5C%7D,对应的转移矩阵:

%5Ctilde%7BP%7D(t)%20%3D%20%5Cbegin%7Bpmatrix%7D%0AP_2%20%26%20P_1%20%26%20%26%20%5C%5C%0A%20%26%20P_2%20%26%20P_1%20%26%20%5C%5C%0A%26%20%26%20%20%5Cddots%20%5C%5C%0A%26%26%26%26P_2%20%26%20P_1%20%5C%5C%0A%26%26%26%26%26%20P_2%20%26%20Q(t)%20%5C%5C%0A%26%26%26%26%26%26%20I_%7BL%7D%5C%5C%0A%5Cend%7Bpmatrix%7D_%7B(nm%2BL)*(nm%2BL)%7D

由于之前Q(t)是基于P_1的第一列生成的,现在也需要进行修改。Q(t)的物理含义为:只要出货,不管出货之后状态会跳转到哪,都将其跳转到吸收壁t。故我们对Q(t)做如下修订:Q(t)%20%3D%20%5Cbegin%7Bpmatrix%7D%0A0_%7Bn*(t-1)%7D%20%26%20P_1*1_%7Bn*1%7D%20%26%200_%7Bn*(L-t)%7D%20%0A%5Cend%7Bpmatrix%7D其中P_1*1_%7Bn*1%7D表示P_1对行求和,得到n*1列向量。

对于每240抽附送的命座,我们考虑在t = 240z的时候将已获得抽数加一,即将右推移算子作用于当前分布:

U%20%3D%20%5Cbegin%7Bpmatrix%7D%0A0%20%26%20I_%7Bn(m-1)%7D%20%5C%5C%0A0_n%20%26%200%20%26%20Q'(t)%20%5C%5C%0A%20%26%20%26%20I_%7BL%7D%5C%5C%0A%5Cend%7Bpmatrix%7D_%7B(nm%2BL)*(nm%2BL)%7D

其中Q'(t)%20%3D%20%5Cbegin%7Bpmatrix%7D%0A0_%7Bn*(t-1)%7D%20%261_%7Bn*1%7D%20%26%200_%7Bn*(L-t)%7D%20%0A%5Cend%7Bpmatrix%7D表示:若当前已经获得m-1个限定,则进入吸收壁t。

由于右推移算子会保持当前卡池的状态(即分布在状态空间S上的投影不变),可以验证在这种情况下在抽完240z抽之前或之后使用这个算子不会影响最终结果。

示例代码

仍然选择matlab作为计算工具,以下为终末地卡池的代码

给定抽数求结果

代码块
PlainText
自动换行
复制代码
initial = 0;       %小保底计数
num = input("输入投入抽数");
m = max(5,ceil(num/40));
n = 9680;
p = [0.008*ones(1,65),0.008 + 0.05*(1:14),1];
P1 = spalloc(n,n,n);
P2 = spalloc(n,n,2*n);
for i = 0:118                   %转移矩阵
    for j = 0:79
        P1(80*i+j+1,80*120+1) = p(j+1)/2;
        P2(80*i+j+1,80*i+j+82) = 1 - p(j+1);
        P2(80*i+j+1,80*i+80+1) = p(j+1)/2;
    end
end
for j = 0:79
    i = 120;
    P1(80*i+j+1,80*i+1) = p(j+1)/2;
    P1(80*119+j+1,80*i+1) = 1;
    if j < 79
        P2(80*i+j+1,80*i+j+2) = 1 - p(j+1);     %防止越界
    end
    P2(80*i+j+1,80*i+1) = p(j+1)/2;
end

P_tilde = spalloc(n*(m+1),n*(m+1),3*n*m);
for i = 0:m
    P_tilde(i*n + (1:n),i*n + (1:n)) = P2;
end
for i = 1:m
    P_tilde((i-1)*n + (1:n),i*n + (1:n)) = P1;
end
P_tilde(m*n + (1:n),m*n + (1:n)) = P_tilde(m*n + (1:n),m*n + (1:n)) + P1;       %结果空间的转移矩阵

mu = zeros(1,n*(m+1));
mu(initial*80+1) = 1;

for i = 1:num           %计算结果
	mu = mu * P_tilde;
end
bonus = floor(num/240);
p = zeros(1,m+1);
exceptation = 0;
for i = 0:m             %统计结果
    p(i+1) = sum(mu(i*n+(1:n)));
    disp(['抽到',num2str(i+bonus),'个目标的概率是',num2str(p(i+1))]);
    exceptation = exceptation + p(i+1)*(i+bonus);
end
bar(bonus+(0:m),p);
disp(['抽到个数的期望是',num2str(exceptation)]);
disp(['精度丢失',num2str(1-sum(p))]);
复制成功

给定结果求抽数

代码块
PlainText
自动换行
复制代码
initial = 0;       %小保底计数
num = input('请输入需要的限定数')
m = num;
n = 9680;
p = [0.008*ones(1,65),0.008 + 0.05*(1:14),1];
P1 = spalloc(n,n,n);
P2 = spalloc(n,n,2*n);
for i = 0:118
    for j = 0:79
        P1(80*i+j+1,80*120+1) = p(j+1)/2;
        P2(80*i+j+1,80*i+j+82) = 1 - p(j+1);
        P2(80*i+j+1,80*i+80+1) = p(j+1)/2;
    end
end
for j = 0:79
    i = 120;
    P1(80*i+j+1,80*i+1) = p(j+1)/2;
    P1(80*119+j+1,80*i+1) = 1;
    if j < 79
        P2(80*i+j+1,80*i+j+2) = 1 - p(j+1);     %防止越界
    end
    P2(80*i+j+1,80*i+1) = p(j+1)/2;
end

L = 240*m-120;
P_tilde = spalloc(n*m,n*m,3*n*m);
for i = 0:(m-1)
    P_tilde(i*n + (1:n),i*n + (1:n)) = P2;
end
for i = 1:(m-1)
    P_tilde((i-1)*n + (1:n),i*n + (1:n)) = P1;
end

mu = zeros(1,n*m+L);
mu(initial+1) = 1;

Q0 = sum(P1,2);
U0 = [sparse(n*(m-1),n),sparse(1:n*(m-1),1:n*(m-1),ones(1,n*(m-1)));sparse(n,n*m)];
for t = 1:L               %计算分布
    Q = [sparse(n*(m-1),L);[sparse(n,t-1),Q0,sparse(n,L-t)]];
    Q_ = [sparse(n*(m-1),L);[sparse(n,t-1),sparse(1:n,1,ones(1,n)),sparse(n,L-t)]];         %Q'
    mu = mu * [P_tilde,Q;sparse(L,n*m),sparse(1:L,1:L,ones(1,L))];
    if mod(t,240) == 0
        mu = mu * [U0,Q_;sparse(L,n*m),sparse(1:L,1:L,ones(1,L))];                         %每240抽保底
    end
end
p = mu(n*m + (1:L));
S = zeros(1,120*m);
S(1) = p(1);
expectation = p(1);
for i = 2:120*m
    S(i) = S(i-1) + p(i);  %累进概率
    expectation = expectation + p(i)*i;
end

figure;
yyaxis left;
plot(1:120*m,S);
ylabel('累进概率');
% yyaxis right;
% bar(1:120*m,p(1:120*m));
% ylabel('单抽概率');
disp(['期望抽数',num2str(expectation)]);
disp(['精度丢失',num2str(1-sum(mu))]);
复制成功

总结

结果展示

图中展示了获得1个限定所需抽数的分布(不包括30抽送的加急招募),求得需要的期望抽数约为81.4抽,吃满保底的概率约为34.1%,与站内其他计算数据吻合。

总结

终于写完了抽卡概率计算的第二篇,这一篇可以体现出随机过程算法对于蒙特卡洛模拟和概率论方法的优势。但是目前的方法也有一些不足——主要是计算方法太过抽象,难以理解。在下一期解析异环卡池之前我会再加一起专栏,介绍一种理解起来更加简单的算法——即使从原理上与当前的算法基本相同。

——转载和引用请标明出处