简介面向西北工业大学nojC/C题库100道练习题的参考题解与代码项目适合正在刷题备考、需要算法思路对照的编程学习者。资源覆盖从基础编程到递归、动态规划、贪心等高级算法每道题均配有带详细注释的代码示例帮助理解解题逻辑同时针对素数判断、字符串处理、矩阵运算等高频考点整理了高效的实现方案与运行优化技巧。压缩包内共3个文件包括InsCode在线运行配置、HTML说明页面和Git忽略文件整体仅6KB结构清晰、便于快速查阅项目入口与说明。目前已有198人学习浏览可作为考前复习和算法入门的轻量参照。通过题解中强调的模板应用、先易后难的刷题顺序、避免复制粘贴等习惯读者能逐步沉淀自己的代码片段提升考场上的应变能力。1. 项目概述这个100题解析库到底解决什么问题事情得从去年秋天说起。西工大的 NOJNorthwestern Polytechnical University Online Judge校内在线评测系统是每个计算机相关专业学生都绕不过去的坎尤其是大一的 C 语言课程老师布置的作业基本都在 NOJ 上完成。当时刷题刷到一半我发现自己反复在同样几个坑里栽跟头——要么是输入输出格式不对要么是边界条件没处理干净要么就是明明本地跑得好好的一提交就 WAWrong Answer。与其每次都在群里问同学要代码不如自己整理一份完整的题库解析把每一道题的思路、代码、易错点全部沉淀下来。这个项目就是为此而生的西工大 NOJ 100 题解析代码库。它的核心交付物不是一篇篇长篇大论的题解博客而是一个组织清晰、可直接编译运行的代码仓库配套 README 里的思路说明和踩坑记录。换句话说这个项目解决的是“你拿到一题之后如何快速理清思路、写出能 ACAccepted的代码、并且在下一次遇到同类题时不再犯同样的错”这个问题。适合谁来参考三类人。第一类是西工大在读、正在 NOJ 上被作业折磨的本科生可以直接拿代码库当参考但我建议你先自己写卡住了再看答案否则这个库对你没有意义。第二类是其他学校用同类 OJ 系统比如 HDU、POJ、洛谷的训练者算法思路是通用的题目原型也大同小异。第三类是刚开始接触 GitHub、想学习怎么管理一个多文件代码仓库的开发者这个项目的目录结构、模块拆分、Git 提交记录本身就是一份活教材。这篇文章就是把整个项目从设计到落地、再到推上 GitHub 的过程完整拆开讲讲。2. 项目结构设计从“一堆散文件”到“一眼能看懂”的代码库2.1 按题号分目录目录本身就是索引100 道题如果全部平铺在一个文件夹里光是滚动文件列表就能把人劝退。我一开始也犯过这个毛病——001.c、002.c、003.c 一路排到 100.c看着挺整齐但真要找某道题的时候要么记不清题号要么得挨个打开文件看注释。后来我做了一次结构调整也就是网上常说的“整理项目结构”那类操作把每一道题作为一个独立目录目录名直接叫P001_TwoSum这种格式其中P001是题号TwoSum是题目的简短描述。这样做有几个立竿见影的好处按题号排序后目录顺序和 NOJ 官网的题目列表一一对应查题零成本。目录名自带语义看到P023_JosephusRing就知道是约瑟夫环问题不用打开文件确认。每个目录里除了放题解源码还能放题目描述截图、测试用例、笔记等附属文件互不干扰。目录结构大概长这样NOJ-100-Solutions/ ├── README.md ├── .gitignore ├── P001_HelloWorld/ │ ├── main.c │ └── notes.md ├── P002_AplusB/ │ ├── main.c │ └── notes.md ├── ... ├── P100_SomeHardProblem/ │ ├── main.c │ ├── data.txt │ └── notes.md └── common/ ├── fast_io.h └── utils.h这其实就是在借鉴“模块化仓库”的思路——每个题目目录相当于一个独立模块而common/目录则是这些模块共享的基础设施层。用你在做业务项目时熟悉的术语来说这就是把“框架层代码”抽出来单独维护其他题目的代码只依赖这个公共模块谁也不越界。2.2 公共代码抽出来别让每道题都重复造轮子刷题刷到后面你会发现很多题目的代码骨架是高度相似的模板化的头文件引用、快速输入输出的封装、一些常用的工具函数比如求最大公约数、快速幂、二分查找的模板。如果每道题都把这些代码从头写一遍代码库会变得极其臃肿而且每次复制粘贴还容易出错——比如某一次修改了快速 IO 的实现其他所有文件里的旧版本不会跟着更新。所以我建了一个common/目录里面放的是所有题目共享的代码片段。这个设计跟热词里提到的“把框架层代码放到私库其他模块依赖 jar 包”是同一个思想只不过在这个项目里公共层不是一个独立的 Git 仓库或 Maven 依赖而是一个本地共用的头文件目录。以fast_io.h为例C 语言的scanf/printf在应对大规模输入时其实已经够用但有些题目的输入数据量可以到几十万甚至上百万行这时候cin/cout的默认同步机制就会拖后腿。我在公共头文件里放了一个简单的快速读入模板配合fread批量读取实测在数据量大的场景下能把 IO 耗时降一个量级。但这部分在代码库里不是强制的——你要用标准输入输出也行结构上保持兼容即可。挑选公共代码的标准只有一个至少被三道题以上复用才配进common/。只用一次的工具函数就老老实实写在题目自己的目录里不要为了抽象而抽象。2.3 配套文档代码 思路 踩坑记录三位一体代码库不能只有代码。每一道题的目录下我都放了一个notes.md里面包含三部分内容核心思路用几句话讲清楚这道题的解法比如“这题本质是最长递增子序列用 O(n log n) 的贪心 二分实现”。复杂度分析时间复杂度和空间复杂度各是多少为什么是这个量级。易错点本地 AC 但 OJ 上 WA 的常见原因或者写法上容易翻车的地方。这样做的好处是当你三个月后再翻到这道题不用重新把代码从头到尾读一遍——看 notes 就能回忆起来。而且在 GitHub 上别人看到的不只是代码还有你的思考过程这比单纯放一份 AC 代码有价值得多。3. 核心题解解析从输入输出到算法套路一步一个坑走过来3.1 输入输出处理一半的错误都出在这一步我统计过自己 WA 的题目里大概有一半问题出在输入输出上而不是算法本身。这不是夸张。OJ 评测系统对格式的要求极度严格多一个空格、少一个换行、输出大小写不一致统统判 WA。刷题刷到一定量你会形成肌肉记忆但刚开始的时候真的会为“到底要不要输出行尾空格”这种问题纠结半天。先说输入尤其是循环读取。NOJ 的很多题目输入格式是“多组测试数据每组占一行读到 EOF 结束”。这种题用 C 语言写的时候循环条件得写成while (scanf(%d, n) ! EOF)而不是想当然地写while (1)。后者在本地跑不会有问题但提交到 OJ 上就没有退出条件评测系统会判断超时。用 C 的话对应的写法是while (cin n)。这两种方式都是等价的核心在于理解“输入流耗尽时返回什么”。再说输出。题目如果要求“每组输出占一行”“输出结果保留两位小数”“行末无多余空格”这些格式要求必须逐字落实。保留两位小数的写法在 C 里是printf(%.2f, ans)在 C 里需要cout fixed setprecision(2) ans。这里有一个常见的坑fixed和setprecision(2)一旦设置之后后续所有浮点输出都会受影响如果你在同一道题里既想输出整数又想输出保留两位小数的浮点数就得注意流状态的恢复否则格式就乱套了。最后说字符串输入。如果一行字符串里可能包含空格比如英文句子用scanf(%s, str)读就会出问题因为%s遇到空格就停了。这种情况在 C 里应该用gets或者fgets在 C 里用getline(cin, s)。还有个细节是如果用完cin读取整数再切到getline中间会残留一个换行符在缓冲区里导致getline直接读到一个空行——解决办法是在切换之前调用一次cin.ignore()把残留换行清掉。这就是那种“不是不会做而是卡了很久才发现的隐藏坑”。3.2 经典算法考点的三个层次100 道题做下来我把它们归纳成三个层次的考点。第一个层次是基础的模拟和枚举比如日期计算、进制转换、字符串拼接——这类题没什么高深算法考察的是你能不能把题目描述翻译成代码细心程度决定成败。第二个层次是经典的数据结构与算法模板比如排序快排的qsort在 C 里要自己写比较函数、栈和队列的模拟、链表的增删改查。这些题型非常适合“背模板”但不是死记硬背而是理解每种数据结构的适用场景。第三个层次是动态规划和贪心这类题最考验建模能力。比如 NOJ 上的经典背包问题变种、最长公共子序列、区间合并这类题的关键不是代码本身而是你怎么把问题化成状态转移方程。我在这三个层次上分别吃过不同的亏。模拟题容易在边界条件上翻车比如循环的起止下标多一少一或者日期跨月、跨年时的判断遗漏。模板题容易在“背错模板”上翻车比如把快排的比较函数写得前后矛盾导致排序结果和预期相反。动态规划题则是在初始状态的定义上容易翻车——dp[0]到底是 0 还是 1直接决定整个递推过程对不对。如果用一个词来总结这三个层次的核心经验那就是“先把样例跑通再想优化”。很多新手喜欢一上来就搞最优解结果写了一堆花里胡哨的代码样例都过不了。我的做法是先用最笨的暴力方法把样例跑通确认自己理解了题意再考虑怎么优化复杂度。这个习惯养成了刷题效率会高很多。3.3 时间复杂度的判断为什么你的程序总是 TLETLETime Limit Exceeded几乎是每个 OJ 用户的必经之路。NOJ 的时限通常是 1 秒到 2 秒而判断一个算法能不能过不是靠感觉而是靠估算。1 秒时限下普通计算机大约能执行 10^7 到 10^8 次基本操作。如果你的算法时间复杂度是 O(n^2)而 n 的上限是 10^5那最坏情况下需要 10^10 次操作必然超时。这时候就需要换 O(n log n) 或者 O(n) 的算法。这个估算方法在写代码之前就应该完成而不是等提交了才去看结果。我自己的习惯是拿到题目先看数据范围然后心里默算一下自己的方案在最大数据下需要多少操作超了就绝不硬交。这道题为什么 TLE本质不是代码写得慢而是算法在数据规模面前撑不住了。补齐这一层的认知你才算真正入门了算法竞赛。4. 模块化与复用把零散题解整理成可持续维护的工程4.1 公共模块的边界什么代码该抽离什么代码该留在原地在整理这个项目结构的过程中我最深的体会是公共代码不是越多越好边界划得清楚才是关键。比如快速输入输出的封装、排序比较函数的通用模板、常用数据结构的实现这些高度通用、跨题复用的代码它们放在common/里是合理的。但如果你发现某段代码只在一道题里用到或者它的逻辑严重依赖题目的特殊约束那它就留在自己的目录里不要硬塞进公共模块。划边界的一个实用判断标准是这段代码能否在不做任何修改的情况下被下一道新题直接引用如果能说明它足够通用可以考虑收进公共模块如果不能它就应该留在原地。这个标准和软件开发中“高内聚、低耦合”的原则是一脉相承的。只不过在刷题仓库里模块的粒度要小得多——“内聚”体现在代码只服务于一个明确功能“耦合”体现在题目代码对公共模块的依赖是单向的公共模块不会反过来知道任何一道题的存在。4.2 命名规范与代码风格写给未来的自己看刷题代码通常是写完就扔但既然要整理成项目代码库命名和风格就值得花点心思。我给自己定的规矩是变量名用有含义的英文单词比如count、length、maxValue而不是a、b、c函数名用动词短语比如calculateSum、findMax关键算法的核心逻辑一定要写注释但注释不是把代码翻译成中文而是说明“这一步在做什么、为什么要这么做”。代码风格这块C 语言我统一用 Allman 风格大括号换行C 统一用 KR 风格大括号不换行这个不一定是标准答案但一个仓库里代码风格保持一致很重要。项目里还加了一个.editorconfig文件约束缩进宽度和编码格式保证在不同编辑器里打开都不会乱。很多人觉得这是形式主义但在一个稍大的代码库里统一的风格比任何代码评审规则都能降低理解成本。4.3 依赖管理的隐喻题目目录就是“模块”公共目录就是“框架”把这个项目结构放到更宏大的软件开发语境里去对照你会发现它和现代 Java 项目的分层思路高度相似。Java 项目里框架层代码比如 Spring 的核心容器会被打成 jar 包放到私有仓库业务模块通过 Maven 或 Gradle 的依赖声明来引用它。刷题仓库里的common/目录扮演的就是“框架层”的角色而每个题目目录就是“业务模块”。这种类比不只是一个修辞它背后反映的是同一个工程原则稳定、通用、变化少的部分集中管理易变、特定、业务相关的部分独立迭代。说得更直白一点如果你的项目里有 30 道题都用到了同一个二分查找的模板而某天你发现这个模板的边界条件写错了需要修正——如果你把模板放在公共目录里只需改一处如果你把它复制到了 30 个题目的文件里你要改 30 处而且大概率会漏改两三处。模块化的收益不是立刻显现的而是在你维护一个仓库三个月、半年之后才真正体现出来。5. Git 实战已存在的项目如何一步步推到远程仓库5.1 本地初始化从git init到第一次提交这个项目最开始是本地文件夹里一堆零散的.c文件没有版本管理。写了几十道题之后我想把它整理成仓库并推上 GitHub方便随时查阅也方便分享给学弟学妹。操作路径其实非常标准但很多第一次接触 Git 的人恰恰容易在最基础的流程上卡住。第一步进入项目根目录执行git init初始化本地仓库。第二步编辑.gitignore文件把不需要纳入版本管理的文件排除掉——比如编译生成的.exe、.out、IDE 配置文件、临时数据文件等。这里有一个很容易踩的坑如果你在git init之后、第一次git add之前没有先配好.gitignore编译生成的二进制文件就会被一起加入版本库仓库体积迅速膨胀而且在后续的每次提交中都会反复更新非常烦人。第三步按逻辑分批git add不要一把梭git add .。我的习惯是先加common/公共模块分一次提交写清楚“初始化公共工具模块”然后按题目批次逐个添加比如一次加 10 道题提交信息写成“新增 P001-P010 题解”。这样提交历史干净清晰将来哪道题写错了可以精准地回退到对应提交。提交信息的写法也有讲究。简短一句话动词开头说明做了什么——Add solution for P023 Josephus ring而不是update、update code这类无关痛痒的记录。我在整理这个仓库的过程中越来越意识到提交信息是写给未来的自己和其他协作者看的写清楚“为什么改”比“改了什么”更有价值。所以我后来提交时会尽量在信息里补一句背景比如Optimize P045 to O(n) solution, fix TLE under large input既有操作结果也有操作意图。5.2 关联远程仓库已存在的项目如何git push本地仓库建好之后去 GitHub或者 GitLab、Gitee 等任意托管平台创建一个空仓库。这里要特别注意创建的时候不要勾选“添加 README 文件”或“添加 .gitignore”因为你的本地仓库已经有这些文件了如果远端也有两边会产生完全无关的提交历史推的时候就会遇到麻烦。创建好之后在本地执行git remote add origin gitgithub.com:yourusername/NOJ-100-Solutions.git git branch -M main git push -u origin main第一条命令是把远程仓库地址关联到本地origin是远程仓库的默认别名相当于给那串长长的 URL 起了一个短名字。第二条命令把默认分支名设为main这是当前 GitHub 的默认分支名——如果你本地初始化的分支名是master老版本 Git 的默认值这条命令就很重要可以避免分支名不一致的问题。第三条命令把本地的main分支推送到远程-u参数的作用是建立本地分支和远程分支的关联关系以后直接敲git push就能推不用再指定远端和分支名。如果推送的时候遇到报错信息中有non-fast-forward字样说明远程仓库有本地没有的提交记录最常见的情况就是刚才说的在 GitHub 上创建仓库时勾选“添加 README”导致两边历史不一致。解决办法是执行git pull --rebase origin main把远端的历史先合并到本地再推送。--rebase的作用是把你本地的提交“挪”到远端历史之后保持线性历史不会产生一个多余的 merge commit。如果远端只有一条 README 初始化提交你的本地代码不会被覆盖放心用这条命令。5.3 分支管理的轻量用法刷题仓库不需要重但需要稳刷题仓库不是大型多人在线协作项目分支不需要用到复杂的功能分支模型。我用一条main分支作为主历史所有题解提交都直接推到main。但如果你有同学想一起维护这个仓库就不用每个人都去main上堆提交了——让对方先fork仓库在自己仓库里改完再发 Pull Request你可以检查一下代码有没有明显问题再合入。这种方式在 GitHub 上是最标准也最安全的多人在线协作路径。还有一个在我实际使用中经常被忽略的操作git tag。每完成一个阶段性目标比如做完 20 题、50 题我都打一个 tag比如v0.2、v0.5。这样将来想对比某个阶段的完整代码直接git checkout v0.5就能回到那个时间点比翻提交记录要高效得多。轻量标签和附加说明的标签区别不大关键是养成打标签的习惯。6. 常见问题与排查技巧实录6.1 本地 AC 但 OJ 上 WA三个最常见的隐藏原因这是刷题过程中最让人抓狂的场景代码在本地测试全都正常样例输入输出完全一致提交到 NOJ 却是 WA 或 RE。我结合自己踩过的坑整理了三个高频原因。第一个是未初始化的局部变量。C 语言中局部变量默认值是“不确定的”垃圾值如果你声明了一个变量但没有初始化就直接使用在本地某些编译器上可能恰好是 0跑出正确结果但 OJ 上的编译器环境不同垃圾值变成了别的数结果自然不对。排查方法是编译时开启告警GCC 加-Wall -Wextra参数所有“used uninitialized”的告警都过一遍。第二个是数组越界。越界读写在本地可能不会立刻崩溃因为内存布局恰好给了你一个“看似正确”的值但到了 OJ 上内存布局变了问题就暴露出来了。排查方式是检查所有数组下标是否可能在边界处溢出一位尤其是循环里i n还是i n这种差一错误。第三个是输入输出的类型不匹配。比如题目给的数值范围可能超出int的 2^31-1 上限需要用long long输出却用了%d导致大数被截断。这个问题在本地测试时如果用的都是小整数根本发现不了。解决方案是做题前先看数据范围描述超过 10^9 一律用long long甚至unsigned long long。我把这些问题整理成了一张速查表放到项目根目录的FAQ.md里现象可能原因排查手段本地 ACOJ WA局部变量未初始化编译时开启-Wall -Wextra本地 ACOJ RE数组越界检查循环边界、下标运算大数输出错误int 溢出演变为截断改成long long输出用%lld多组输入死循环循环条件没有判断 EOF改为while (scanf() ! EOF)浮点数输出格式错误未控制小数位或流状态残留fixedsetprecision注意恢复状态git push 报 non-fast-forward远端有本地没有的提交git pull --rebase origin main后再推6.2 编译环境的忠告C 语言等级考试与 OJ 的差异还有一点值得单独拿出来讲NOJ 上有些题目允许的编译器标准比较老比如 C89 或 C99。这意味着你在代码里使用 C11 的新特性比如匿名结构体、某些stdint.h的特定实现可能无法通过编译。这种现象在本地环境不会出现因为你的 GCC 版本很新默认开的是高版本标准。解决方案是在项目里写清楚每道题适用的编译器标准以及统一使用标准 C 语法不依赖特定编译器的扩展特性。C 做题时也类似优先使用标准库提供的容器和算法而不是手写一些平台相关的底层代码。如果你用的是 VS Code MinGW 这类本地开发环境提交前的最后一步建议是使用学校 OJ 系统同款编译器如果能够查到先编译一遍或者至少用gcc -stdc99 -Wall -Wextra -O2这样的参数做一次严格编译这个动作能提前拦截掉大量提交了才会暴露的问题。这是我在整理这个仓库的过程中得到的最实用的一条经验。7. 踩坑和迭代从 v0.1 到 v1.0 的复盘7.1 代码仓库结构和版本迭代的取舍这个仓库一开始的时候其实没想那么多就是随手把代码按题号扔进去。发展到 v0.3 的时候我开始意识到目录规划有问题题目文件多了之后为了找一道题的注释或测试数据得同时打开好几个窗口去翻。于是我做了一轮大规模重构把每个题目单独建目录整理公共代码重写 README。重构之后整个仓库的定位就从“一批答案源码”变成了“一套可学习的题解样例集”后面维护起来顺了很多。v1.0 是在 100 题全部完成之后打的标签。这个版本里每一道题都有对应的源码、notes 文档和必要的测试数据公共模块稳定运行几乎没有为适配某一题而做的“特供”改动。回看这一路我最大的体会是项目代码结构这件事越早规划越好但任何时间开始规范化都不嫌晚。7.2 后续还能怎么扩展这个仓库后续的方向可以有很多。比如按题型给代码打标签从模拟、排序、贪心、动态规划、搜索这些类别做交叉索引比如在 README 里加一个“题解路线图”标注每类题目的推荐学习顺序比如加上 Java 或 Python 语言的实现让使用不同语言的学弟学妹都能参考甚至可以考虑写一个简单的脚本自动从 NOJ 网站抓取题目的通过率和难度评级做成数据可视化帮后来的人快速定位哪些题目值得优先练习。不过这些扩展里我最推荐的做法是先把代码库本身的可维护性磨扎实——公共模块模块边界清晰、每道题有测试用例、提交历史干净。这些基础工作做好了后续任何扩展都是水到渠成的事反之如果基础结构一团乱麻加再多功能也只是让仓库变得更难用。本文还有配套的精品资源点击获取