
1. 多项式除法从数学概念到算法实现在算法竞赛和计算机科学的学习中多项式运算是一个绕不开的话题。它不仅是数学分析的基础更在信号处理、编码理论如CRC校验、机器学习如多项式回归等领域有着广泛的应用。今天我们不谈那些高深的算法就从一个看似基础却让不少初学者“卡壳”的点入手多项式的手动长除法以及如何用程序精确地实现它。题目“L2-018 多项式A除以B”正是这样一个经典的练手题。它要求你实现两个一元多项式的除法运算输出商式Q和余式R。很多朋友一看到“除法”可能下意识地想调用库函数或者用数值方法近似求解。但在算法题的世界里尤其是在处理系数可能为浮点数、要求精确输出的场景下我们需要回归到多项式除法的代数定义和计算过程本身。这就像做整数除法你不能只告诉计算机“10除以3等于3.333...”而必须明确商是3余数是1。多项式除法也是同样的道理核心是确定每一次“消元”时商式的当前项系数是多少以及如何更新被除式。理解这个过程不仅能帮你轻松拿下这类题目更能让你深刻体会到许多复杂算法比如CRC校验中的模2除法、多项式拟合中的基函数处理其底层思想都与这个朴素的手算过程一脉相承。接下来我们就抛开对库函数的依赖一步步拆解如何用代码“模拟手算”实现精确的多项式除法。2. 算法核心模拟手算的“降次消元”法多项式除法的核心过程与我们小学学过的多位数除法非常相似都是一个“试商、乘、减”的循环。假设我们有两个多项式被除式 A(x) a_n*x^n a_{n-1}*x^{n-1} ... a_0除式 B(x) b_m*x^m b_{m-1}*x^{m-1} ... b_0 (其中 b_m ≠ 0)我们的目标是找到商式 Q(x) 和余式 R(x)使得 A(x) B(x) * Q(x) R(x)并且余式 R(x) 的次数严格小于除式 B(x) 的次数 m。手算过程的计算机翻译 这个过程完全可以被翻译成一个清晰的循环算法初始化商式 Q 初始化为空或全零当前余式 R 初始化为被除式 A。循环条件只要当前余式 R 的最高次项的次数deg(R)大于等于除式 B 的最高次数deg(B)就继续循环。单步迭代一次消元 a.试商计算当前商项系数q_coef r_lead / b_lead其中r_lead是 R 当前最高次项的系数b_lead是 B 最高次项的系数。计算当前商项次数q_exp deg(R) - deg(B)。 b.记录商项将(q_exp, q_coef)加入到商式 Q 中。 c.构造减式用刚刚得到的商项(q_exp, q_coef)去乘整个除式 B得到一个中间多项式Tmp B * (q_coef * x^{q_exp})。 d.更新余式令R R - Tmp。这一步就是完成一次“消元”消去了 R 当前的最高次项。循环结束当deg(R) deg(B)时循环终止。此时的 R 就是最终的余式而 Q 就是累积的商式。这个算法的美妙之处在于它直接模拟了我们笔算时在草稿纸上的操作。每一次迭代都明确地消除被除式或当前余式中的当前最高次项直到不能再消除为止。2.1 数据结构的选择为何不用数组而用map在动手实现前数据结构的选择至关重要。一个多项式本质是一系列(指数 系数)对的集合。最直观的想法可能是用数组下标表示指数值表示系数。例如poly[5] 3.2表示3.2*x^5。但这种方法有致命缺陷空间浪费多项式往往是稀疏的。一个最高次项为1000的多项式可能只有不到10个非零项。用数组会浪费大量空间。操作不便在除法过程中我们需要频繁地查找当前最高次项、插入新的项在更新余式时减法可能产生新的指数项、删除系数变为零的项。用数组实现这些操作效率低下。因此使用std::map或std::unordered_mapC是更优的选择。这里更推荐std::map因为它能自动按键指数排序这带来一个巨大优势我们可以通过rbegin()直接获取当前最高次项而无需遍历整个容器。// 使用 mapint, double 表示多项式key是指数value是系数 mapint, double A, B, Q, R; // 获取当前多项式的最高次项假设非空 auto get_leading_term(const mapint, double poly) { return *poly.rbegin(); // rbegin() 返回指向最大key的迭代器 }map的排序特性让“找最高次项”这个核心操作变成了 O(1) 复杂度极大地简化了逻辑。2.2 浮点数比较的“坑”与处理题目中系数是“实数”在计算机中用浮点数double表示。浮点数的精度问题是一个经典陷阱。在多项式运算中经过多次乘法和加减一个理论上应为零的系数可能存储为1e-15这样极小的值。如果不对这些“近似零”进行处理会导致输出不符合要求题目通常要求系数保留1位小数而0.0000001会被输出为0.0但它仍作为一个项存在影响项数统计。在判断多项式是否为零、或查找最高次项时一个系数极小的项会被误判为有效项导致算法逻辑错误或死循环。关键处理必须定义一个精度EPS例如1e-6或1e-8当系数的绝对值小于EPS时就认为该项为零将其从多项式中删除。这个操作需要在每次更新多项式特别是余式 R后执行。const double EPS 1e-6; void normalize(mapint, double poly) { vectorint to_erase; for (auto [exp, coef] : poly) { if (fabs(coef) EPS) { to_erase.push_back(exp); } } for (int exp : to_erase) { poly.erase(exp); } } // 在 R R - Tmp 后立即调用 normalize(R);3. 从理论到代码一步步实现除法器有了清晰的理论和数据结构设计我们可以开始编码了。整个过程分为输入解析、除法核心循环、输出格式化三大块。3.1 输入解析与存储输入格式通常是先给出多项式的项数 K然后跟着 K 对(指数N, 系数aN)。我们需要按指数从高到低的顺序读入并直接存入map中。由于map会自动按 key 排序我们读入时无需关心顺序。mapint, double read_poly() { mapint, double poly; int k; cin k; for (int i 0; i k; i) { int exp; double coef; cin exp coef; // 题目保证输入指数递减但用map后顺序不再重要 // 注意系数可能为0吗根据题意通常不为0但为健壮性可判断 if (fabs(coef) EPS) { poly[exp] coef; } } return poly; } int main() { mapint, double A read_poly(); mapint, double B read_poly(); // ... 后续除法运算 }3.2 除法核心循环的实现细节这是整个程序的心脏。我们需要严格按照第2章描述的循环来实现。mapint, double Q, R A; // 初始化余式R为被除式A int deg_B B.empty() ? -1 : B.rbegin()-first; // 获取除式B的次数 while (!R.empty()) { // 1. 获取当前余式R的leading term auto r_lead_it R.rbegin(); // 指向最高次项 int r_exp r_lead_it-first; double r_coef r_lead_it-second; // 2. 判断循环条件余式次数 除式次数 if (r_exp deg_B) break; // 3. 计算本次的商项 double b_lead_coef B.rbegin()-second; // 除式首项系数 int q_exp r_exp - deg_B; double q_coef r_coef / b_lead_coef; // 4. 将商项加入商式Q Q[q_exp] q_coef; // 使用因为同次数的项可能合并虽然除法中通常不会 // 5. 构造减式 B * (q_coef * x^{q_exp}) 并更新余式 R for (const auto [b_exp, b_coef] : B) { int new_exp b_exp q_exp; double delta_coef - (q_coef * b_coef); // 注意是减去所以取负号 R[new_exp] delta_coef; } // 6. 关键步骤规范化余式R清除系数近似为零的项 normalize(R); } // 循环结束后R即为最终余式Q为商式 // 同样需要对Q也做一次normalize确保没有近似零项 normalize(Q);几个值得注意的实现要点循环条件while (!R.empty())结合if (r_exp deg_B) break;是安全的。即使R非空但只要其最高次项次数已小于deg_B就必须立即退出。更新余式的技巧在R[new_exp] delta_coef;这一步我们直接利用了map的特性。如果键new_exp不存在operator[]会自动插入一个默认构造的值0.0然后加上delta_coef。这完美实现了多项式的加法实际上是减法。normalize的调用时机必须在每次更新R后立即调用。因为本次减法可能产生新的近似零项如果不清理在下一次循环中R.rbegin()获取到的可能就是这些“垃圾项”导致计算出错或死循环。3.3 输出格式化四舍五入与项数统计输出要求通常是先输出商式/余式的项数 K然后按指数递减顺序输出非零项系数保留1位小数。这里的坑点在于四舍五入。我们不能在运算过程中对系数进行四舍五入否则会累积误差破坏计算精度。正确的做法是在最终输出前对Q和R中的每一项系数进行四舍五入到一位小数并再次判断四舍五入后是否为零。void round_and_output(const mapint, double poly) { vectorpairint, double vec; for (const auto [exp, coef] : poly) { double rounded_coef round(coef * 10) / 10.0; // 四舍五入到一位小数 if (fabs(rounded_coef) 0.05) { // 四舍五入后判断是否为零阈值0.05 vec.emplace_back(exp, rounded_coef); } } // 输出项数 cout vec.size(); if (vec.empty()) { cout 0 0.0; // 特殊处理零多项式 } else { // 因为map是升序我们需要逆序输出 for (auto it vec.rbegin(); it ! vec.rend(); it) { printf( %d %.1f, it-first, it-second); } } cout endl; }注意零多项式的处理。如果经过四舍五入后所有项都为零那么应该输出“0 0 0.0”或其他题目规定的格式。这是一个常见的边界条件务必检查。4. 边界条件与异常处理让你的程序更健壮任何实用的算法都必须考虑边界情况。对于多项式除法以下几个边界条件需要特别注意4.1 除式为零多项式如果除式 B 是零多项式所有系数为零那么除法是无定义的。在算法题中题目通常会保证 B 不为零多项式。但在自己的代码中我们可以添加一个防御性检查if (B.empty()) { // 根据题目要求处理可能是输出错误或特殊值 // 通常题目不会出现此情况 }4.2 被除式次数低于除式如果deg(A) deg(B)那么循环一次都不会进入。此时商式 Q 应该是零多项式余式 R 等于 A。我们的算法能自然地处理这种情况因为初始化R A后第一次进入循环条件判断r_exp deg_B就会成立直接跳出循环Q 保持为空即零多项式。4.3 产生系数精确为零的项在更新余式R R - Tmp时有可能两个相同的项相减产生理论值恰好为零的项。由于浮点数精度它可能是一个很小的数。这就是为什么normalize函数如此重要。但还有一种情况如果系数本来就是整数且计算过程也是整数运算比如用int或long long那么可能产生精确的零。这时即使没有浮点误差也需要将其删除。因此normalize中的零值判断是必须的。4.4 输出格式的严格性算法竞赛对输出格式要求极其严格。需要注意空格和换行严格按照题目要求是每个数据间有空格还是行末无多余空格。系数为负数的输出printf( %d %.1f, exp, coef)会保留负号格式是符合要求的。零多项式的输出务必确认题目对零多项式的定义。常见的格式是“0 0 0.0”但有些题目可能只输出一个“0”。5. 完整代码参考与逐行解析将以上所有部分组合起来下面是一个完整的、带有详细注释的C实现。这个版本注重可读性和健壮性可以直接作为理解算法的模板。#include iostream #include map #include vector #include cmath #include algorithm using namespace std; const double EPS 1e-8; // 定义精度阈值 // 工具函数清理多项式中的近似零项 void normalize(mapint, double poly) { vectorint zero_exps; for (auto item : poly) { if (fabs(item.second) EPS) { zero_exps.push_back(item.first); } } for (int exp : zero_exps) { poly.erase(exp); } } // 工具函数读取一个多项式 mapint, double read_poly() { int k; cin k; mapint, double poly; for (int i 0; i k; i) { int exp; double coef; cin exp coef; // 理论上输入系数非零这里直接存入 poly[exp] coef; } return poly; } // 工具函数输出多项式包含四舍五入和零项过滤 void output_poly(const mapint, double poly) { vectorpairint, double items; for (const auto item : poly) { double rounded_coef round(item.second * 10) / 10.0; // 四舍五入到一位小数 if (fabs(rounded_coef) 0.05) { // 判断四舍五入后是否有效 items.emplace_back(item.first, rounded_coef); } } // 处理零多项式 if (items.empty()) { cout 0 0 0.0; return; } // 输出项数 cout items.size(); // map默认按key升序排列输出需要降序 for (auto it items.rbegin(); it ! items.rend(); it) { printf( %d %.1f, it-first, it-second); } } int main() { // 1. 读入数据 mapint, double A read_poly(); mapint, double B read_poly(); // 2. 初始化商Q和余式R mapint, double Q, R A; // 3. 获取除式B的次数题目保证B非零 if (B.empty()) { // 防御性代码实际题目不会出现 return 0; } int deg_B B.rbegin()-first; // B的最高次项指数 // 4. 核心除法循环 while (!R.empty()) { // 获取当前余式R的最高次项 auto r_lead *R.rbegin(); // pairexp, coef int r_exp r_lead.first; double r_coef r_lead.second; // 检查是否继续余式次数 除式次数 if (r_exp deg_B) break; // 计算本次商项 double b_lead_coef B.rbegin()-second; int q_exp r_exp - deg_B; double q_coef r_coef / b_lead_coef; // 将商项加入商式Q Q[q_exp] q_coef; // 计算 B * (q_coef * x^{q_exp})并从R中减去 for (const auto term : B) { int b_exp term.first; double b_coef term.second; int new_exp b_exp q_exp; double delta -(q_coef * b_coef); // 注意是减所以取负 R[new_exp] delta; } // 关键步骤立即规范化R清除计算产生的近似零项 normalize(R); } // 5. 循环结束后对商式Q也做一次规范化虽然通常不需要但为安全起见 normalize(Q); normalize(R); // 最终余式也再清理一次 // 6. 输出结果 output_poly(Q); cout endl; output_poly(R); cout endl; return 0; }代码逐行解析与避坑点第8行EPS定义这个值不宜过小如1e-12因为保留一位小数输出时1e-8的量级足够被视为零。也不宜过大如1e-4以免误删有效的小系数项。第40行round(item.second * 10) / 10.0这是实现四舍五入到一位小数的标准方法。round函数对正负数都能正确处理。注意不要用(int)(x*10 0.5)这种只对正数有效的方法。第41行fabs(rounded_coef) 0.05这是判断四舍五入后是否为零的阈值。为什么是0.05因为系数保留一位小数如果一个数四舍五入后是0.0那么它的原始值一定在[-0.05, 0.05)区间内。0.05本身四舍五入为0.1所以用0.05可以正确判断。第70行while (!R.empty())循环条件检查R是否为空。如果一开始A就是零多项式或者某次normalize后R变为空循环会正确终止。第78行if (r_exp deg_B) break;这是真正的计算终止条件。即使R非空但只要其最高次项次数已小于B的次数除法过程就结束了。第89-95行的更新循环这是整个算法中计算量最大的部分复杂度约为 O(deg(Q) * M)其中 M 是 B 的项数。对于稠密多项式这是可以接受的。注意这里直接修改了R。第98行normalize(R)这是保证算法正确的生命线。务必在每次更新R后立即调用确保下一次循环R.rbegin()拿到的是真正的最高次项。6. 测试用例与调试技巧再好的算法没有经过充分测试也是不可靠的。设计测试用例是编码的一部分。基础测试用例普通除法A: 4 4 3 2 1 0(表示 4x^4 3x^2 2x 1),B: 2 1 0(表示 x^2 1)。手算验证商和余式。整除情况A: 3 2 1 0(x^3 x^2 x 1),B: 1 0(x 1)。结果商应为x^2 1余式为0。被除式次数更低A: 2 1 0(x^2 1),B: 3 1 0(x^3 1)。结果商应为0余式等于A。系数为浮点数A: 2 3.5 1 -2.2,B: 1 1.0 0。计算并检查精度。零多项式A: 0 0 0.0,B: 1 1 0。商和余式都应为0。调试技巧打印中间过程在核心循环中打印出每次迭代的q_exp, q_coef以及更新前后的R。这是最直接的调试方法。对比手算用简单的、系数为整数的例子比如(x^3 2x 1) / (x 1)在纸上演算然后与程序输出对比。检查normalize在normalize函数中打印被删除的项确认那些“近似零”确实被清除了没有误删有效项。边界测试专门测试B为单项式如2x^3的情况以及A和B最高次项系数相除结果为1或-1的情况这些情况容易暴露索引和符号错误。7. 算法扩展与关联应用实现基础的多项式除法远不是终点。理解了这个过程你可以轻松扩展到更多相关领域多项式求模取余在CRC循环冗余校验等通信编码中核心就是二进制系数的多项式模2除法。我们的算法稍加修改系数取模2加减法变为异或运算即可实现。这正是“发送信息11001001”进行CRC校验背后的数学原理。多项式求最大公因式GCD可以使用欧几里得算法反复做多项式除法直到余式为零。最后的非零余式就是最大公因式。多项式插值与拟合在多项式拟合中有时需要将一个高次多项式除以另一个多项式来进行化简或分析。符号计算如果你需要实现一个简单的符号计算系统多项式除法是基础功能之一。链接到其他算法许多复杂算法都内嵌了多项式运算的思想。例如在快速傅里叶变换FFT用于多项式乘法时理解多项式的代数结构是基础。一些优化算法如求解方程组的迭代法也会涉及到多项式求值而求值过程可以看作是多项式的一种特殊形式。手动实现这个算法的价值不在于解决这一个问题而在于掌握将数学运算过程精确转化为计算机指令的思维。这种“模拟手算”的算法设计思想在实现大数运算、矩阵运算、甚至某些几何算法时都会用到。当你下次遇到“模拟”类题目时你会更加从容先想清楚人是怎么做的再一步步翻译给计算机处理好边界和精度代码自然就水到渠成了。