news 2026/8/27 3:10:50

多项式对数函数(多项式ln)算法详解:从数学原理到C++实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多项式对数函数(多项式ln)算法详解:从数学原理到C++实现

1. 项目概述:从一道题到一类算法

看到“P4725 【模板】多项式对数函数(多项式 ln)”这个标题,很多刚接触多项式科技(俗称“多项式全家桶”)的同学可能会有点懵。这看起来像是一道数学题,又像是一个算法实现。实际上,它正是算法竞赛(尤其是ICPC、OI)中一个非常经典且重要的“模板题”。所谓模板题,就是题目本身会提供一个清晰的接口定义,要求你实现一个特定的、可复用的算法函数。这道题的核心,就是要求你实现一个程序,对于给定的一个多项式,计算出它的自然对数(ln)所对应的另一个多项式。

这听起来有点抽象,多项式怎么还有对数?这其实是形式幂级数(Formal Power Series)理论在算法竞赛中的应用。我们不关心这个多项式作为函数在某个点的取值是否收敛,只关心它的系数之间存在的代数关系。实现这个“多项式 ln”是构建更复杂多项式操作(如指数函数exp、三角函数、快速幂)的基石。在洛谷这样的在线评测平台上,它被标记为“模板”,意味着你需要写出高效、正确的代码,并通过所有测试点,之后这个函数就可以像积木一样被你用在其他更复杂的题目中。我当年在啃这块硬骨头的时候,也是被各种推导和边界条件折腾得不轻,今天就把其中的门道和实现细节掰开揉碎了讲清楚。

2. 核心思路与数学原理拆解

2.1 问题形式化定义

首先,我们必须明确题目到底在问什么。题目会给出一个多项式 ( A(x) ) 的系数。通常,多项式表示为: [ A(x) = a_0 + a_1x + a_2x^2 + \cdots + a_{n-1}x^{n-1} ] 其中 ( n ) 是多项式的长度(或者说次数界)。注意,在算法实现中,我们通常只处理前 ( n ) 项。

我们要计算的是多项式 ( B(x) ),使得在形式幂级数的意义下满足: [ B(x) \equiv \ln(A(x)) \pmod{x^n} ] 这里的“模 ( x^n )”意思是,我们只关心计算结果的前 ( n ) 项系数,( x^n ) 及更高次项可以忽略。这是形式幂级数操作的常见约定。

2.2 从微积分到代数:核心推导

为什么这道题能用算法实现?关键是将分析学中的对数函数求导公式,转化为纯粹的代数运算。

