POJ 1001-1009 C++题解:高精度、哈希与模拟算法实战精析

发布时间:2026/7/9 20:35:00

POJ 1001-1009 C++题解:高精度、哈希与模拟算法实战精析 1. 项目概述与价值定位最近在整理自己大学时期的代码仓库翻出来一个尘封已久的文件夹里面是我当年刷北京大学在线评测系统POJ前100题时写下的C代码。从1001到1009这九道题可以说是无数算法竞赛初学者的“新手村”任务也是检验编程基本功的试金石。我重新审视了这些代码发现当年的实现虽然能AC通过但在代码风格、算法效率和工程化思维上都有太多可以优化的地方。于是我决定以现在的视角重新梳理这九道题的完整C实现并分享其中踩过的坑、悟出的道。POJ 1001-1009这组题目涵盖了高精度计算、模拟、简单搜索、动态规划入门等核心算法思想。对于初学者而言它们不仅仅是九道独立的题目更是一个完整的、循序渐进的算法思维训练闭环。网上虽然有很多零散的题解但往往只给出核心代码缺乏对问题本质的剖析、对多种解法的对比以及对工程实践中细节陷阱的深入探讨。这份完整的实现与解析旨在填补这一空白。无论你是正在备战蓝桥杯、ACM校赛的新手还是希望夯实C编程与算法基础的在职开发者相信这份结合了实战代码与深度思考的总结都能让你在理解“怎么做”的同时更明白“为什么这么做”以及“如何做得更好、更稳”。2. 解题环境与核心思路总览在深入每一道题之前我们先统一解题环境和宏观思路。我使用的环境是C11/14标准编译器为G或Clang。POJ的评测系统相对传统但支持现代C的特性。一个良好的开始是准备一个简洁的模板包含常用的头文件和加速语句。#include iostream #include cstdio #include vector #include string #include algorithm #include cstring #include cmath // 其他头文件按需引入 using namespace std; // 对于需要大量I/O的题目可以关闭同步流以提升速度但注意与scanf/printf混用的风险 // ios::sync_with_stdio(false); // cin.tie(nullptr);这九道题可以大致分为三类高精度运算、模拟与枚举、基础算法应用。我的核心解题思路遵循以下步骤彻底理解题意与数据范围这是最重要却最容易被忽视的一步。POJ的题目描述有时比较“古朴”需要仔细阅读明确输入输出格式、边界条件如极值、零、负数等。抽象问题模型将实际问题转化为算法问题。例如1001的求高精度幂本质是高精度乘法1002的号码标准化本质是字符串处理与排序计数。选择合适的数据结构与算法在准确理解模型的基础上选择最直接、最不易出错的解法。对于新手不一定要追求最优时间复杂度代码的清晰正确性优先。设计测试用例在编码前或编码中设计涵盖常规、边界、特殊情况的测试用例。这是保证一次通过率的关键。编码与调试实现代码并利用设计的测试用例进行验证。注意POJ的题目输入输出通常使用标准输入输出且多数题目对格式要求严格行末空格、多余空行都可能导致错误。建议先完全按照样例格式编写通过后再考虑优化。3. POJ 1001: 高精度幂运算的陷阱与实现3.1 问题重述与难点分析POJ 1001 “Exponentiation” 要求计算 R^n其中 R 是一个实数0.0 R 99.999n 是整数0 n 25。难点在于R 最多有6位字符包括小数点和小数点后的数字n 最大为25直接使用double进行浮点数计算会遇到精度问题导致结果不准确WA。因此必须采用高精度计算来模拟小数的乘法过程。核心难点拆解精度丢失浮点数的二进制表示无法精确表示所有十进制小数连续乘法会放大误差。小数点的处理需要将小数乘法转化为整数乘法最后再确定小数点的位置。前导零与后导零的处理结果中整数部分开头不能有多余的零小数部分末尾也不能有多余的零。结果为纯整数或纯小数的输出格式例如10.000的平方是100而不是100.0000.1的平方是0.01需要保留前导零。3.2 高精度乘法的实现策略我们采用字符串或数组来存储大整数。这里使用vectorint来存储每个元素代表一位十进制数0-9采用倒序存储低位在前高位在后方便进位处理。高精度乘法函数multiply的实现思路 给定两个高精度整数a和b已去除小数点并倒序存储结果的长度最多为len(a) len(b)。我们模拟竖式乘法vectorint multiply(const vectorint a, const vectorint b) { int len_a a.size(), len_b b.size(); vectorint result(len_a len_b, 0); // 初始化结果数组全部置0 for (int i 0; i len_a; i) { int carry 0; // 每行乘法的进位 for (int j 0; j len_b; j) { // 当前位的乘积加上之前的进位再加上此位置原有的值来自低位的进位 int sum a[i] * b[j] carry result[i j]; result[i j] sum % 10; // 当前位结果 carry sum / 10; // 新的进位 } // 处理最高位的进位 if (carry 0) { result[i len_b] carry; } } // 去除结果高位的0注意保持至少一位如果结果是0 while (result.size() 1 result.back() 0) { result.pop_back(); } return result; }3.3 小数点定位与格式化输出这是本题最繁琐的部分考验细心程度。输入处理读入字符串R_str和整数n。找出R_str中小数点 ‘.’ 的位置dot_pos。如果没有小数点则小数位数为0。转换为整数移除小数点得到一个整数字符串int_str。记录原始小数位数decimal_places (dot_pos string::npos) ? 0 : (R_str.length() - dot_pos - 1)。计算总小数位数结果的总小数位数total_decimal decimal_places * n。高精度计算将int_str转换为高精度整数base然后进行n次自乘或使用快速幂思想优化但n25直接乘即可。插入小数点得到的结果数组是倒序的低位在前。我们需要将其转换为正序字符串并根据total_decimal插入小数点。如果total_decimal 0直接输出整数。如果total_decimal result.size()说明结果是纯小数需要在最前面补0格式为“0.xxx”。否则在正序字符串的倒数第total_decimal位前插入小数点。去除无效零去除尾部零小数部分从末尾开始去除连续的0。如果去除后小数点后没有数字则连小数点也去掉。去除头部零整数部分从开头开始去除连续的0但要保留至少一位如果整数部分全为0则在经过上一步后可能只剩下“0”或“.”需特殊处理成“0”。实操心得这里极易出错。一个有效的调试方法是将中间变量如去除小数点后的整数、总小数位数、乘法结果、插入小数点后的字符串都打印出来与手算过程对比。特别要测试n0题目已排除、R0.0、R10.0、R0.00001等边界情况。POJ的测试数据很全面会考察所有这些细节。4. POJ 1002: 电话号码归一化与哈希映射4.1 问题建模与思路选择POJ 1002 “487-3279” 要求将大量不同格式的电话号码可能包含字母、数字、横线统一转换为标准数字格式XXX-XXXX并统计每个号码出现的次数最后输出出现次数大于1的号码及其次数。核心挑战数据量最大为10万条。如果使用mapstring, int直接存储标准格式字符串并进行统计在C中特别是旧版POJ环境可能会导致超时TLE因为字符串的比较和哈希开销在数据量大时比较显著。优化思路转换为整数键值将标准格式的电话号码7位数字转换为一个唯一的整数例如直接拼接成int或long long。这样键的比较和哈希效率远高于字符串。使用更高效的容器将整数作为键使用unordered_mapC11POJ可能不支持或map。由于整数范围是0到9999999不到一千万也可以考虑使用**桶排序计数排序**的思想直接用一个大小为10000000的数组来计数这是效率最高的方法但需要注意内存约40MB在POJ限制内。4.2 关键实现字符到数字的映射题目给出了字母到数字的映射规则ABC-2, DEF-3, ...。我们可以用一个数组或函数来实现这个映射。char mapCharToDigit(char c) { if (isdigit(c)) return c; if (c A c Z) { // 注意题目输入可能包含小写字母需要先转换为大写 c toupper(c); if (c O) return 2 (c - A) / 3; else if (c S) return 7; else if (c V) return 8; else return 9; } return \0; // 非数字非字母字符如‘-’在预处理时已过滤 }处理每条号码时遍历每个字符过滤掉‘-’将字母映射为数字收集满7个数字后转换为整数键。long long key 0; for (char ch : raw_str) { if (ch -) continue; char digit mapCharToDigit(ch); if (digit) { key key * 10 (digit - 0); } }4.3 性能对比与容器选择我实测了三种方法mapstring, int逻辑最简单但POJ上大概率TLE。maplong long, int将7位数字转为long long作为键效率比字符串map有显著提升通常可以AC。桶排序数组声明int count[10000000] {0};直接将key作为下标进行计数。这是最快的O(n)方法。需要注意key的范围是0~9999999数组大小正好。输出时遍历数组将出现次数1的key格式化输出为XXX-XXXX。注意事项使用桶排序数组时初始化这么大的数组特别是全局静态数组要确保内存足够。在函数内部声明如此大的数组可能导致栈溢出应声明为全局变量或静态变量。格式化输出时printf(“%03lld-%04lld\n”, key/10000, key%10000)可以方便地补零。另外题目要求按升序输出号码桶排序数组自然满足顺序而map本身有序。5. POJ 1003-1005: 数学模拟与边界思维5.1 POJ 1003: 叠卡片问题这是一个经典的物理/数学模拟题。已知卡片长度L2.0从上往下叠第1张卡片最多可以伸出桌面 1/2 * L第2张可以再伸出 1/4 * L第3张是 1/6 * L以此类推即第n张伸出 L/(2n)。给定一个目标伸出长度S求最少需要多少张卡片。解题要点这本质上是一个调和级数部分和的问题。我们需要计算sum 1/2 1/4 1/6 ... 1/(2n)直到sum S/L。注意S是浮点数比较时使用而非以避免浮点误差。循环累加即可n通常很小。陷阱题目输入以0.00结束需要持续读入。输出格式要求“3 card(s)”这样的形式。5.2 POJ 1004: 财务管理这道题可能是最简单的一道求12个浮点数的平均值。但恰恰是简单题容易翻车。浮点数精度虽然可以直接用double累加除以12但为了严谨可以先将所有数以分为单位乘以100转为整数进行累加得到总分数最后再除以12并格式化输出。格式化输出要求输出一个精确到美分的美元数printf(“$%.2f\n”, average)是最简单的。注意POJ的G编译器double用%lf读取%f输出。5.3 POJ 1005: I Think I Need a Houseboat这是一道有趣的几何应用题。每年半圆形的侵蚀面积扩大50平方英里area_increase 50。给定一个坐标点(x, y)求多少年后这个点会被侵蚀。解题思路计算该点到原点(0,0)的距离d sqrt(x*x y*y)。这就是当前未被侵蚀区域的半径因为侵蚀是从原点开始的圆形扩张。被侵蚀的区域是一个半圆其面积公式为A 1/2 * π * r^2。题目说每年面积增加50所以第n年后的侵蚀面积为A_n 50 * n。我们需要解方程1/2 * π * r_n^2 50 * n 对于给定的点当r_n d时点被侵蚀。所以n ceil( (π * d^2) / 100 )。这里ceil是向上取整因为年份是整数且一旦面积超过阈值当年即被侵蚀。由于π的取值会影响结果为了与评测数据匹配通常取π 3.1415926或直接用acos(-1.0)。实操心得这道题的关键是理解“半圆面积每年增加50”这个模型。很多初学者会误用整圆面积公式。另外浮点数比较和ceil函数的使用要小心。测试时可以用(0,0)点第1年就被侵蚀和距离刚好等于某一年半径的点来验证。6. POJ 1006-1007: 周期性与排序应用6.1 POJ 1006: 生理周期与中国剩余定理这是“中国剩余定理”的经典例题。三个周期体力23天、情感28天、智力33天。给定从当年第一天开始某个日期分别距离下一个体力、情感、智力高峰的天数p, e, i以及给定的日期d。求从给定日期d之后下一次三高峰同一天出现的日期。暴力解法从d1天开始每天检查是否满足(day - p) % 23 0 (day - e) % 28 0 (day - i) % 33 0。由于周期两两互质下一个峰值出现在d 1到d 23*28*33之间直接枚举在这个范围内是可行的且计算量不大约2万次循环不会超时。中国剩余定理CRT解法更优雅高效。目标是找到最小的正整数day满足day ≡ p (mod 23) day ≡ e (mod 28) day ≡ i (mod 33) 且 day d由于23、28、33互质可以直接套用CRT公式。先找到模2833关于23的逆元模2333关于28的逆元模23*28关于33的逆元然后计算day (p * M1 * inv1 e * M2 * inv2 i * M3 * inv3) % M其中M 23*28*33M1 M/23,M2 M/28,M3 M/33inv1是M1 mod 23的逆元以此类推。 最后如果得到的day d则不断加M直到大于d。因为M是周期day k*M都是解。注意事项最终结果要求输出从当前日期到下一个三重峰的天数即day - d。同时结果可能很大但题目保证在21252天内一个完整的大周期有解。暴力法虽然简单但理解CRT解法对思维提升很有帮助。6.2 POJ 1007: DNA排序与逆序数给定一系列等长的DNA字符串仅包含A/C/G/T定义其“排序度”为字符串中逆序对的总数即位置ij但character_i character_j。要求根据排序度从小到大输出字符串。核心计算计算一个字符串的逆序数。由于字母只有4种我们可以用O(n)的算法。遍历字符串维护一个计数器数组count[4]记录A、C、G、T当前已出现的次数映射为0,1,2,3。对于当前字符c其“权重”wA0, C1, G2, T3。逆序数就是所有比c权重大的字符已经出现的次数之和。例如遇到Gw2那么逆序数需要加上count[3]即T出现的次数因为TG。int getInversionNumber(const string s) { int count[4] {0}; int inv 0; for (char ch : s) { int w; switch(ch) { case A: w 0; break; case C: w 1; break; case G: w 2; break; case T: w 3; break; } for (int k w 1; k 4; k) { inv count[k]; // 所有比当前字符大的字符的计数之和 } count[w]; } return inv; }排序输出将字符串和其逆序数组成pair放入vector然后使用std::sort自定义比较函数按逆序数升序排序。如果逆序数相同则按输入顺序输出稳定排序stable_sort或记录原始索引。踩坑记录最初我使用了O(n²)的双重循环计算逆序数在字符串长度m100数量n100时复杂度为O(nm²)10^6完全可行。但上述O(nm)的计数方法更优雅高效。排序时要注意sort是不稳定排序如果要求输入顺序作为第二关键字需要使用stable_sort或在pair中记录原始索引i并在比较函数中体现。7. POJ 1008与1009: 模拟与编码的挑战7.1 POJ 1008: Maya日历转换这道题是复杂的模拟题考察细心和耐心。有两种历法Haab历365天19个月和Tzolkin历260天20个日名和13个日编号组合。给定Haab历的日期转换为Tzolkin历。解题步骤统一时间基准题目说Haab历的起始日是0. pop 0对应Tzolkin历的1 imix 0。我们需要计算从起点到给定Haab日期的总天数totalDays。Haab历一年365天月份有名字每月20天最后一个月Uayet只有5天。输入格式为day. month year。计算totalDays year * 365 monthIndex * 20 day。注意day后面有个点需要处理。转换为Tzolkin历Tzolkin历的日编号是1到13循环日名是20个单词循环。已知起点1 imix 0对应总天数0。对于totalDays Tzolkin年份 totalDays / 260。在当年内的天数daysInYear totalDays % 260。日编号 daysInYear % 13 1因为起点编号是1。日名索引 daysInYear % 20对应到20个日名数组中的位置。实现细节月份和日名用字符串数组存储。解析Haab月份时比较字符串找到对应索引。输出格式[日编号] [日名] [Tzolkin年]。注意事项这道题本身算法不复杂但输入输出处理和细节非常繁琐。很容易在计算monthIndex时出错比如pop是第0月还是第1月题目描述pop是第0月。必须仔细对照题目表格。另一个常见错误是忘记处理Haab最后一个月只有5天但输入保证日期有效所以计算总天数时直接用monthIndex * 20是安全的因为输入的day一定在该月有效范围内。7.2 POJ 1009: 图像边缘检测这是本系列最难的一道题涉及**游程编码RLE**和复杂的逻辑处理。题目给出了一幅用RLE编码的灰度图像宽度已知要求输出其边缘检测后的图像同样用RLE编码。边缘检测规则每个像素的新值等于其与周围8个邻居像素值差的最大绝对值。难点分析数据规模图像像素总数可能高达10^9宽度可达10^5不能展开为完整像素数组。RLE编码输入是(value, length)对表示连续length个像素值为value。输出也要求是RLE编码。邻居访问对于图像中间的像素其邻居容易确定。但对于处于RLE块边界、行首尾的像素其邻居可能位于前一个或后一个RLE块甚至前一行或后一行。这要求我们必须能根据像素的全局索引快速定位其所在RLE块及其邻居的值。核心策略按“变化点”处理一个关键观察是只有那些自身值发生变化或者邻居值发生变化的像素其边缘检测结果才可能发生变化。这些像素的位置称为“变化点”。变化点包括每个RLE块的起始像素索引。每个RLE块的结束像素的下一个像素索引即下一个块的开始。对于上述每个像素其周围8个邻居像素所在的索引。因为邻居的值变了中心像素的结果也可能变。步骤读取所有RLE对计算出每个块的起始索引和结束索引存储在一个列表中。收集所有“变化点”的索引去重排序。遍历这些“变化点”对于每个点 a. 根据其全局索引计算出其行号、列号。 b. 遍历其周围8个邻居的全局索引。 c. 对于每个邻居索引通过二分查找快速定位它属于哪个RLE块从而得到邻居的像素值。 d. 计算中心像素与所有邻居差值的绝对值取最大值得到该“变化点”的新像素值。将计算出的“变化点”的新值再按索引顺序进行游程编码合并连续相同值合并输出。实现复杂度假设有M个RLE输入对那么“变化点”的数量大约是O(M)级别每个块产生常数个点。对每个变化点需要O(log M)的时间二分查找邻居所在的块总复杂度约为O(M log M)可以接受。实操心得这是典型的“思维难度大于编码难度”的题目。直接模拟会内存溢出且超时。理解“变化点”思想是关键。在实现二分查找邻居值时要特别注意边界处理如图像边缘的像素其邻居可能不存在视为差值0。最终输出的RLE格式要与输入样例严格一致。这道题极其锻炼对复杂问题的抽象、分解和实现能力。8. 通用技巧与常见问题排查在系统性地解决这9道题后我总结出一些适用于POJ乃至其他在线评测平台的通用技巧和常见问题。8.1 输入输出效率与格式C vs C I/O对于大量数据输入输出如POJ 1002使用C的scanf/printf通常比C的cin/cout更快。如果坚持用cin/cout可以在主函数开头加入ios::sync_with_stdio(false); cin.tie(nullptr);来关闭与C标准流的同步并解除cin与cout的绑定能大幅提升速度。但此后就不能混用scanf/printf了。读入整行对于包含空格的字符串输入使用getline(cin, str)。注意之前如果用cin 读过数据可能会留下换行符需要先用cin.ignore()忽略掉。格式控制POJ对输出格式要求严格。使用printf可以方便地控制宽度和精度%3d,%.2f。输出结束后有时需要换行有时不需要务必根据题目要求来。8.2 浮点数处理与精度问题比较操作不要直接用比较浮点数。应该判断两者差的绝对值是否小于一个极小值eps如1e-8。const double eps 1e-8; bool equal(double a, double b) { return fabs(a - b) eps; } bool greater(double a, double b) { return a - b eps; }转换为整数对于确定是有限位小数的计算如POJ 1004的金额可以乘以一个倍数如100转换为整数运算避免浮点误差。数学函数sqrt,pow,三角函数等都会产生误差。在条件判断中要预留容差。8.3 数组、容器与内存管理全局数组在POJ中大的数组如POJ 1002的count[10000000]应声明为全局变量或静态变量以免栈溢出。vector的预分配如果知道数据大致规模使用vector::reserve()预分配内存可以减少多次扩容的开销。memset的使用对于char,int数组的初始化memset很快。但注意memset按字节赋值对int数组赋非0或-1的值要小心。fill函数更通用。8.4 调试与测试策略设计测试用例应包括样例输入输出。边界情况最小输入、最大输入、为零、为负如果允许。特殊值如POJ 1001中的10.0,0.1,0.00001等。随机生成大量数据用暴力但正确的小规模程序对拍。输出中间变量在本地调试时将关键变量如循环计数器、计算结果、条件判断值打印出来与手算或预期对比。利用OJ的反馈WA (Wrong Answer)检查算法逻辑、边界条件、输出格式。TLE (Time Limit Exceeded)检查算法时间复杂度是否存在死循环尝试优化I/O或使用更高效的数据结构。RE (Runtime Error)最常见的是数组越界、除零、栈溢出、指针错误。检查数组大小、循环边界、除数是否可能为零。PE (Presentation Error)基本是输出格式问题多空格、少空格、换行符不对。8.5 算法选择与思维提升从暴力开始对于新手如果对更优算法没把握先实现一个思路清晰的暴力解法如POJ 1006的枚举确保正确性。然后再思考优化。空间换时间POJ 1002的桶排序就是典型例子。当数据范围不大时用数组直接寻址比用map查找更快。预处理POJ 1007中可以预处理每个字符的权重避免在循环中反复switch。POJ 1002中预处理字母到数字的映射表。模块化函数将重复使用的功能封装成函数如高精度乘法、逆序数计算、日期转换等。这使代码清晰易于调试和测试。回顾这九道题从高精度到模拟从排序到编码它们像是一套精心设计的“算法体操”训练了编程者最基本也最重要的能力将现实问题抽象为计算模型的能力、对数据边界和特殊情况的敏感度以及用清晰、稳健的代码实现想法的能力。这些能力远比AC一道题本身更重要。在刷题的路上我们经常会为了一个“Accepted”而欢欣鼓舞但真正让我们成长的是那些为了找出一个隐蔽的bug而绞尽脑汁的夜晚是那些尝试了多种方法后豁然开朗的瞬间。希望这份结合了代码与思考的总结能帮助你更扎实地走过这段入门之路。

相关新闻