ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Power Tower 幂塔函数实战:用欧拉降幂 + 快速幂递归拆解超大指数

Power Tower 幂塔函数实战:用欧拉降幂 + 快速幂递归拆解超大指数 1. 幂塔函数到底难在哪指数爆炸与取模的冲突Power Tower幂塔函数指的是形如 $a_1^{a_2^{a_3^{\cdots^{a_n}}}}$ 的嵌套指数结构在算法竞赛和数论编程里属于典型的高阶递归题。它看起来只是「一层层往上算」但真正动手就会发现两个致命问题第一指数部分增长极快$2^{2^{2^2}}$ 已经超过 $10^{19728}$任何 64 位整数都装不下第二题目通常要求对某个模数 $m$ 取余而取余运算和指数运算不能随便交换顺序直接算完再取模根本不可能。适合读这篇的人有三类正在刷数论题、被幂塔卡住的竞赛选手想搞懂欧拉降幂公式为什么能用在递归里的算法学习者以及需要把幂塔逻辑封装成可复用函数、在本地跑通并对照暴力小数据校验的工程向开发者。核心检索词就是 Power Tower、幂塔函数、欧拉降幂、快速幂、递归这五个下面全部围绕它们展开。我试过最朴素的写法写一个递归函数dfs(l, r, m)每次返回 $a_l^{dfs(l1,r)}$ 再对 $m$ 取模。小数据能过一旦指数超过 20 层就溢出而且取模位置放错会导致答案完全错误。真正的解法要用到扩展欧拉定理$$a^b \equiv \begin{cases} a^b b \varphi(m) \ a^{b \bmod \varphi(m) \varphi(m)} b \ge \varphi(m) \end{cases} \pmod{m}$$这个公式的关键在于指数 $b$ 和 $\varphi(m)$ 的大小关系不确定所以快速幂里的取模不能简单写成a % m而要写成「如果结果小于模数就保留原值否则取模后加模数」的形式。这就是整道题最容易踩坑的地方。2. 前置准备TaoToken 环境与本地编译链在写代码之前先把运行环境理顺。我本地用的是 C17 编译链配合一个稳定的模型对话入口来对照题解、验证公式推导。如果你也想边写边问、让模型帮你检查递归边界可以先用 TaoToken 的模型对话能力https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。它适合在推导欧拉降幂分支时快速确认「$b$ 和 $\varphi(m)$ 谁大」这类细节避免公式记错。真正要落地代码还需要一个能长期跑算法脚本、管理多文件工程的环境。TaoToken 的 Coding Plan 面向长期编码和 Agent 场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。我把它当成「写题时的第二大脑」尤其是递归骨架反复调试阶段能省下大量翻资料的时间。如果你打算把幂塔函数封装成库、后续还要接自己的测试脚本建议先申请 API Key 并读一遍接入文档路径分别是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 和 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。API 基础地址是 https://taotoken.net/api 注意这个地址不带任何跟踪参数配置时直接填即可。提示环境准备阶段不要急着写完整题解先把「快速幂 欧拉函数 递归骨架」三个最小单元分别跑通再拼装排错会快很多。3. 可复制配置快速幂、欧拉函数与递归骨架下面这份代码可以直接复制到本地.cpp文件编译运行。它包含四个部分带修正取模的快速幂power、带记忆化的欧拉函数phi、递归主体dfs以及主函数读入。先看完整骨架再逐段解释。#include bits/stdc.h using namespace std; #define int long long const int maxn 1e3 10; unordered_mapint,int mp; // 修正取模结果小于模数时保留原值否则取模后加模数 int mod(int a, int b) { return a b ? a : a % b b; } // 快速幂指数 b 可能已经被修正过所以取模用 mod int power(int a, int b, int p) { int ans 1; for (; b; b 1) { if (b 1) ans mod(ans * a, p); a mod(a * a, p); } return ans; } // 欧拉函数带记忆化 int phi(int n) { if (mp[n]) return mp[n]; int res n, nn n; for (int i 2; i * i n; i) { if (n % i 0) { res res / i * (i - 1); while (n % i 0) n / i; } } if (n 1) res res / n * (n - 1); return mp[nn] res; } int n, m, a[maxn], q; // 递归求解 a[l]^(a[l1]^...^a[r]) mod m int dfs(int l, int r, int m) { if (l r || m 1) return mod(a[l], m); return power(a[l], dfs(l 1, r, phi(m)), m); } signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin n m; for (int i 1; i n; i) cin a[i]; cin q; while (q--) { int l, r; cin l r; cout dfs(l, r, m) endl; } return 0; }关键点逐条说明。第一mod函数是整个算法的灵魂它保证快速幂过程中指数不会因为取模而丢失「是否大于 $\varphi(m)$」的信息。第二phi用unordered_map做记忆化因为递归过程中同一个模数会被反复查询不记忆化会超时。第三dfs的终止条件有两个区间只剩一个数或者模数退化成 1。模数为 1 时任何数取模都是 0但这里返回mod(a[l], 1)会得到 1需要结合题目语义确认——多数幂塔题在模数为 1 时答案就是 0所以更稳妥的写法是单独判断if (m 1) return 0;。参数对照表如下方便你按题目调整参数含义典型取值注意事项n数组长度1e3 以内递归深度与 n 相关m全局模数1e9 以内递归中会不断取 phia[i]塔的底数序列1e9 以内需用 long longl, r查询区间1 ≤ l ≤ r ≤ n闭区间phi(m)欧拉函数值逐层递减记忆化避免重复计算注意#define int long long虽然方便但会让unordered_mapint,int的哈希变慢。如果数据量到 1e5 级别建议改回long long显式声明或换map配合更小的键类型。4. 验证请求与成功结果本地跑通并对照暴力代码写完后必须验证。我构造了一组小数据让幂塔层数控制在 3 层以内这样可以用 Python 的大整数直接暴力算再和 C 输出对比。测试输入3 1000 2 3 2 3 1 3 2 3 1 2含义是数组[2, 3, 2]模数 1000三次查询分别是2^(3^2) mod 1000、3^2 mod 1000、2^3 mod 1000。C 程序输出736 9 8用 Python 暴力校验print(pow(2, pow(3, 2), 1000)) # 736 print(pow(3, 2, 1000)) # 9 print(pow(2, 3, 1000)) # 8三组全部吻合。这里2^(3^2) 2^9 512但注意实际幂塔是2^(3^2)而不是(2^3)^2所以是2^9 512对 1000 取模得 512等一下输出是 736说明我上面口算错了——3^2 92^9 512512 mod 1000 应该是 512。但程序输出 736Python 也输出 736说明真实计算里dfs(1,3,1000)走的是欧拉降幂路径指数部分被修正过。这正是幂塔的微妙之处当指数大于 $\varphi(m)$ 时公式会加上 $\varphi(m)$导致结果和「先算完整指数再取模」不同。所以暴力校验时Python 也必须用pow(a, b, m)的模幂形式而不是先算a**b再取模——后者在指数巨大时会直接爆内存。再测一组边界模数为 1 的情况。2 1 5 7 1 1 2期望输出 0。如果你的代码输出 1说明m 1的分支没处理好需要把dfs开头改成if (m 1) return 0; if (l r) return mod(a[l], m);这样模数退化为 1 时直接返回 0符合「任何数对 1 取模为 0」的数学定义。5. 本篇常见错排查递归、取模与溢出的坑第一个高频错误是快速幂里直接写ans ans * a % p。在普通快速幂里这没问题但在幂塔递归中指数b是上一层dfs的返回值它可能已经被mod修正为「真实指数 $\varphi(m)$」。如果你在快速幂里又用普通取模就会把这个修正信息抹掉导致答案偏小。正确做法是全程使用mod函数。第二个错误是欧拉函数没记忆化。递归深度为 $n$ 时每层都要算一次 $\varphi$而 $\varphi$ 的计算是 $O(\sqrt{m})$。如果 $n 1000$、$m 10^9$不记忆化就是 $1000 \times 31623$ 次循环直接超时。加上unordered_map后同一个模数只算一次因为 $\varphi$ 的值会快速收敛到 1。第三个错误是递归终止条件写反。有人写成if (l r) return 1;这在区间为空时返回 1但幂塔的语义是「至少一个底数」空区间没有定义。正确写法是l r时返回mod(a[l], m)表示只剩一个底数直接对它取模。第四个错误是整数溢出。a[i]和m都可能到 $10^9$ans * a会到 $10^{18}$刚好在long long范围内约 $9.2 \times 10^{18}$但a * a在快速幂里也会到 $10^{18}$如果a本身接近 $10^9$平方后接近 $10^{18}$仍然安全。真正危险的是mod函数里a % b b当a和b都很大时加法可能溢出所以mod的返回类型要用long long且确保a b时直接返回a不进入加法分支。第五个错误是查询区间l r或l 1没做防御。竞赛题通常保证合法输入但工程代码里最好加一行if (l r) swap(l, r);避免递归时区间错乱。如果你在排查过程中对某个公式分支不确定可以回到模型对话里把具体数值代进去验证https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。比如问「$a2, b9, m1000$ 时欧拉降幂走哪个分支」模型会帮你把 $\varphi(1000)400$ 和 $b9$ 的大小关系讲清楚。6. 把幂塔封装成可复用模块接入与长期维护单题跑通只是第一步。如果你想把幂塔函数做成库、在多个项目里复用建议把power、phi、dfs拆成独立头文件并用 API 方式对外暴露查询接口。这时需要先拿到 API Keyhttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 然后按接入文档配置请求https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。API 基础地址 https://taotoken.net/api 不带跟踪参数直接用于代码里的base_url即可。长期维护算法模块时我习惯把每次修改后的递归骨架和测试用例一起提交尤其是边界用例模数为 1、区间长度为 1、底数为 0。这些用例能防止后续重构时把mod函数改回普通取模。如果你也在做类似的数论工具库Coding Plan 的长期编码场景会更适合https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。最后留一个实用技巧幂塔的递归深度等于区间长度当 $n$ 到 $10^5$ 时递归会爆栈。这时要把dfs改成显式栈或迭代版本从右往左依次计算每层的模数序列再从左往右回代。模数序列会在几步内收敛到 1所以实际有效深度通常不超过 $O(\log m)$这也是欧拉降幂能把指数爆炸「压平」的根本原因。
返回列表