
上周周赛的 A 题 AWC0001A Bacteria Growth Experiment 出来之后我在评论区看到不少人在纠结明明叫“细菌生长实验”看起来就是个模拟题怎么模拟都超时还有人拿着取模后的结果去判断“是否达到培养皿容量”结果答案全错。这题其实是个非常典型的“分段模拟 快速幂”问题今天我把完整题解、代码和踩坑过程都捋一遍希望能帮到正在刷模拟题和快速幂入门的同学。这类题适合谁看如果你是刚接触 OJ 的初学者可以把这篇当作“怎么从生物背景题里提取数学模型”的案例如果你已经会快速幂但总是被边界卡住那重点看第四章的坑点总结尤其是取模时机和临界点判断想一次写对没那么容易。1. 题意拆解细菌生长实验到底在算什么1.1 题目给了一条什么样的增长规则题面讲了一个培养皿里细菌数量变化的过程我拿到手第一件事是把它翻译成纯数学规则。常见版本是这样初始有 a 个细菌实验持续 n 分钟。每分钟结束后数量按规则更新当当前真实数量小于某个阈值 LIM 时每只细菌分裂出 X 只新细菌于是总数变成原来的 X 1 倍一旦真实数量达到或超过 LIM营养就跟不上了每只细菌只分裂出 Y 只新细菌总数变成原来的 Y 1 倍并且有 X Y。题目最后要求输出 n 分钟后细菌总数对 MOD 取模的结果MOD 一般取 1e9 7。这个规则看着像生物实验其实是出题人包装过的分段线性增长模型。为什么会设置一个阈值再降速因为真实培养皿里细菌增长不是无限指数增长受空间和营养限制到达某个密度后增长率会下降这就是教材里 Logistic 增长模型的极简版。出题人把复杂生物过程简化成“两个不同倍增系数”目的是让你处理一个分段函数而不是真去学微生物。我见过有些同学读题后直接开一个变量 cur循环 n 次每次 if (cur LIM) cur * (X 1) else cur * (Y 1)看起来很合理一交 TLE。问题在哪往下看数据范围就能明白。1.2 真正需要求的东西是哪个时刻的值先明确一个很容易混淆的点题目要的是“n 分钟后”的总数而且最终结果要取模。但判断“当前是否达到阈值 LIM”时必须用真实数量不能用 cur % MOD 之后的值。我之前看到有人这么写cur 一直对 MOD 取模然后拿 cur 和 LIM 比大小。这完全错了。取模之后的数会变小可能从 1e98 取模变成 1导致阈值判断不断反复横跳模拟直接乱套。正确做法是临界点之前用真实值参与判断跨过临界点之后再进入纯取模计算阶段因为后续只关心对 MOD 的余数。这也是为什么这题的第一道坎不是快速幂而是能不能把“真实值判断”和“取模值计算”这两个逻辑分开。我在代码里用变量 cur 存真实值直到它跨过 LIM之后再用另一个思路处理剩余分钟数逻辑一下就清晰了。2. 思路推导为什么不能直接模拟 n 分钟2.1 O(n) 模拟的结构性问题题目数据范围里n 最大可以到 1e18 这个量级。就算每轮循环只做一次乘法和一次比较1e18 次操作在现代 CPU 上也要跑几十年显然不可能。这就是典型的大指数模拟必须找到数学上的捷径。如果你熟悉快速幂应该已经闻到味道了在跨过阈值之后每一分钟变化都是固定乘 Y 1这不就是求 (Y 1) 的幂次吗真正麻烦的只有“跨过阈值之前”那一段因为那段时间乘 X 1总会让人下意识觉得需要逐分钟模拟。但关键观察是阈值之前的模拟次数其实非常少。因为每次数量至少乘以 X 1而 X ≥ 1所以乘数至少是 2。从一个有限初值 a 增长到 LIM最多也就 log2(LIM / a) 次。如果 LIM 是 1e9a 是 1那大约 30 次就超过去了就算 a 很小、LIM 很大这个步数也就是几十上百完全可以在常数时间内跑完。所以整体思路就是先用一个 while 循环快速模拟到临界点记录用了多少分钟如果 n 比这个时间短直接输出当时真实值取模如果 n 更长剩下部分用快速幂一次算完。这个思路的本质是“分段处理”前段是一个不超过几十步的暴力模拟后段是一个 O(log n) 的快速幂。两部分加起来总复杂度 O(log n)1e18 的数据量毫无压力。2.2 临界点之前的模拟为什么很快有人会问万一 LIM 是 1e18a 是 1X 特别小比如 X 1乘 2那也需要约 60 次这也不多啊。就算 X 0题目一般保证 X Y 且 Y ≥ 0X 至少为 1所以乘数至少是 2。哪怕你把 LIM 拉到 1e1860 次循环都是可以忽略不计的。再极端一点如果 a 非常接近 LIM循环一次就直接 break如果 a 初始就已经大于等于 LIM那 while 压根不会进入直接走快速幂分支。所以说临界点之前的模拟步数是由 log 决定的和 n 完全无关这是整个算法成立的核心。我记得第一次做这道题时还担心过如果 LIM 是 1e18X 是 1a 是 1每秒翻倍到 1e18 需要约 60 步但如果 X 很大比如 1e9那一步可能直接从 1 跳到 1e9 以上。不论哪种情况循环次数都远小于 n 的数量级所以完全可以放心模拟。2.3 临界点之后的快速幂怎么用一旦真实数量 cur 达到 LIM题目规则说之后每分钟乘 Y 1。因为 Y ≥ 0Y 1 ≥ 1所以数量一旦跨过阈值就不会再掉回阈值以下。这意味着从跨过的那一分钟开始模型变成一个单调的等比数列于是可以直接写成ans cur % MOD * qpow(Y 1, remaining) % MOD其中 remaining 是剩余分钟数cur 已经取过模或者会在乘法时取模qpow 是标准快速幂。这里还有一个容易忽略的点跨过阈值的那一分钟本身也执行了一次乘法 X 1所以不能用“刚好等于 LIM”来判断而要用“第一次达到或超过 LIM”作为切换标志。很多人在代码里写 if (cur LIM) 或者 cur LIM 时切换一旦跳变越过了 LIM就会少算一分钟答案差很远。更稳妥的写法是判断 cur LIM或者先算 next cur * (X 1) 再判断 next LIM。我建议直接算下一步的 next 值用 next LIM 判断是否跨过临界。这样既避免了乘法后丢失边界也方便单步调试。3. 完整实现与代码讲解3.1 C 实现含防溢出写法下面是我提交时用的 C 代码带注释。这里我特意用了 __int128 做中间乘法避免在某些数据范围下 cur * (X 1) 溢出 64 位整数。#include bits/stdc.h using namespace std; typedef long long ll; const ll MOD 1000000007LL; ll qpow(ll a, ll b) { ll res 1 % MOD; a % MOD; while (b 0) { if (b 1) res res * a % MOD; a a * a % MOD; b 1; } return res; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin T; while (T--) { ll a, n, x, y, LIM; cin a n x y LIM; ll cur a; ll cnt 0; ll mulX x 1; // 临界点之前用真实值模拟步数最多几十次 while (cnt n cur LIM) { __int128 nxt (__int128)cur * mulX; if (nxt LIM) { // 这一分钟执行的是乘 X1且跨过阈值 cur (ll)(nxt % MOD); cnt; break; } cur (ll)nxt; cnt; } if (cnt n) { cout cur % MOD \n; continue; } ll remaining n - cnt; ll ans (cur % MOD) * qpow(y 1, remaining) % MOD; cout ans \n; } return 0; }有几个细节我在代码里做了特殊处理。第一qpow 的初始值写成 1 % MOD是为了兼容 MOD 可能为 1 的特殊情况虽然这题 MOD 是 1e97但养成习惯可以避免以后踩坑。第二while 里的判断是 cur LIM因为只有真实值小于阈值时才走高速增长如果 cur 初始就大于等于 LIM循环体一次都不执行直接走快速幂逻辑天然正确。第三break 之后的 cur 已经存的是模值而 not break 的情况下 cur 还是真实值这两种情况在后续计算中能统一处理是因为后面所有乘法都在模意义下进行。3.2 Python 实现Python 不需要担心整数溢出所以代码更简洁但思路完全一致。MOD 10**9 7 def qpow(a, b): res 1 a % MOD while b: if b 1: res res * a % MOD a a * a % MOD b 1 return res def solve(): T int(input()) for _ in range(T): a, n, x, y, LIM map(int, input().split()) cur a cnt 0 mulX x 1 while cnt n and cur LIM: nxt cur * mulX if nxt LIM: cur nxt % MOD cnt 1 break cur nxt cnt 1 if cnt n: print(cur % MOD) else: ans cur * qpow(y 1, n - cnt) % MOD print(ans) if __name__ __main__: solve()Python 版的边界处理和 C 一样。如果你在本地测试建议多造几组临界数据比如 a 恰好等于 LIM、n 0、x 1 等情况确保代码在这些边界上行为一致。3.3 为什么临界点那一步要这样处理很多人不理解为什么跨过阈值时把 cur 更新成 nxt % MOD而不是存真实值 nxt因为在跨过阈值之后所有计算都只需要模 MOD 的值真实值已经不重要了。但跨过阈值的那一刻cur 必须代表“当时真实数量对 MOD 取模的结果”这样才能作为后续幂乘的基数。举个例子设 a 1, X 3, LIM 10, n 3那么第一分钟 cur 4第二分钟 cur 16 已经超过 10。此时真实值是 16模 MOD 还是 16。从第三分钟开始如果 Y 1那答案就是 16 * (2^(3-2)) 32。如果在 break 时你错误地把 cur 设成 LIM也就是 10那后面就是 10 * 2 20答案直接错了。所以“真实值到底有多”这件事在临界点不能丢而“真实值是多少”可以通过 nxt % MOD 完整保留到模意义下两者并不矛盾。我在代码里用 __int128 存 nxt目的就是先算出真实乘积再比较确定是否跨过阈值同时拿到它对 MOD 的余数。如果你不想用 __int128也可以提前用除法判断 cur LIM / mulX但那样代码可读性会差一些出错概率更高。4. 最容易踩的坑与排查思路4.1 取模时机错位的典型症状这个坑我在群里看了不下三次。有的同学第一分钟开始就 cur % MOD然后用 cur 和 LIM 比较导致阈值判断完全失真。比如 LIM 100MOD 1000000007真实 cur 从 80 变成 160应该跨过阈值但取模后 cur 可能变成 3程序以为还在低速增长继续乘 X 1于是后面的幂次全错了。排查方法很简单在临界点之前打印 cur 的值看它是不是真实数量一旦发现 cur 小于 LIM 但实际数量已经超过 LIM基本就是取模时机错了。记住结论取模只发生在跨过阈值之后的计算里临界判断永远基于真实值两者不要混用。如果你已经写完了代码但不知道错在哪建议构造这类数据a 1, LIM 10, X 9, n 2。第一分钟 1 乘 10 等于 10刚好等于 LIM这时候应该 break第二分钟乘 Y 1。很多错误代码会因为在 while 里判断 cur LIM 时第一分钟之后 cur 等于 10循环继续然后错误地再乘一次 X 1。用这种边界数据跑一遍能暴露大部分问题。4.2 溢出问题与类型选择C 里最容易忽略的是乘法溢出。即使 LIM 只有 1e9cur 在临界点前的任意一次乘法也可能达到 1e9 * (X 1)如果 X 本身是 1e9乘积就接近 1e18已经逼近 long long 的上限。万一题目把 LIM 设成 1e18乘积就可能直接超过 9e18溢出后就变成负数或者错误数值。所以我在代码里直接用 __int128 做中间乘法这是最省心的方案。如果你的编译器不支持 __int128那就用除法判断如果 cur LIM / mulX说明下一步必然跨过阈值手动处理 break 逻辑。不管用哪种方式都不要直接拿 long long 去硬乘再判断这是这题最容易引发运行时错误的点。Python 用户相对安全但也要注意一个隐蔽问题Python3 的 int 是任意精度理论上不会溢出但无限增长也可能拖慢速度。好在这道题临界点前循环次数很少不会出现问题。4.3 边界情况初始就超阈值、n 为 0、X 等于 0这题边界情况不少我总结了一张速查表建议你依次过一遍场景预期行为常见错误a LIM全程按乘 Y 1 计算误进低速增长循环n 0直接输出 a % MOD循环内 cnt 后输出错误x 0, y 0数量不变快速幂底数为 0恰好 cur LIM已经达到阈值后续走慢速增长用 cur LIM 继续高速增长跨过阈值那一分钟按 X 1 增长之后按 Y 1少算或多算一分钟拿 n 0 来说如果 cur LIMwhile 条件里的 cnt n 为假直接跳过循环走到 cnt n 分支输出 cur % MOD逻辑正确。但如果有人在循环里无脑 cnt就会出错。这种边界最好专门写 if 处理或者像我这样用 while 条件把逻辑统一起来。还有 x 0 的情况乘数是 1低速和高速增长率可能相同题目也许会保证 X Y但你不确定时最好测试一遍。快速幂对底数为 1 的情况也能正确处理所以重点还是临界判断是否还能退出循环。5. 从这道题看一类模拟题的优化套路5.1 分段模拟的通用框架做多了题你会发现AWC0001A 不是个例很多模拟题都长着同一副骨架过程本身看起来很长n 个时间步但行为会在某个条件发生变化变化前是无法跳过的过程变化后变成简单规律。处理这类题通用框架是四步第一步读题后立刻把“条件”和“变化”列出来比如本题的阈值 LIM 和两个增长率。第二步找“临界点之前为什么不会太长”的证据一般是指数增长、累加和、或有限状态能达到 O(log) 或 O(sqrt) 的模拟步数。第三步在临界点之后寻找可公式化的规律比如等比、等差、矩阵递推直接套快速幂或前缀和。第四步把两个阶段拼接起来特别小心临界点那一时刻的归属。这个框架同样适用于很多看似复杂的题目。比如给定一个数每次乘 2 再取模超过某个界限后改乘 3问最终结果或者一个人先以某个速度跑达到一定里程后降速问多少时间后到达终点。核心思想都是“模拟到变化点公式算完剩余量”。5.2 和常见平台题解思路的对照我平时逛洛谷、力扣和一些算法博客发现这类题目的题解风格也高度统一先讲结论再给代码。但很多人看题解只看代码复制粘贴就过了遇到数据范围一变就崩溃。真正有用的题解会把“为什么临界点之前的模拟步数少”这个点讲透因为这才是算法的本质。你去看 leetcode 上一些二分答案、快速幂的题解和洛谷上的入门模拟题解其实底层都是一样的寻找状态变化的分界点然后分段求解。比如二分判定问题里分界点是第一个满足条件的值在本题里分界点是第一次达到 LIM 的时刻。理解了这种“找分界点”的思维再看其他题解时就能举一反三而不是每次都背模板。我建议你把这道题的题解当作一个模板收藏以后遇到“过程分段变化”的题目先画一条时间轴标出临界点位置再决定左右两边分别用什么方法思路会清晰很多。6. 一点个人经验与最后的提醒最后说点我自己的习惯。做这种带生物背景的模拟题我从来不先写代码而是先在纸上把“什么时候进入第二阶段”写清楚再决定数据范围允许什么算法。这题我一开始也想直接 for 循环一看 n 到 1e18 立刻收手。写代码时我习惯把临界点单独抽出来测一遍比如手动算几组小规模数据确认临界点那分钟被正确计数再跑大数据。如果你现在还被 WA 卡着我建议别急着改代码先构造三组数据a 很小且 n 很大、a 正好等于 LIM、n 0确保这三类情况输出正确很多隐藏 bug 都会暴露出来。这题整体难度不大但能把临界点、取模时机、快速幂三个点都写对说明你对模拟题的把握已经上了个台阶。以后遇到再复杂的“分段变化”模型这个思路依然能用。