尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

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

多项式对数函数(多项式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 的流程就清晰了计算输入多项式 ( A(x) ) 的形式导数( A(x) )。计算 ( A(x) ) 的形式逆元乘法逆( A^{-1}(x) )。将 ( A(x) ) 与 ( A^{-1}(x) ) 相乘得到 ( B(x) )。对 ( 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 为 vectorint 类型存储系数 vectorint polyInv(const vectorint a, int n) { // 初始条件a[0] 的逆元作为 B0 的常数项 vectorint b(1, inv(a[0])); int len 1; while (len n) { len 1; // 计算当前长度的 a 模 x^len vectorint a_cut(a.begin(), a.begin() min((int)a.size(), len)); // 计算 t a_cut * b * b注意长度限制 vectorint t mul(a_cut, b); t.resize(len); t mul(t, b); t.resize(len); // 迭代公式b_new 2b - t vectorint 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_cut是a截断到当前长度len的部分。每次乘法后要立即resize(len)来模拟“模 ( x^{len} )”的操作否则长度会翻倍导致复杂度和内存剧增。这是新手最容易出错的地方之一。3.3 主角登场多项式对数函数实现有了乘法和求逆实现polyLn就水到渠成了。我们严格遵循公式 ( \ln(A) \int \frac{A}{A} dx )。// 多项式求导 vectorint polyDeri(const vectorint a) { int n a.size(); if (n 1) return vectorint(); // 常数求导为0 vectorint res(n - 1); for (int i 1; i n; i) { res[i - 1] 1LL * a[i] * i % MOD; } return res; } // 多项式积分 vectorint polyInte(const vectorint a) { int n a.size(); vectorint res(n 1); // 预处理逆元避免每次调用快速幂 vectorint 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) 在 a01 时常数项为0的条件 return res; } // 多项式对数函数 vectorint polyLn(const vectorint a, int n) { // 前置检查常数项必须为1 assert(a[0] 1); // 1. 求导 A vectorint a_deri polyDeri(a); // 2. 求逆 A^{-1} vectorint a_inv polyInv(a, n); // 3. 计算 A * A^{-1} vectorint b_deri mul(a_deri, a_inv); b_deri.resize(n); // 只保留前n项 // 4. 积分得到最终结果 B vectorint b polyInte(b_deri); b.resize(n); // 积分后长度1我们调整回目标长度n return b; }踩坑记录长度管理每一步的resize都必不可少。求导后长度减1与逆元相乘后要截断到n积分后长度加1最后再调整回n。混乱的长度是导致结果错误或运行时错误的常见原因。常数项断言务必检查a[0] 1。如果题目输入不保证你的程序需要处理或报错。这是数学定义的要求不能忽略。积分逆元优化积分时需要乘以i的逆元。不要在循环内每次都调用qpow(i, MOD-2)这样会带来巨大的常数开销。务必像上面代码一样线性预处理出1到n1的逆元。这是一个非常有效的常数优化技巧。4. 模板的整合与使用示例在实际解题如洛谷 P4725时我们需要将上述所有模块整合到一个完整的程序中。程序框架通常包括读入多项式长度n和系数调用polyLn函数输出结果系数。下面是一个高度简化的主函数逻辑int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin n; vectorint a(n); for (int i 0; i n; i) { cin a[i]; } // 核心调用 vectorint 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的幂。检查模数与原根确认MOD998244353和G3。对于其他模数如 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-1和n的多项式相乘结果长度是(n-1)n-1 2n-2。但我们需要的是模 ( x^n ) 的结果所以必须立即resize(n)。这里n指的是目标长度通常等于输入多项式a的长度。polyInte(b_deri)输入长度n输出长度n1。最后再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 vectorint避免不必要的拷贝。NTT 预处理旋转因子可以预先计算好不同长度下的单位根减少重复计算。使用迭代版 NTT递归版 NTT 有函数调用开销迭代版通过蝴蝶操作更快。5.4 调试工具与小技巧对拍写一个暴力的、复杂度 ( O(n^2) ) 的polyLn函数用于小数据范围如 n 10的对比。随机生成数百组小数据用你的高效程序和暴力程序对比输出。中间输出在polyLn的关键步骤后打印中间多项式如前5项系数与手算的小例子进行对比。静态检查清单在提交前心里默念检查[ ] 常数项为1[ ] 求导、积分公式写对了系数乘以指数指数减一积分是系数除以新指数[ ] 每一步的resize都正确[ ] 乘法后取模了吗[ ] 逆元预处理了吗实现一个正确的多项式 ln 模板是进入多项式高级算法世界的敲门砖。它本身是求逆和乘法的简单组合但精确的长度控制和每一步的细节处理恰恰是算法实现功力的体现。把这个模板调通、理解透后面再面对多项式指数函数exp、快速幂pow、开根sqrt时你会发现它们都共享着相似的设计模式和迭代思想。
返回列表