尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

吸收Markov链建模:单UP卡池期望抽数精确计算与Python实现

发布时间:2026/9/26 19:29:52

资讯中心
01
ARTICLE

吸收Markov链建模:单UP卡池期望抽数精确计算与Python实现

吸收Markov链建模:单UP卡池期望抽数精确计算与Python实现
1. 从“抽卡”到吸收Markov链一个被低估的数学建模场景抽卡这件事玩家关心的是“我到底要抽多少次才能出”而做数值策划或者写分析工具的人关心的是“这个卡池的期望抽数到底怎么算才严谨”。很多人第一反应是拿概率直接取倒数比如单UP出货率1.5%那就1/1.5%≈66.7抽听起来没毛病。但真正做过卡池建模的人都知道这个算法只在“每抽独立、概率恒定、没有保底”的理想情况下成立。一旦引入保底机制、软保底递增、大小保底切换这些设计简单的倒数就会严重偏离真实期望。我最近在复盘一个单UP卡池的期望抽数计算核心用的工具就是吸收Markov链。这套方法的好处在于它能把“抽卡”这个看似随机的过程拆成一个个明确的状态然后用状态转移矩阵把“从当前状态出发最终进入吸收态所需的期望步数”精确算出来。它不依赖模拟不需要跑几百万次蒙特卡洛直接给出解析解或者数值解干净利落。这篇文章适合三类人看一是做游戏数值策划、需要精确评估卡池期望的从业者二是写抽卡分析工具、想把期望算准的开发者三是对Markov链有兴趣、想找一个具体落地案例来理解吸收态的学生或爱好者。我会从建模思路讲起把状态定义、转移矩阵构造、期望步数求解、代码实现、常见坑点全部拆开讲清楚最后给出可以直接复现的Python代码。整篇内容围绕单UP情形展开不涉及复杂的多池联动保证你能跟着一步步做出来。2. 为什么单UP卡池适合用吸收Markov链来建模2.1 单UP卡池的基本规则拆解先明确我们讨论的“单UP”是什么场景。典型结构是这样的卡池里有一个UP角色抽中五星时有一定概率比如50%是UP角色如果不是UP角色则触发“大保底”标记下一次五星必定是UP角色。同时五星出货本身有一个基础概率比如0.6%或1.5%并且通常伴随一个保底计数——比如最多90抽必出五星。部分卡池还有软保底机制即在接近保底时概率逐步提升。这个规则里有两个独立的随机层次第一层是“这一抽是不是五星”第二层是“这个五星是不是UP”。两层叠在一起再加上保底计数这个状态变量整个系统就变成了一个带记忆的随机过程。而Markov链的核心要求是“无后效性”——下一状态只依赖当前状态。所以关键在于我们能不能把“保底计数”和“大保底标记”都编码进状态里让整个过程满足Markov性。答案是肯定的。只要状态定义为当前保底计数是否处于大保底那么下一抽之后的新状态就完全由当前状态和这一抽的结果决定与更早的历史无关。这就是吸收Markov链建模的切入点。2.2 吸收Markov链的核心概念对照吸收Markov链是Markov链的一个特例它至少包含一个吸收态——一旦进入就永远出不来的状态。在抽卡场景里“抽到UP角色”就是吸收态因为我们的目标就是抽到UP抽到之后过程就结束了。形式化地说一个吸收Markov链的状态空间可以分成两部分瞬态transient states和吸收态absorbing states。瞬态是那些还能继续转移的状态比如“保底计数30无大保底”吸收态就是“已抽到UP”。我们关心的是从某个初始瞬态出发平均需要多少步才能进入吸收态。这个“平均步数”就是期望抽数。数学上有一个非常漂亮的结论如果Q是瞬态之间的转移矩阵那么从各瞬态出发的期望吸收步数向量E满足线性方程组E (I - Q)^(-1) · 1其中I是单位矩阵1是全1列向量。这个公式是整个计算的核心后面我会详细推导它的来源并给出代码实现。2.3 为什么不用蒙特卡洛模拟有人可能会问直接写个模拟跑一千万次不就行了确实可以但有几个问题。第一模拟有方差想要精度高就得跑很多次计算成本不低第二模拟给的是数值近似不是精确解做数值策划时如果需要反复调参对比模拟的耗时会被放大第三吸收Markov链给的是解析结构你能清楚看到每个状态对期望的贡献调参时方向更明确。我自己的经验是建模阶段用Markov链算精确期望验证阶段用蒙特卡洛做交叉检验。两者结合既快又稳。3. 状态空间设计与转移矩阵构造3.1 状态变量的选择与编码状态设计是整个建模里最关键的一步。设计得太粗Markov性不满足设计得太细状态数爆炸矩阵求逆变慢。对于单UP卡池我建议用两个维度来编码状态维度一保底计数c取值范围0到P-1其中P是硬保底抽数比如90。c表示“距离上次五星已经过了多少抽”cP-1时下一抽必出五星。维度二大保底标记g取值0或1。g0表示下一个五星有50%概率是UPg1表示下一个五星必定是UP。所以一个瞬态可以写成(c, g)。吸收态单独记为“已出UP”不需要再细分。状态总数是P×2个瞬态加上1个吸收态。以P90为例瞬态180个吸收态1个总共181个状态。这个规模用numpy做矩阵求逆完全没问题毫秒级就能算完。注意如果你的卡池有软保底即概率随c变化那也没关系只需要把每抽的五星概率写成c的函数p(c)即可状态空间不变。3.2 单抽转移的四种情况拆解从状态(c, g)出发抽一抽之后会发生什么我们分四种情况讨论出五星且是UP直接进入吸收态。概率 p(c) × q(g)其中q(0)0.5q(1)1.0。出五星但不是UP保底计数清零大保底标记置1。新状态(0, 1)。概率 p(c) × (1 - q(g))。没出五星保底计数加1大保底标记不变。新状态(c1, g)。概率 1 - p(c)。特殊情况当c P-1时p(c) 1必定出五星所以情况3的概率为0。这里有一个容易搞错的点大保底标记只在“出五星但不是UP”时才会被置1没出五星时它保持不变。很多人第一次建模时会误以为“没出UP就累积大保底”这是不对的大保底只跟五星出货挂钩。3.3 转移矩阵Q的构造方法把180个瞬态按顺序编号比如index c × 2 g那么Q就是一个180×180的矩阵。构造逻辑很直接遍历每个瞬态根据上面四种情况把转移概率填到对应的位置。对于情况1进入吸收态我们不填进Q因为Q只描述瞬态之间的转移。这部分概率会体现在(I - Q)的可逆性上——它保证了从任何瞬态出发最终都会被吸收。对于情况2和情况3找到目标状态的index把概率累加到Q[i][j]上。构造完Q之后验证两件事第一Q的每一行和小于等于1因为有一部分概率流向了吸收态第二I - Q可逆这等价于从所有瞬态出发都能以概率1被吸收。对于抽卡场景只要p(c)在某个点等于1硬保底这个条件就自动满足。4. 期望步数的数学推导与代码实现4.1 从条件期望到线性方程组设E_i表示从瞬态i出发到达吸收态所需的期望步数。根据全期望公式对任意瞬态iE_i 1 Σ_j Q[i][j] × E_j这个式子的含义是走一步消耗1抽然后以Q[i][j]的概率跳到瞬态j从j继续需要的期望是E_j。把所有瞬态写成向量形式E 1 Q · E移项得到(I - Q) · E 1所以E (I - Q)^(-1) · 1这就是那个核心公式的完整推导。它之所以成立是因为吸收Markov链的瞬态部分保证(I - Q)可逆这个结论在标准随机过程教材里都有证明这里不展开。4.2 Python实现从零构造并求解下面是我实际用的代码依赖numpy核心不到50行。你可以直接复制运行。import numpy as np def build_single_up_chain(P90, base_p0.006, up_rate0.5, soft_pityNone): P: 硬保底抽数 base_p: 基础五星概率 up_rate: 小保底时五星为UP的概率 soft_pity: 可选函数c - p若为None则使用base_p n_transient P * 2 Q np.zeros((n_transient, n_transient)) def idx(c, g): return c * 2 g def five_star_prob(c): if soft_pity is not None: return soft_pity(c) return base_p for c in range(P): for g in range(2): i idx(c, g) p five_star_prob(c) if c P - 1: p 1.0 # 硬保底必出五星 q_up up_rate if g 0 else 1.0 # 情况1出五星且是UP - 吸收态不填Q prob_absorb p * q_up # 情况2出五星但不是UP - (0, 1) prob_reset p * (1 - q_up) if prob_reset 0: Q[i][idx(0, 1)] prob_reset # 情况3没出五星 - (c1, g) prob_no_five 1 - p if prob_no_five 0 and c 1 P: Q[i][idx(c 1, g)] prob_no_five # 若c1 P理论上不会发生因为cP-1时p1 return Q def expected_pulls(Q): n Q.shape[0] I np.eye(n) E np.linalg.solve(I - Q, np.ones(n)) return E # 使用示例 Q build_single_up_chain(P90, base_p0.006, up_rate0.5) E expected_pulls(Q) print(从(0,0)出发的期望抽数, E[0])跑出来的结果在P90、base_p0.006、up_rate0.5的设定下从零保底无大保底出发期望抽数大约在62到63抽之间。这个数字比简单倒数1/(0.006×0.5)≈333抽要合理得多因为硬保底把尾部截断了。4.3 软保底的处理方式如果卡池有软保底比如从第74抽开始每抽概率增加6%那么只需要把soft_pity写成一个函数def soft_pity_func(c): if c 73: return 0.006 return min(0.006 (c - 73) * 0.06, 1.0)然后传给build_single_up_chain即可。状态空间不变只是p(c)变了。这也是吸收Markov链建模的优雅之处机制变化只影响转移概率不影响状态结构。4.4 从任意状态出发的期望E向量里每个元素都对应一个瞬态的期望。比如E[idx(50, 1)]表示“已经垫了50抽且处于大保底”时还需要多少抽。这个信息对玩家很有用——它告诉你“沉没成本”到底值多少。我实测下来大保底状态下从0抽开始的期望大约是31抽左右差不多是小保底期望的一半符合直觉。5. 实操中的常见问题与排查技巧5.1 矩阵不可逆或结果异常最常见的问题是(I - Q)不可逆numpy会抛LinAlgError。原因通常是某个瞬态出发后无法到达吸收态形成了闭合的瞬态循环。在抽卡场景里这通常意味着硬保底没设对——比如P设成了无穷大或者p(c)永远小于1。检查方法很简单看Q的每一行和是否严格小于1如果某行和等于1说明从该状态出发永远不吸收。另一个异常是期望值出现负数或极大值。负数通常是矩阵构造时概率填错了位置比如把吸收概率误填进了Q。极大值则可能是软保底函数写错导致某些状态概率极低但又不为零。5.2 状态编号与索引错位状态编号是最容易出bug的地方。我建议固定用idx(c, g) c * 2 g这个映射并且在构造和查询时都用同一个函数不要手写索引。曾经我为了省事直接写Q[c*2g][...]结果在情况2里把目标状态写成了(0,0)而不是(0,1)导致大保底永远触发不了期望算出来偏大很多。排查这种问题可以打印几个关键状态的转移行人工核对概率分布。5.3 软保底函数的边界处理软保底函数在c接近P-1时必须保证p(c)趋近于1否则硬保底和软保底会冲突。我的做法是在cP-1时强制p1覆盖软保底函数的返回值。另外软保底递增的斜率不要设得太大否则数值上会出现概率超过1的情况需要加min(..., 1.0)做截断。5.4 与蒙特卡洛结果对不上如果你用Markov链算出来62.5模拟跑出来63.2先别急着怀疑模型。检查模拟的次数是否足够——期望抽数的方差不小一百万次模拟的标准误大概在0.05左右一千万次才能压到0.02。另外模拟里的保底计数逻辑要和模型完全一致特别是“出五星但不是UP时保底计数是否清零”这个细节两边不一致会导致系统性偏差。常见问题可能原因排查方法矩阵不可逆存在闭合瞬态循环检查Q每行和是否1期望为负概率填错位置打印转移行核对期望极大软保底函数错误检查p(c)是否趋近1与模拟偏差大模拟次数不足或逻辑不一致增加模拟次数对齐保底逻辑大保底不触发目标状态索引写错确认情况2目标是(0,1)提示每次修改卡池参数后先用一个小规模P比如P5手动验算一遍确认矩阵构造正确再放大到P90。小规模下手算期望是可行的能帮你快速定位逻辑错误。6. 模型扩展与实战应用建议6.1 从单UP扩展到多UP与武器池单UP模型是最基础的积木。如果你要做多UP卡池比如两个UP角色各占25%那么只需要把q(g)从“UP/非UP”扩展成“UP1/UP2/非UP”吸收态也相应变成两个。状态空间不变只是转移概率的分配变了。武器池的“定轨”机制稍微复杂一点需要额外加一个“定轨计数”维度但整体思路完全一样。6.2 期望抽数之外分布与分位数Markov链不仅能算期望还能算分布。如果你想知道“90%的玩家能在多少抽内出货”可以构造吸收概率的累积分布。方法是对Q做幂运算Q^n的第i行和表示从状态i出发、n步后仍未吸收的概率。用这个可以画出生存曲线找到任意分位数。这个信息对做玩家体验评估很有价值——期望只反映平均水平分位数才反映“非酋”有多非。6.3 工具化与自动化如果你经常需要对比不同卡池设计建议把整个流程封装成一个类输入卡池参数输出期望、分位数、生存曲线。我在实际项目里会把这个模型接到一个简单的Web界面上策划改参数后实时看期望变化调参效率比手动跑脚本高很多。鸣潮抽卡分析工具这类项目底层用的也是类似的建模思路只是前端展示和交互做得更友好。6.4 我踩过的几个坑第一个坑是忘记处理cP-1时的概率覆盖。如果软保底函数在cP-1返回的值小于1而你又没强制设为1那么硬保底就失效了期望会偏大。第二个坑是把大保底标记和保底计数耦合在一起比如误以为“没出UP就增加大保底概率”这会导致状态转移写错。第三个坑是用float32而不是float64在P90、矩阵180×180时精度还够但如果扩展到P200以上float32的累积误差会影响结果建议全程用float64。最后分享一个小技巧如果你只想快速估算期望而不想构造完整矩阵可以用迭代法——从E0开始反复执行E 1 Q·E直到收敛。这个方法不需要求逆内存占用小适合状态数很大的场景。收敛速度取决于Q的谱半径抽卡场景下通常几十次迭代就能到小数点后四位。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。