多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

算法竞赛入门经典第二版例题答案:从UVA题解到自建模板库的完整拆解

算法竞赛入门经典第二版例题答案:从UVA题解到自建模板库的完整拆解 简介《算法竞赛入门经典第二版》例题答案资源面向ACM竞赛初学者与算法爱好者用于对照书中例题代码、理解经典算法实现。压缩包共224个文件以191个cpp源代码为主配合12个in测试输入、11个md说明文档和10个ans答案文件其中cpp为完整实现in对应输入样例md记录解题思路ans给出参考输出整体仅208KB轻量便携。目前已有2000人学习浏览适合备战ACM或系统提升编程能力。内容覆盖排序搜索、动态规划、图论、字符串处理、基础数据结构、贪心与分治等常见算法专题。通过例题答案与测试数据的组合读者可对照运行、逐行调试直观验证算法正确性部分ans文件还提供标准输出便于自查与复盘能有效巩固教材中的竞赛技巧适合反复研读。1. 例题答案这道坎为什么买回《算法竞赛入门经典第二版》之后真正的挑战才刚开始一个很常见的画面书到了翻到第一章例题 1-1 的代码能看懂抄上去也能跑。翻到第二章DFS、BFS 开始冒头书上只给了思路和输入输出样例没有完整源程序——你盯着题目发呆想去网上搜“算法竞赛入门经典第二版例题答案”搜出来的东西要么是零散的贴吧楼层要么是只有代码没有讲解的博客。最难受的是同一道题有人用 C 交出 AC有人用 C 交就 CE你分不清哪个能用哪个是前人放进回收站的东西。这篇文章写给三类人刚入坑算法竞赛、刷紫书刷到一半卡住的自学者想给校队新人整理一份可复用题解资源的老队员以及那些不想单纯抄代码、想把例题答案内化成自己的算法模板的长期主义者。这篇不替你把书里的题都解完而是把“整理一份能跑、能讲、能举一反三的例题答案”这条路跑通怎么组织怎么写怎么验往哪个方向深化。先说明一点例题答案的价值并不在那份代码本身而在“你为什么写这一行、下一题怎么迁移它”。2. 先看懂这本书的例题编排题号、章节与网上答案为什么不靠谱2.1 第二版的例题结构章节推进顺序不从“简单”到“难”而是从“能跑”到“能想”《算法竞赛入门经典第二版》的例题编排是跟随章节主题走的不是按 UVA 原始难度排的。语言篇教的是“怎么写程序”基础篇教的是“怎么想算法”竞赛篇教的是“怎么成套地解决问题”。这意味着同一个 UVA 题号可能在书里对应的是某个专题的引例换个专题再看它又变成另一类算法的变体。整理例题答案时如果只按“第几章第几题”存你会失去跨章节的对比能力但如果完全按 UVA 题号归档又丢掉书的递进关系。我一般会这样建目录第一层用chapter-01到chapter-12对应书的章第二层用uva-题号建子目录每个题目目录里放三个文件solution.cpp是对应可提交的源程序notes.md记录思路和踩坑input.txt放书里给出的样例输入。听起来很简单但这一套结构能救你两次命第一次是期末回头看时你知道某道题当时是怎么个来龙去脉第二次是当校队新人来问你“老师这题怎么过的”你不用翻聊天记录打开目录就能把这个资源转给他。目录命名不要用“第1章”“第2章”这种中文序数用chapter-01这种带补零的格式排序才稳定。题号不要写100写成uva-100因为 UVA 题号存在前缀冲突比如10050、1001之类目录里一长串数字会看花眼。notes.md一开始不需要写长三行就够题目在问什么、算法切入点、提交前最担心哪一处。这三行坚持写后面价值会越来越大。2.2 网上流传的例题答案为什么不能直接抄搜“算法竞赛入门经典第二版例题答案”时你会看到几类结果。先说第一类只有代码没有讲解的题解博客。这类代码往往能 AC但它为什么这样写博主默认读者已经懂了等你复制到本地跑通样例换一道题还是无法迁移。问题最大的是第二类代码本身有瑕疵却因为 OJ 数据弱而侥幸过了的“玄学过题”。典型如某些老题用int就能通过但换一组更极限的数据就溢出网上答案里没人提这件事。还有第三类把书里的伪代码仓促改成 C编译都不过还挂在博客里当一个“答案”。不能直接用最本质的原因是对齐问题网上代码的输入输出风格未必匹配你本机的验证方式。UVA 很多题是多组输入、EOF 结束、输出行尾不带多余空格有些题看似相同实际却对行尾格式严格判 PEPresentation Error。你没有答案作者的运行环境也没拿到他的测试数据唯一可靠的终点是 OJ 的 Accepted。所以别人的答案只能当作一份候选实现而不是交付标准。收下它之前至少要自己过一遍编译、与样例比对、再在 OJ 上交一次。2.3 第一步动手搭一份自己的例题答案笔记骨架不必等整本书看完再动手先在本地建好目录和笔记模板边刷边填。以chapter-01为例常见的做法是mkdir -p algorithm-notes/chapter-01/uva-100 cd algorithm-notes/chapter-01/uva-100 touch solution.cpp notes.md input.txtsolution.cpp是你最终要通过 OJ 的源程序不是你的草稿。草稿另开文件或写在纸上notes.md放思路总结与坑点input.txt放样例。这样做的理由是让“验证”这件事有一个固定的工作流写完solution.cpp立刻用input.txt做一次样例比对而不是一会儿命令行手敲一会儿从博客复制数据。一个稳定的工作流比熬夜多看十篇题解更能提升刷题效率。notes.md的初始模板可以长这样# uva-100 The 3n1 problem ## 思路 一两句话 ## 复杂度 空间/时间 ## 易错点 - - ## 提交记录 - 日期/结果/language记录提交语言这一点常被忽略同一个题用 C11 和 C17 提交min/max等标准库行为有差异记录在哪一次、用什么版本拿到了 AC未来在做模板迁移时能给“重测”提供线索让你少走弯路。3. 用 UVA 题号定位题目再拿一个真正的例题把它跑通3.1 拿到题目先做两件事确认输入格式确认输出格式题目来源是 UVA Online Judge或校内搭建的 UVA 镜像 / vjudge 上的远程题目。你可以直接在 UVA 的题号输入框里输入题号进入题面。题面是英文的但对入门阶段而言最需要抓住的只有三块Input 格式几组输入、是否 EOF 结束、Output 格式每行一个结果、有空行还是无空行、还有 Sample 后面给出的完整输入输出。这里要特别提醒Sample 不是全部测试数据它只是验证你理解没有跑偏的锚点。UVA 的样例输入有不少是故意不覆盖边界条件的它只负责让你觉得“我好像做出来了”。真正的坑要从题目描述里的隐含条件去挖。比如题目里说 “The integers i and j must appear in the output in the same order as they were in the input”这就在提醒你输入的两个数未必是前小后大输出却必须保持原顺序如果没有这句话很多人的做法是直接把参数交换后再输出。3.2 一道例题的完整答案应该长什么样UVA 100 的拿法UVA 100The 3n1 problem几乎每个刷紫书的人都会碰见它足够简单同时又埋着三个典型的“算法竞赛人才知道”的陷阱多组输入、i与j大小不保序、中间结果可能超过int范围。这里给出一份可以直接组织的答案骨架#include iostream // 计算 n 到 1 的循环长度n 可能超过 int使用 long long unsigned int cycle_length(long long n) { unsigned int len 1; while (n ! 1) { if (n % 2 1) { n 3 * n 1; } else { n n / 2; } len; } return len; } int main() { int i, j; // UVA 典型的 EOF 多组输入写法读不到数据时 while 退出 while (std::cin i j) { int left i, right j; // 题目保证输出顺序与输入一致需要保存原始 i, j if (left right) { int tmp left; left right; right tmp; } unsigned int max_len 0; for (int k left; k right; k) { unsigned int len cycle_length(k); if (len max_len) { max_len len; } } std::cout i j max_len std::endl; } return 0; }逻辑说明while (std::cin i j)是 UVA 题目最常见的输入框架。它每次尝试读入一对整数文件到末尾或输入流到达 EOF 时返回 false从而自动结束不需要人为数“共几组数据”。这在本地调试时有一个坑从终端手动输入时你需要自己生成 EOF 信号Linux 下按CtrlD否则程序会一直等着你继续输入。参数说明cycle_length的形参类型用long long这是 UVA 100 最容易错的细节。n1时如果不进入while循环长度初始化为 1代表从 1 到 1 的序列长度就是 1而n为奇数时执行3*n1这个中间结果在部分输入下可以超过 32 位int的最大值再用int就会溢出成负数陷入死循环或结果错误。默认情况下std::cin读入i, j为int是安全的因为题目给定输入范围通常在int内但计算过程必须交给更宽的类型。另外注意输出时使用的是原始i, j而不是交换后的 left/right这一点来自题目里的“same order”要求。实际上UVA 100 还有一种更快的做法是用数组做记忆化缓存把算过的n的循环长度存起来之后再遇到同一个n直接复用。这样做在范围大时能获得肉眼可见的提速不过它引出了“数组该开多大”的问题。常见的做法是把缓存长度数组开到一百万只对较小规模的n做记忆化但这一步容易在遇到超长链时出现数组越界因此入门阶段先以这份朴素实现交付跑通后再优化安全第一。3.3 提交前最后的验证三关缺一不可第一关叫样例验证。把题目给的 Sample Input 保存到input.txt然后执行./solution input.txt观察输出是否与 Sample Output 逐字符一致。这里特别注意行尾空格UVA 有些题目对多出来的空格会判 WA 而不是 PE肉眼比对很难发现建议用命令比对./solution input.txt my_output.txt diff my_output.txt expected_output.txt如果diff没有任何输出说明样例这关通过。diff在本地用得多但它对比的是“逐字符同行”空格的差异会被悄悄放过去吗不会diff会明确指出差异位置这比人眼可靠得多。第二关是边界验证。什么样的输入算边界范围最小的一组数、范围最大的一组数、输入中i j的情况。比如 UVA 100你至少要试1 1、999999 999999以及2 1确认输出依然与输入顺序一致。第三关才是真正提交到 OJ。你在 vjudge 或 UVA 官方页面交一次看到 Accepted 之后把语言版本和提交日期写进notes.md。从此这份答案才算是“能交付的答案”而不是“看着像答案的代码”。4. 例题答案不是用来背的三遍法、对比法与专题驱动4.1 第一遍自己憋第二遍对照第三遍隔一周重写直接打开别人的例题答案能看懂但合上之后写不出来——这是最常见的“假学习”。正确的做法是把一道题的答案当作三个独立资源思路、实现、坑点。第一次看题时不看答案先自己试写卡住半小时再翻notes.md里的思路提示不看代码。第二遍对着 AC 代码核对自己实现与标准写法的差异这时候特别注意输入读取方式、循环边界、输出格式这些地方最体现一个人的竞赛习惯。第三遍是隔一周之后不看任何资料把这道题重写一遍重写完再提交一次。这个流程听起来慢但它省的是未来回头补课的代价。你可能觉得一周后重写同一道题没有新意可这一遍最能暴露“当时到底是会了还是背下来了”。如果重写时能顺带用另一种实现比如把while改写为for或给 UVA 100 加上记忆化收获会更大。重写的提交如果 WA你正好又多了一条比对着找差异的训练机会。这个“三遍法”用在紫书的例题答案上比把书页翻烂更接近“学会”。4.2 用专题驱动代替章节顺序把散落的例题按算法家族归堆很多人刷《算法竞赛入门经典第二版》的方式是线性推进第三章做完做第四章做完进入第五章。这种方式在语言篇阶段问题不大但到图论、动态规划这几章时书里是先点概念、再给例题你按章节顺序刷很容易陷入“每道题都会综合题不会”的尴尬。更有效的做法是专题驱动把例题答案按算法分类重新过一遍。比如你在第二遍刷题时可以一口气刷“贪心专题”把书里涉及贪心的经典例题全部拉出来记录每一题怎么构造选择策略、为什么当前选择能保证最优。再开“搜索专题”把 DFS、BFS、回溯的例题放在同一周肝完。专题驱动的本质是你主动建构知识网络而不是顺着作者的章节顺序被动接收。按算法归类整理后你需要做一次索引在每个solution.cpp文件的注释头部写一行“算法标签”比如// 算法贪心/区间重叠这样未来做专题复习时直接用 grep 就能把一类题捞出来。4.3 把例题答案变成自己的模板库注释不只是给人看的也是给未来的自己看的当例题刷满几十道之后你会发现代码里开始反复出现一些固定骨架while (std::cin T)处理多组数据、用std::vector预开空间、对结构体排序时写operator……这些骨架就是未来的模板。我建议在solution.cpp之外单独维护一个templates/目录按类型拆成io.cpp、sort-helper.cpp、graph-traversal.cpp等文件。每做一道新题先看能不能把旧模板复用进去如果旧模板需要改改完顺手把改动同步回模板文件。这个习惯的转折点是你开始“信任”自己的模板。模板信任有两层第一层是确定性你知道它一定能编译过不会出现“玄学错误”第二层是解释力你能对别人讲清楚每一行是干嘛的为什么这么写。对例题答案而言最高级的复用不是“这题做过我抄这题的代码”而是“这题虽然没做过但它和那题的骨架完全一样”。到这一步你手里那份“算法竞赛入门经典第二版例题答案”实际上已经不只是答案了而是一套属于你自己的算法脚手架。5. 提交时最容易翻车的五个细节现象、原因与处理5.1 答案是对的交上去却 WA 或 PE输出格式里的空格现象本地diff通过样例完全匹配提交 UVA 后返回 WA 或 Presentation Error。原因往往是行尾多了一个空格或者本应每行之间有空行你却只在最后多打了一个空行。UVA 对输出格式的检验比本地终端严格有些题目标输出“每行结果之间空一行”你没注意首行则不能先空行就会反复 PE。解决提交前检查输出逻辑而不是靠最后人工目测。推荐让程序只负责算答案然后用统一函数输出不要用cout ans endl endl这种手写空行方式写一个print_case之类的辅助函数在函数内统一管理在哪个位置加换行。这样所有的输出路径只有一个格式错误只会集中在函数头部调试一次就能解决。5.2 多组输入用while(cin a b)本地跑一次就退出现象在终端里手动输入一组数据后程序立刻结束你以为写错了或者交到 OJ 上只处理了第一组数据就退出。原因是你用if (cin a b)或者没用循环输入流碰到 EOF 时cin为 false正确的写法应该是while。解决记一个死规矩——UVA 题面里如果写着 “Input contains multiple test cases”输入框架一律用while (std::cin ...)。本地测试时记得生成 EOF 信号或直接使用重定向 input.txt。如果题目输入格式是“第一行给一个 T后接 T 组数据”那才改用cin T的固定次数循环两者不要混淆。5.3 样例过了交上去全军覆没整型溢出算法竞赛最经典的暗坑现象UVA 100 的样例输入1 10输出1 10 20一切正常但输入较大的边界值如999999 1000000时程序长时间不动或输出负数。原因n 3*n1在中间过程可能短时间突破 32 位有符号整形的上限你用int存一溢出就变成负值而负数永远无法通过n ! 1的判断程序陷入死循环。解决涉及乘法、递推、累加时先思考“中间结果会不会超过 int”。入门阶段最省心的策略是凡边界可能超过百万的数值计算形参一律用long long。不要迷信“我看别的题解用 int 也过了”那可能只是 OJ 当时的数据没有逼出边界算法竞赛里类型宽度不一定是“别人过了就等于正确”。5.4 网上答案里混着不能提交的伪代码抄过来编译都过不了现象从题解博客复制一段代码本地g直接报错错误信息指向using namespace std;或数组下标这类问题。原因不少题解是把书里的伪代码原样粘贴或者在代码里用了非标准的头文件与扩展语法还有人会把printf和cout混用产生未刷新缓冲区的怪问题。解决明确自己的编译环境与标准。本地可以用g -stdc17 -O2 solution.cpp -o solution编译以 C17 标准作为底线。把solution.cpp从网上拿来之前先在本地编译一次绝不能没编译就提交。遇到编译错误先看第一行错误提示多数情况下是缺头文件或数组越界不要直接怀疑 OJ。5.5 输出顺序与题目要求不一致改了内部变量忘了改动输出现象输入1 2程序输出2 1 20而题目要求输出按输入顺序1 2 20。原因为了计算区间你把i和j交换成left和right但是输出时直接用了交换后的变量。解决在代码里把原始输入值单独保存如const int orig_i i; const int orig_j j;。最后输出时只使用orig_i和orig_j。这看起来笨但它是处理“输入顺序必须保留”类题目的一个普适做法比“交换回去”少一层出错可能。很多题目不会明说这一点只会在描述里用 “same order” 提醒养成保留原值的习惯后这类错误基本不再出现。6. 从例题答案到自己的板子一张卡片的三个要素和每周维护节奏整理例题答案的终局是把它变成一张张能支撑新题的可复用卡片。每张卡片不需要写长篇只需要三块触发场景、代码骨架、常见错法。触发场景回答“看到什么特征会调用这段”比如“多组 EOF 输入且要算区间最值”代码骨架就是可编译的最小实现常见错法是这一题上一次提交失败的原因。把这块内容写在notes.md末尾或者新建card-001.cpp这类文件都可以。我自己的习惯是每周五下午做一次“卡片审计”把本周做过的例题过一遍凡是两道题都出现过同一段骨架的就把骨架提升进templates/凡是某道题踩过的坑在另一道题再次出现的就在卡片“常见错法”栏加一行。这样一来题库里的答案不是越堆越厚而是不断沉淀成少而实用的基础构件。顺带一提我在这件事上吃过亏刚刷题的那阵子我见到例题就想存代码三个月后回头翻solution.cpp还在但当时为什么要交换i和j、为什么用long long全忘干净了等于收藏了一堆无法理解的符号。后来我把“每提交一个 AC 都必须写三行 note”当成纪律才避免继续积累那种无法消化的答案。如果你只是照抄网上的代码而没有留下自己思考的痕迹三个月后你会得到同样的结果。希望你从现在开始就给每一道例题答案留三行注释清清爽爽地推进更重要的是把“答案”当成过程而不是目的——每一道题最终都要长成你自己能讲解、能改写的代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表