对于可导函数 ( f(x) ),有 ( \frac{d}{dx} \ln(f(x)) = \frac{f'(x)}{f(x)} )。这个公式在形式幂级数中依然成立(作为形式导数的定义)。

因此,如果我们令 ( B(x) = \ln(A(x)) ),两边对 ( x ) 求形式导数,得到: [ B'(x) = \frac{A'(x)}{A(x)} ]

接下来是关键的一步:如果我们得到了 ( B'(x) ),那么通过对 ( B'(x) ) 进行形式积分,就可以还原出 ( B(x) )(忽略常数项,因为 ( \ln1 = 0 ),通常要求 ( a_0 = 1 ))。

于是,计算多项式 ln 的流程就清晰了:

  1. 计算输入多项式 ( A(x) ) 的形式导数( A'(x) )。
  2. 计算 ( A(x) ) 的形式逆元(乘法逆)( A^{-1}(x) )。
  3. 将 ( A'(x) ) 与 ( A^{-1}(x) ) 相乘,得到 ( B'(x) )。
  4. 对 ( B'(x) ) 进行形式积分,得到 ( B(x) )。

用公式表示就是: [ \ln(A(x)) \equiv \int \frac{A'(x)}{A(x)} dx \pmod{x^n} ]

注意:这个公式成立有一个极其重要的前提:( A(x) ) 的常数项 ( a_0 ) 必须为 1。因为在形式幂级数中,( \ln(A(x)) ) 的展开要求 ( A(0) = 1 ),否则常数项无法定义(ln0无意义)。在实际题目中,这通常作为输入约束给出。如果 ( a_0 \neq 1 ),我们需要通过缩放等方法将其化为1,这属于更进阶的技巧。

2.3 所需的基础算法模块

从上述流程可以看出,实现多项式 ln 并不是一个孤立的算法,它依赖于几个更基础的“积木”:

  • 多项式乘法:通常使用快速傅里叶变换(FFT)快速数论变换(NTT)实现。由于系数往往很大,需要取模,NTT 在算法竞赛中更为常用。
  • 多项式求逆:计算 ( A^{-1}(x) \mod x^n )。这是一个核心的递归/迭代算法,利用牛顿迭代法求解。
  • 多项式求导与积分:这是最简单的环节,时间复杂度为 ( O(n) )。

因此,在动手实现polyln之前,你必须已经拥有稳定、高效的多项式乘法(NTT)和多项式求逆模板。这就像盖房子,砖瓦(NTT)和钢筋(求逆)必须先准备好。

3. 算法实现与关键细节剖析

理解了数学原理,我们来一步步拆解代码实现。我将以最常见的、基于 NTT 和模数 998244353(原根为3)的 C++ 实现为例进行讲解。

3.1 准备工作:NTT 与多项式乘法模板

这是所有多项式操作的基础。你需要实现:

  • 快速数论变换(NTT)的正变换和逆变换函数。
  • 多项式乘法函数,它内部会处理长度扩展为2的幂、调用NTT、点乘、逆变换、规格化等一系列操作。
  • 常用的工具函数,如快速幂、求逆元等。
const int MOD = 998244353; // 常用模数 const int G = 3; // 模数的原根 // 省略:快速幂 qpow, 逆元 inv 等函数 // 省略:NTT 的蝴蝶变换、ntt() 函数 // 省略:多项式乘法 mul() 函数

实操心得:务必确保你的 NTT 模板是正确的。一个常见的检查方法是自己构造两个随机小多项式,用暴力乘法(O(n²))和 NTT 乘法分别计算,对比结果是否一致。这是后续所有高级操作的地基,地基不稳,全盘皆输。

3.2 核心依赖:多项式求逆

多项式求逆polyInv是 ln 算法的关键依赖。它的功能是,给定多项式 ( A(x) ),求多项式 ( B(x) ) 使得 ( A(x) * B(x) \equiv 1 \pmod{x^n} )。

算法采用牛顿迭代法。假设我们已经求出在模 ( x^{\lceil n/2 \rceil} ) 意义下的逆 ( B_0(x) ),则有迭代公式: [ B(x) \equiv 2B_0(x) - A(x) * B_0(x)^2 \pmod{x^n} ]

实现时需要注意长度处理和对齐。

// 假设 poly 为 vector<int> 类型,存储系数 vector<int> polyInv(const vector<int>& a, int n) { // 初始条件:a[0] 的逆元作为 B0 的常数项 vector<int> b(1, inv(a[0])); int len = 1; while (len < n) { len <<= 1; // 计算当前长度的 a 模 x^len vector<int> a_cut(a.begin(), a.begin() + min((int)a.size(), len)); // 计算 t = a_cut * b * b,注意长度限制 vector<int> t = mul(a_cut, b); t.resize(len); t = mul(t, b); t.resize(len); // 迭代公式:b_new = 2b - t vector<int> b_new(len); for (int i = 0; i < len; ++i) { b_new[i] = (2LL * b[i] - t[i] + MOD) % MOD; if (b_new[i] >= MOD) b_new[i] -= MOD; if (b_new[i] < 0) b_new[i] += MOD; } b.swap(b_new); } b.resize(n); return b; }

注意事项:在迭代过程中,参与乘法的多项式长度管理至关重要。a_cuta截断到当前长度len的部分。每次乘法后要立即resize(len)来模拟“模 ( x^{len} )”的操作,否则长度会翻倍,导致复杂度和内存剧增。这是新手最容易出错的地方之一。

3.3 主角登场:多项式对数函数实现

有了乘法和求逆,实现polyLn就水到渠成了。我们严格遵循公式 ( \ln(A) = \int \frac{A'}{A} dx )。

// 多项式求导 vector<int> polyDeri(const vector<int>& a) { int n = a.size(); if (n <= 1) return vector<int>(); // 常数求导为0 vector<int> res(n - 1); for (int i = 1; i < n; ++i) { res[i - 1] = 1LL * a[i] * i % MOD; } return res; } // 多项式积分 vector<int> polyInte(const vector<int>& a) { int n = a.size(); vector<int> res(n + 1); // 预处理逆元,避免每次调用快速幂 vector<int> inv(n + 2); inv[1] = 1; for (int i = 2; i <= n + 1; ++i) { inv[i] = 1LL * (MOD - MOD / i) * inv[MOD % i] % MOD; } for (int i = 0; i < n; ++i) { res[i + 1] = 1LL * a[i] * inv[i + 1] % MOD; } // 积分后常数项默认为0,符合 ln(A) 在 a0=1 时常数项为0的条件 return res; } // 多项式对数函数 vector<int> polyLn(const vector<int>& a, int n) { // 前置检查:常数项必须为1 assert(a[0] == 1); // 1. 求导 A' vector<int> a_deri = polyDeri(a); // 2. 求逆 A^{-1} vector<int> a_inv = polyInv(a, n); // 3. 计算 A' * A^{-1} vector<int> b_deri = mul(a_deri, a_inv); b_deri.resize(n); // 只保留前n项 // 4. 积分得到最终结果 B vector<int> b = polyInte(b_deri); b.resize(n); // 积分后长度+1,我们调整回目标长度n return b; }

踩坑记录

  1. 长度管理:每一步的resize都必不可少。求导后长度减1,与逆元相乘后要截断到n,积分后长度加1,最后再调整回n。混乱的长度是导致结果错误或运行时错误的常见原因。
  2. 常数项断言:务必检查a[0] == 1。如果题目输入不保证,你的程序需要处理或报错。这是数学定义的要求,不能忽略。
  3. 积分逆元优化:积分时需要乘以i的逆元。不要在循环内每次都调用qpow(i, MOD-2),这样会带来巨大的常数开销。务必像上面代码一样,线性预处理出1n+1的逆元。这是一个非常有效的常数优化技巧。

4. 模板的整合与使用示例

在实际解题(如洛谷 P4725)时,我们需要将上述所有模块整合到一个完整的程序中。程序框架通常包括:读入多项式长度n和系数,调用polyLn函数,输出结果系数。

下面是一个高度简化的主函数逻辑:

int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<int> a(n); for (int i = 0; i < n; ++i) { cin >> a[i]; } // 核心调用 vector<int> b = polyLn(a, n); // 输出结果 for (int i = 0; i < n; ++i) { cout << b[i] << " \n"[i == n - 1]; } return 0; }

性能与优化提示

  • 递归 vs 迭代:多项式求逆的牛顿迭代实现,有递归和迭代两种写法。上述代码展示了迭代写法,更容易理解长度翻倍的过程。递归写法代码更简洁,但可能稍难调试。
  • 内存复用:在性能要求极高的场景,可以预先分配好足够大的内存池,通过指针或数组下标进行操作,避免频繁的vector构造、析构和拷贝,但这会牺牲代码的可读性。在模板题阶段,清晰正确比极致优化更重要。
  • 封装:最好将 NTT、乘法、求逆、求导积分、Ln、Exp 等函数封装在一个命名空间或类里,避免全局命名冲突,也使代码结构更清晰。

5. 常见问题与调试技巧实录

即使理解了算法,第一次实现也难免出错。这里分享几个我踩过的坑和调试方法。

5.1 结果完全不对或随机

  • 检查NTT的正确性:这是根源。写一个test_ntt()函数,用暴力乘法对比。确保你的 NTT 能正确处理长度非2的幂的情况(通常是补零到2的幂)。
  • 检查模数与原根:确认MOD=998244353G=3。对于其他模数(如 1004535809),原根可能不同。
  • 检查求逆函数:单独测试polyInv。用一个小多项式(如[1, 2]),手动计算其逆(模 ( x^2 ) 下,[1, MOD-2]),看结果是否匹配。

5.2 部分点正确,部分点错误(特别是长数据)

  • 长度管理:这是最可能的原因。仔细检查polyLn流程中每一步的输入输出长度。
    • polyDeri(a):输入长度n,输出长度n-1
    • polyInv(a, n):输出长度应为n
    • mul(a_deri, a_inv):两个长度分别为n-1n的多项式相乘,结果长度是(n-1)+n-1 = 2n-2。但我们需要的是模 ( x^n ) 的结果,所以必须立即resize(n)。这里n指的是目标长度,通常等于输入多项式a的长度。
    • polyInte(b_deri):输入长度n,输出长度n+1。最后再resize(n)截断。
  • 数组越界:确保所有vector访问都在size()范围内。在乘法或求逆的内部循环中,尤其要注意。

5.3 运行超时

  • 复杂度分析:多项式求逆的复杂度是 ( O(n \log n) ),其中主要开销是 NTT 乘法。polyLn中进行了 1 次求导(O(n))、1 次求逆(多轮NTT)、1 次乘法(NTT) 和 1 次积分(O(n))。所以总复杂度依然是 ( O(n \log n) )。
  • 常数优化
    • 预处理逆元:如前所述,积分时预处理逆元。
    • 减少拷贝:使用引用传参const vector<int>&,避免不必要的拷贝。
    • NTT 预处理旋转因子:可以预先计算好不同长度下的单位根,减少重复计算。
    • 使用迭代版 NTT:递归版 NTT 有函数调用开销,迭代版(通过蝴蝶操作)更快。

5.4 调试工具与小技巧

  1. 对拍:写一个暴力的、复杂度 ( O(n^2) ) 的polyLn函数,用于小数据范围(如 n <= 10)的对比。随机生成数百组小数据,用你的高效程序和暴力程序对比输出。
  2. 中间输出:在polyLn的关键步骤后,打印中间多项式(如前5项系数),与手算的小例子进行对比。
  3. 静态检查清单:在提交前,心里默念检查:
    • [ ] 常数项为1?
    • [ ] 求导、积分公式写对了?(系数乘以指数,指数减一;积分是系数除以新指数)
    • [ ] 每一步的resize都正确?
    • [ ] 乘法后取模了吗?
    • [ ] 逆元预处理了吗?

实现一个正确的多项式 ln 模板,是进入多项式高级算法世界的敲门砖。它本身是求逆和乘法的简单组合,但精确的长度控制和每一步的细节处理,恰恰是算法实现功力的体现。把这个模板调通、理解透,后面再面对多项式指数函数(exp)、快速幂(pow)、开根(sqrt)时,你会发现它们都共享着相似的设计模式和迭代思想。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/27 3:10:48

用 m4s-converter 把B站缓存m4s转成MP4:零画质损失

用 m4s-converter 把B站缓存m4s转成MP4&#xff1a;零画质损失 【免费下载链接】m4s-converter 一个跨平台小工具&#xff0c;将bilibili缓存的m4s格式音视频文件合并成mp4 项目地址: https://gitcode.com/gh_mirrors/m4/m4s-converter 你缓存好的视频&#xff0c;打开缓…

作者头像 李华
网站建设 2026/8/27 3:10:26

开放权重模型调用占比62%,AI网关如何引领模型选型新趋势?

先给结论&#xff1a;Vercel AI Gateway 开放权重模型调用占比上升到 62%&#xff0c;这并不是某个宣传话术&#xff0c;而是开发者实际请求流量里一个很直观的结构变化。简单说&#xff0c;现在通过 AI Gateway 转发出去的模型请求&#xff0c;超过六成最终落在开放权重模型上…

作者头像 李华
网站建设 2026/8/27 3:10:23

ABAP IN BACKGROUND TASK 原理与高可用实践指南

1. 为什么“悄悄跑起来”不是一句空话&#xff1a;IN BACKGROUND TASK 的真实价值边界在 ABAP 开发中&#xff0c;我们常被要求“优化响应时间”“提升用户体验”“避免用户等待”。但现实里&#xff0c;很多开发者一看到耗时操作——比如生成千条凭证、导出万行报表、调用外部…

作者头像 李华
网站建设 2026/8/27 3:08:03

Agent Skills 改变 AI 生成 PPT 的实用路径与工程实践

AI 做 PPT 这件事&#xff0c;过去半年经历了两次明显变化。第一次是“对话生成 PPT”满地开花&#xff0c;你输入一句需求&#xff0c;工具吐出一套模板&#xff0c;看起来很快&#xff0c;但改版式、调逻辑、换配色常常比从头做还痛苦。第二次就是现在正在发生的&#xff1a;…

作者头像 李华
网站建设 2026/8/27 3:07:46

模块化建筑资产全流程:Blender制作与UE5拼接实战

在游戏场景或影视剧背景的制作中&#xff0c;有一个重复出现的痛点&#xff1a;建筑资产做了一套&#xff0c;结果换楼层布局要推倒重来&#xff1b;单栋楼做得再精致&#xff0c;等需要组成一条街道、一片营地时&#xff0c;资源量直接翻倍&#xff0c;内存和贴图开销也随之失…

作者头像 李华
网站建设 2026/8/27 3:07:29

从零制作RGB LED控制器:电路设计到Python联动调光

开头 做嵌入式或者DIY电子这块儿的朋友&#xff0c;应该都有过这样的经历&#xff1a;拿到一颗RGB LED&#xff0c;想让它按照自己的节奏亮起来、变色、甚至跟着音乐闪烁&#xff0c;但真到动手的时候&#xff0c;发现“不就是一个灯吗”这事儿远比想象中复杂。 我这两年断断续…

作者头像 李华