把真题变成知识点
把知识点用到新题上
明晚与后天早上的冲刺路线,加上可随时查阅的完整讲义。每段程序说明关键逻辑、复杂度、边界与变式,遇到原资料错误明确纠正。
CSP-S 第一轮:考前两时段冲刺
现在最有价值的事:先稳住会做的题,再补最容易迁移的方法。 本资料按“明晚约3小时+后天早上约2.5小时”设计,可根据你的实际时间压缩。这不是押题,所有优先级都用于安排有限时间。若基础较弱,P0全部看完就比匆忙背高级模板更有效;若P0已熟,再选P1;P2最后看。
明晚:基础得分+程序阅读(建议180分钟)
| 时段 | 做什么 | 达标标准 |
|---|---|---|
| 0—15分钟 | 阅读本篇速查卡、纠错重点;用自测题定位弱点速查卡 临考易错点 开始自测 | 不把有问题的旧答案当标准 |
| 15—45分钟 | 位运算、进制、类型、复杂度;看C01及基础章F02 进制、位权、容量单位与字节序 F03 C++ 类型、运算符、短路和整数边界 F05 复杂度 C01 固定字长异或变换与整数提升 | 能手算两行位运算、解释溢出发生在哪一步 |
| 45—75分钟 | 排序与二分:C03、基数排序、快速选择F08 排序 F35 二分查找与答案的单调性 C03 第 k 小数对距离 O02 LSD 基数排序 O12 随机快速选择 | 能写出区间含义、判断函数与边界更新 |
| 75—85分钟 | 休息 | 离开屏幕、活动 |
| 85—120分钟 | 组合计数、树、图论选择题F09 加法、乘法、排列、组合与去重 F10 相邻限制 F11 圆排列、图中的环与棋盘计数 F12 容斥原理与整除计数 F22 二叉树、完全树、满树与高度 F23 遍历、二叉搜索树与重建 F33 树的最近公共祖先 LCA 与重心 F28 图的基本量、存储与极值 F29 二分图、染色、奇环与边数 F30 欧拉通路、欧拉回路与连通性 F31 强连通、DAG、拓扑排序与删环 F32 最短路、最小生成树与最小割 | 每题先识别模型,公式有条件,不盲套 |
| 120—155分钟 | 选两段阅读程序限时手算:S4阅读1+S6阅读2C01 固定字长异或变换与整数提升 O02 LSD 基数排序 | 每段≤15分钟,过程写变量表 |
| 155—180分钟 | 核对并写“我最容易犯的5个错”自测答案 错题记录模板 | 错因具体到条件、类型、下标,而非“粗心” |
后天早上:完善程序+错题回收(建议150分钟)
| 时段 | 做什么 | 达标标准 |
|---|---|---|
| 0—15分钟 | 合上资料复述昨晚5个错与速查公式速查卡 错题记录模板 | 说出适用前提 |
| 15—45分钟 | 最短路、分层最短路、BFSF32 最短路、最小生成树与最小割 PR04 分层状态最短路 O05 容器分水 | 能解释为什么要多一维状态,能写松弛式 |
| 45—75分钟 | 二分答案+第k小;C03与两数组和第k小F35 二分查找与答案的单调性 C03 第 k 小数对距离 PR09 两个有序数组的第 k 小和 | 先写count含义,再决定 <、<= 与k的位置 |
| 75—85分钟 | 休息 | 不在脑疲劳时硬啃长代码 |
| 85—115分钟 | 做两段完善程序:S3完善1+熟悉的一段C04 DAG 路径计数与字典序第 k 小 选择另一段完善程序 | 每空能说出变量含义与不变量 |
| 115—140分钟 | 重做仍错的题;有余力看C05、背包、字符串开始自测 C05 所有子数组最大值之和 F34 动态规划 O14 分数背包 F26 Trie 与 KMP 前缀函数 O01 Sunday 字符串匹配 | 不再追求全新难点数量 |
| 140—150分钟 | 看考试策略,整理文具及组织方要求的证件考场配速与检查 F37 读题限定词与构造反例 | 结束学习,留下缓冲 |
只有90分钟时。 速查卡15分钟→位运算与复杂度20分钟→二分模板20分钟→Dijkstra/BFS20分钟→自己的错题15分钟。暂放球体交体积公式细节、折半状态优化、复杂双向搜索、一般最大流实现和长篇证明。
一页心智速查:进考场前能复述
| 模块 | 必须会的结论 | 条件/常见坑 |
|---|---|---|
| 类型F03 C++ 类型、运算符、短路和整数边界 | 1LL*a*b 才先扩成64位;(long long)(a*b)可能太晚 |
有符号溢出不是可靠取模 |
| 除余F13 模运算、最大公约数、逆元与费马小定理 O03 负进制 | 整数除法截向0;负余数先用 (x%k+k)%k 归一化 |
k须正;极端大值考虑加法溢出 |
| 位运算F03 C++ 类型、运算符、短路和整数边界 C01 固定字长异或变换与整数提升 | x&(x-1)删最低位1;x&-x取最低位1 |
位宽、符号、移位次数必须合法 |
| 逻辑F03 C++ 类型、运算符、短路和整数边界 | &&、||短路;&、|逐位计算 |
x&1==0应写成(x&1)==0 |
| 复杂度F05 复杂度 F06 分治递推、重复计算与快速幂 | 顺序相加取主项;独立嵌套相乘;双指针看总移动 | 见while不直接判O(n²) |
| 二分F35 二分查找与答案的单调性 C03 第 k 小数对距离 | 最小可行值:if(check(mid)) r=mid; else l=mid+1; |
配while(l<r);上界必须可行 |
| 排列组合F09 加法、乘法、排列、组合与去重 F10 相邻限制 | 排列A(n,k),组合C(n,k);不相邻先排其余再插空 | 相同物品别再乘阶乘 |
| 容斥F12 容斥原理与整除计数 | 单集合相加−两两交集+三重交集 | 整除交集用lcm,未必是乘积 |
| 树F22 二叉树、完全树、满树与高度 | n点树n−1边;根为第1层,高h最多2^h−1点 | 二叉树、满二叉树、完全二叉树分清 |
| 遍历F23 遍历、二叉搜索树与重建 | 前序根左右、中序左根右、后序左右根 | 仅前序+后序一般不能唯一重建 |
| 图F28 图的基本量、存储与极值 F31 强连通、DAG、拓扑排序与删环 | 无向度和2m;DAG存在拓扑序 | 有拓扑序不表示拓扑序唯一 |
| BFSF20 栈、队列、后缀表达式与 BFS/DFS O05 容器分水 | 无权/等权边最少步数;入队时标记 | 有不同非负权用Dijkstra |
| DijkstraF32 最短路、最小生成树与最小割 PR04 分层状态最短路 | 非负边;取最小距离;松弛d[v]>d[u]+w |
小根堆;跳过旧记录;负边不适用 |
| MSTF32 最短路、最小生成树与最小割 | 连通所有点且总边权最小;n−1条边 | 与单源最短路树不是同一目标 |
| DPF34 动态规划 | 状态→转移→初值→顺序→答案 | 0/1背包容量倒序,完全背包正序 |
| 计数F09 加法、乘法、排列、组合与去重 F11 圆排列、图中的环与棋盘计数 | 先问“不同”按值、下标、方向还是起点区分 | 无向环除旋转和反向;重复值仍可能算多次 |
| 字符串F26 Trie 与 KMP 前缀函数 O01 Sunday 字符串匹配 | find查连续子串;子序列允许跳过 |
next数组先读题目的定义,不背单一版本 |
| LinuxF01 Linux 命令、编译与运行计时 | pwd路径,cd切换,ls列出,mkdir新目录 | 不运行危险命令来练习记忆 |
阅读程序的六步方法
- 圈出输入范围和数据类型:是否允许0、负数、相等元素、空集合?容量是否包含最后一个下标?
- 找输出式,然后给关键变量写中文含义。不要第一遍就逐行硬算所有代码。
- 写入口不变量。例如二分“答案在[l,r]”,DFS“前k−1位已填好”,DP“本轮只用上轮状态”。
- 用最小样例模拟:1个元素、全相等、递增、递减、答案在边界;手算时记录更新前后。
- 改代码题先找反例。一个合法反例就能推翻“一定不变”,几十个正常样例不能证明永远不变。
- 最后分析复杂度:统计每层实际工作、重复状态和指针总位移;空间包括数组与递归栈。
完善程序的五个检查点
先定量纲。 空里应是下标、数量、权值、布尔条件还是一整个状态?很多选项无需跑代码就能排除。再定方向。 左右转移是否守恒,入度先减还是后减,排名从0还是1开始。再定边界。 [l,r]长度r−l+1,[l,r)长度r−l;指针访问前应先判范围。再用小例。 第1小、最后1小、只有一条边、无法到达。最后回代。 连续两个空要联合检查,不能逐空各选一个“看起来对”的表达式。
120分钟训练配速
参考安排:单选25分钟、阅读45分钟、完善35分钟、检查与答题卡15分钟。具体题目难度不同可以调整,不要求按题号死磕。单选卡住2分钟先标记;读代码先拿能直接模拟的小问;完善程序能凭局部状态确定的空先做。最后优先查:题号错位、判断题涂法、问正确还是不正确、复杂度漏排序、答案是次数还是数值。以考场规则为准,不假设能使用电脑运行代码。
最值得避免的十种临考误记
- “资料写了答案就可信”:附件确有代码残缺和错解,主讲义纠错优先。
- “某算法的结论等于给定代码一定做到”:漏基例、越界会改变行为。
- “只有输出变量需要long long”:中间乘法先溢出仍会错。
- “排序后相等元素没有区别”:计数和稳定性题必须保留原下标含义。
- “记忆化DFS能处理任意有环最短路”:依赖尚未定值时不成立。
- “见二分就把右边界改mid−1”:边界模板必须整套一致。
- “所有偶度图都是欧拉图”:必须满足相应连通条件。
- “贪心排一个看着合理的顺序就行”:先读目标,最大延迟与加权迟交罚金不同。
- “复杂度一样,实际次数也一样”:常数、输入分布和数据范围仍有影响。
- “两天要学完所有高级内容”:先完成P0闭环,再花剩余时间补P1。
来源、范围与使用说明
本讲义依据用户提供的九份文本附件,以及乐程信奥《最后一周!CSP-S初赛考点统计与冲刺建议》(网页标注 2026-09-11,读取于 2026-09-17)整理。文章地址:https://mp.weixin.qq.com/s/vV53RVMQct1lm1uoJhSv3A 。
网页自述统计范围为 2020—2025 年六年试题:90 道单选、18 段阅读、12 段完善程序。附件题组数量与之相符,但有附件未注明年份、有代码被改写或省略,不能把每段文本都当作官方原卷。这里保留来源编号;年份只按附件明确标题标注,不推测缺失年份。
| 来源 | 用户附件识别信息 | 包含内容 | 本讲义引用 |
|---|---|---|---|
| S1 | 完整原版 Markdown(含全部分析);7f94568b… | T1—T15 单选,T16—T18 阅读,T19—T20 完善 | S1-T题号 |
| S2 | 第一部分:单项选择题;c54226f3… | 15 单选,3 阅读,2 完善 | S2-T题号 |
| S3 | 第 k 小路径;e3c17681… | 2 段完善程序 | S3-完善1/2 |
| S4 | unsigned short f;1933b901… | 3 段阅读程序 | S4-阅读1/2/3 |
| S5 | CSP-S 2023;0679196c… | 15 单选 | S5-Q题号 |
| S6 | CSP-S 2022;f4f34292… | 15 单选、3 阅读 | S6-Q题号/阅读题号 |
| S7 | 归并第 k 小;3ab616ed… | 2 段完善程序 | S7-完善1/2 |
| S8 | CSP-S 2021;ce2fb632… | 15 单选、3 阅读、2 完善 | S8-Q题号/阅读/完善 |
| S9 | 竞赛题目与解析整理;b1091b7b… | 15 单选、3 阅读、2 完善 | S9-Q题号/程序/完善 |
内容标记。 “真题关联”表示对应用户附件,不额外担保附件的官方性;“纠错”表示与附件不同且给出理由;“教学重写”是帮助理解的完整算法,不能用于直接回答缺失原码的行号题;“拓展/变式”是练习方向,不是对本次试题的预测。P0/P1/P2 是按剩余复习时间安排的建议优先级,不是官方考纲等级。
完整的含义。 本资料逐项覆盖附件里出现的知识点、90 道选择题的知识归属以及30段程序的学习内容。它不声称穷尽信息学的全部知识;同一知识点合并讲解,原文另附以便追溯。源材料缺失的图、行号和代码无法凭空恢复,相应题目不作为确定答案背诵。
网页建议的吸收。 保留“先稳阅读程序、二分与最短路模板,再查漏基础知识”的方向;将网页七天计划压缩为两个复习时段。网页的地区分数建议只是作者经验,本资料不将其写成分数线或通过保证。正式考试时长、时间和规则以准考证及组织方通知为准;以下120分钟安排沿用网页所述试卷结构作训练参考。
网页统计整理与30段程序导航
以下统计转录自所给网页的三张表,是文章作者的归类口径,未以官方试卷逐题重新做年度统计。一个题目可涉及多个知识点,表中按文章主分类计数,不能把历史次数当作今年概率。附件独立题号仍用S编号,便于处理文本改写与年份缺失。
选择题:网页的六年分类统计
| 考点类别 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | 共计 | 占90题 |
|---|---|---|---|---|---|---|---|---|
| 组合数学与基础数学F09 加法、乘法、排列、组合与去重 F12 容斥原理与整除计数 F13 模运算、最大公约数、逆元与费马小定理 F16 概率、条件平均与期望 | 3 | 4 | 2 | 3 | 3 | 2 | 17 | 18.9% |
| 数据结构基础F08 排序 F20 栈、队列、后缀表达式与 BFS/DFS F21 哈希表 | 3 | 2 | 4 | 2 | 2 | 2 | 15 | 16.7% |
| 图论基础F28 图的基本量、存储与极值 F30 欧拉通路、欧拉回路与连通性 F31 强连通、DAG、拓扑排序与删环 F32 最短路、最小生成树与最小割 | 3 | 2 | 2 | 2 | 2 | 2 | 13 | 14.4% |
| 计算机基础常识F01 Linux 命令、编译与运行计时 F02 进制、位权、容量单位与字节序 F36 网络、编码与计算机常识 | 3 | 2 | 3 | 2 | 2 | 0 | 12 | 13.3% |
| 树结构F22 二叉树、完全树、满树与高度 F23 遍历、二叉搜索树与重建 F24 堆与 Huffman 编码 F26 Trie 与 KMP 前缀函数 F27 线段树的区间分解与“访问结点数” F33 树的最近公共祖先 LCA 与重心 | 0 | 2 | 1 | 1 | 1 | 4 | 9 | 10.0% |
| 复杂度与算法分析F05 复杂度 F06 分治递推、重复计算与快速幂 F07 比较次数的下界 | 1 | 2 | 2 | 2 | 1 | 1 | 9 | 10.0% |
| 算法思想F19 贪心选择必须有适用条件 F34 动态规划 F35 二分查找与答案的单调性 | 1 | 0 | 0 | 1 | 2 | 4 | 8 | 8.9% |
| 其他:进制、递归求值、运算符等F02 进制、位权、容量单位与字节序 F03 C++ 类型、运算符、短路和整数边界 F04 递归、调用栈、初值与数组边界 | 1 | 1 | 1 | 2 | 2 | 0 | 7 | 7.8% |
| 合计 | 15 | 15 | 15 | 15 | 15 | 15 | 90 | 100% |
前四类合计57题,即63.3%。这说明数学、基本结构、基本图论与常识值得快速查漏;近年来树和算法思想在该口径下增加,复习不能只记命令和人物。我们的基础章F01—F37进一步拆成可学习的小知识点,分类与文章不完全相同。
阅读程序:18段题材与讲义位置
| 网页年份 | 第1段 | 第2段 | 第3段 |
|---|---|---|---|
| 2020 | 位运算综合 → O11;S9-程序1O11 按位恒等式与枚举 | 随机快速选择 → O12;S9-程序2O12 随机快速选择 | 字符串最短变换、BFS与哈希 → O13;S9-程序3O13 字符串旋转状态图 |
| 2021 | 球体积、浮点计算 → O06;S8-阅读1O06 两个球的交体积 | 最大子段和、分治 → O07;S8-阅读2O07 最大子段和 | Base64、位运算 → O08;S8-阅读3O08 Base64 |
| 2022 | 字符串匹配跳步 → O01;S6-阅读1O01 Sunday 字符串匹配 | k进制基数排序 → O02;S6-阅读2O02 LSD 基数排序 | 负进制转换 → O03;S6-阅读3O03 负进制 |
| 2023 | 异或移位 → C01;S4-阅读1C01 固定字长异或变换与整数提升 | 筛法与约数和 → C02;S4-阅读2C02 约数和、筛法与交换求和 | 第k小数对差、二分与双指针 → C03;S4-阅读3C03 第 k 小数对距离 |
| 2024 | 位运算与限制深度快排 → PR06;S2-T16PR06 位运算化简与有深度限制的快速排序 | 子序列计数DP → PR07;S2-T17PR07 子序列计数 DP、二进制值与倒序更新 | 筛法、双哈希、二叉树 → PR08;S2-T18PR08 完全二叉树、字符串拼接哈希与去重 |
| 2025 | DFS排列计数 → PR01;S1-T16PR01 排列 DFS、剪枝与恢复现场 | 鸡蛋测试策略 → PR02;S1-T17PR02 两个鸡蛋、最坏情况与递减分段 | 折半搜索求和 → PR03;S1-T18PR03 折半搜索、快速幂与带重数的配对 |
术语校正。 网页将2021几何题写作“两球体积并”,而附件小问写“交”,给出的等半径样例约5π/12也对应交。因原码残缺,本讲义明确按附件意图讲交体积,并补“并=两球体积和−交”。网页把2022匹配称为Boyer–Moore式跳跃;附件实现具体是看窗口后一字符的Sunday方法。网页“第k大”与附件按k找第k小也要按实际代码辨别,不能只看表标题。
网页判断“位运算最多,排序二分常见”,并建议逐行模拟、特别注意边界,复杂度小问也应优先拿下。这里将这些建议落成变量表、不变量与最小反例练习。
完善程序:12段题材与讲义位置
| 网页年份 | 第1题 | 第2题 |
|---|---|---|
| 2020 | 分数背包、贪心 → O14;S9-完善1O14 分数背包 | 最优子序列、DP与位拆分 → O15;S9-完善2O15 最优子序列 |
| 2021 | 魔法数字、隐式图最短代价 → O09;S8-完善1O09 魔法数字 | RMQ、Four Russians方法 → O10;S8-完善2O10 RMQ、笛卡尔树、LCA、±1 分块 |
| 2022 | 两有序数组归并第k小 → O04;S7-完善1O04 两个有序数组的第 k 小 | 容器分水、搜索与路径恢复 → O05;S7-完善2O05 容器分水 |
| 2023 | DAG字典序第k小路径 → C04;S3-完善1C04 DAG 路径计数与字典序第 k 小 | 子数组最大值之和、分治 → C05;S3-完善2C05 所有子数组最大值之和 |
| 2024 | 两数组第k小和、二分答案 → PR09;S2-T19PR09 两个有序数组的第 k 小和 | 严格次短路 → PR10;S2-T20PR10 严格次短路 |
| 2025 | 分层最短路 → PR04;S1-T19PR04 分层状态最短路 | 受限组合编码测试 → PR05;S1-T20PR05 组合编码、测试集合与二进制解码 |
网页认为最短路值得优先复习,二分答案模板性强。容器分水原程序的一次性有环DFS记忆化不能泛化为可靠的任意状态最短路模板,学习时应同时理解BFS版。RMQ章节讲解笛卡尔树、LCA与±1分块,并给出以块间稀疏表、块内状态预处理实现的完整Four Russians教学代码,明确区分残缺原稿和教学重写。
原网页七天建议如何压缩
文章的Day1为四套选择题限时与分类,Day2为结构及复杂度约25题,Day3为数学图论与两个模板,Day4为位运算和两段阅读,Day5为最短路与二分完善程序,Day6为完善程序及错空复盘,Day7为错题回看与休息。
你只有明晚和后天早上的主要复习窗口,所以前面的冲刺路线合并为:明晚解决基础、位运算、排序和阅读;后天早上解决图论状态、二分、完善程序和错题。文章提到的强省65、中等省48只是作者经验目标,本资料不将其用作省份分数线;过去题目分布也不构成通过保证。若时间不足,先保证P0题目能独立解释,不以看过多少页为目标。
第一篇:选择题知识体系与考前变式
本篇把所给六组、共 90 道独立选择题合并为知识点。S1、S2、S9 没有可靠年份标注;S5、S6、S8 分别是资料自标的 2023、2022、2021。题号可在后面的“选择题核查索引”反查。这里的“必看”表示短期复习优先级,不是对今年出题的预测。
明晚先读: F01—F08、F09—F15、F20—F25、F28—F34。**后天早上:**遮住每节变式答案,独立做一遍,再回看错误;F16—F19、F26—F27、F35—F37 在有余力时补。不要为了临考背熟陌生模板而挤掉模拟程序、计算复杂度和排除错误选项的时间。
F01【必看】Linux 命令、编译与运行计时
概念与用法。 操作系统管理进程、内存、文件、设备等软硬件资源,并向应用程序提供服务。编译器把源程序转换为可执行程序或目标代码;“编译源程序”不是操作系统本身的基本定义。
| 需求 | 命令 | 容易混淆的地方 |
|---|---|---|
| 显示当前工作目录的路径 | pwd |
不会切换目录 |
| 切换目录 | cd 路径 |
cd .. 到父目录 |
| 列出文件及子目录 | ls |
默认通常不列出以点开头的隐藏项;ls -a 包括隐藏项 |
| 新建目录 | mkdir 目录名 |
不是 newdir |
| 复制文件 | cp 源 目标 |
不是移动文件 |
| 移动或重命名 | mv 源 目标 |
路径和目标名要分清 |
| 删除 | rm 文件 |
此处只记命令含义 |
| 输出文本 | echo 文本 |
与输出当前路径的 pwd 不同 |
| 编译 C++ | g++ main.cpp -o main |
-o 后面是输出文件名;g++ -o main main.cpp 也成立 |
time 的 real 是从开始到结束的实际经过时间,最接近秒表;user 是用户态 CPU 时间,sys 是内核态 CPU 时间。等待输入输出可以增加 real 而几乎不增加 CPU 时间。多线程并行时,累计 CPU 时间可以大于 real。
易错。 不能只凭 real > user+sys 就断言程序一定是 I/O 密集型,排队等待 CPU 也会造成差值;资料中的这个概括过强。
变式。 real=12s, user=5s, sys=2s,秒表约多少秒,累计 CPU 时间多少秒?答:12 秒、7 秒。
对应:S2-1;S5-1、11;S6-1、2;S8-1;S9-2。
F02【必看】进制、位权、容量单位与字节序
位权。 b 进制数 (a_k…a_1a_0)_b = Σ a_i b^i,每位满足 0≤a_i<b。十六进制 A—F 分别是 10—15;每个十六进制位对应 4 个二进制位,每个八进制位对应 3 位。
例如 (22F)₁₆=2×256+2×16+15=559;00101010₂+00010110₂=42+22=64=01000000₂。比较不同进制的数,先统一成十进制或二进制,不要直接比较字符串。
容量。 1 B=8 bit;二进制口径下 1 KiB=1024 B、1 MiB=1024 KiB、1 GiB=1024 MiB。题目常将二进制口径写作 KB/MB/GB,应辨认约定。严格十进制 GB 是 10⁹ B。
原始图像字节数=宽×高×每像素位数÷8;无压缩视频字节数再乘 每秒帧数×秒数,题目若另给音频则还要加音频空间。S9-3:每帧 2048×1024×4=8 MiB,共 8×60×24=11520 帧,大小 92160 MiB=90 GiB,约 96.637 GB;按原题选项约定选 90G。
字节序。 对假定为 32 位的 0xDEADBEEF,从低地址向高地址:小端 EF BE AD DE;大端 DE AD BE EF。unsigned char *p=(unsigned char*)&x 后,*p 读的是最低地址的一个字节,不是无条件读取数值的低 8 位。字节内部的位不倒过来。
变式。 32 位数 0x12345678,小端机器上 p[1] 是什么?答:0x56。 640×480、24 bit 的原始图像占多少字节?答:921600 B。
对应:S6-6;S8-2;S9-1、3。
F03【必看】C++ 类型、运算符、短路和整数边界
值与类型。 整数除法截去小数,5/2=2;正数的取模是余数,17%5=2。bool 只有 false、true 两个值,整数转布尔时 0 为假、非 0 为真;默认 cout << true 输出 1,使用 boolalpha 后输出 true。不能由此说数值 1 与真完全无关。int、bool 的字节数依实现而定,竞赛常见环境 int 为 32 位。
常考优先级(高→低)。 一元 ! ~ → * / % → + - → << >> → < <= > >= → == != → & → ^ → | → && → || → 赋值。遇到混合表达式,先写括号再算。
| 操作 | 含义 | 例子 |
|---|---|---|
a & b |
对应位都为 1 才为 1 | 5&3 = 101₂&011₂ =001₂=1 |
a | b |
对应位至少一个 1 | 5|3=7 |
a ^ b |
对应位不同为 1 | 5^3=6 |
!a |
逻辑非,结果是布尔值 | !5=false |
a && b、a || b |
逻辑与、逻辑或 | 不保留各位信息 |
A && B 若 A 假,不执行 B;A || B 若 A 真,不执行 B。S5-9 中 (a&b)||((c^b)&&(a|c)),左边已为真,右边实际不会求值;原文 101 & 11 =101 是笔误,正确是 001。
int a = 5, b = 3, c = 4; // 二进制分别为101、011、100
bool res = (a & b) || ((c ^ b) && (a | c));
// a&b=1,转换为true;||因此短路,右侧不执行,res为true。
// 如果单独计算右侧,c^b=7、a|c=5,也都为非零。
溢出。 32 位有符号整数常见范围为 −2³¹ 到 2³¹−1。long long ans=a*b 若 a、b 都是 int,可能在赋值前已溢出,写 1LL*a*b。标准 C++ 有符号溢出是未定义行为;不要想当然按循环取模处理。位移量不能为负,也不能大于等于被移位类型的位宽;需要大数位时使用 1ULL << k 并保证 k 合法。
变式。 int x=0; bool r=(x!=0 && 10/x>1); 是否除以 0?答:不会,发生短路,r=false。 2+3<<1 等于多少?答:(2+3)<<1=10。
对应:S5-9;也是全部程序题的基础。
F04【必看】递归、调用栈、初值与数组边界
递归的三件事。 状态是什么、何时停止、规模是否向终点缩小。每层调用通常需要保存返回位置、参数及局部状态,因此递归深度过大可能造成调用栈溢出。深度与调用总次数不同:朴素 Fibonacci 的深度 O(n),调用总次数为指数级。
void f(){int a[1000]; f();} 没有终止条件,按通常执行模型会不断加深调用;编译器可能进行优化,考试考的是无限递归及栈空间风险,不能把它写成所有环境下必然同一种报错。
初值和范围。 全局/静态数值数组默认清零;普通局部数组未初始化时内容不确定。int a[1000] 合法下标为 0—999,存 1000 个数并不越界;判断应看实际下标。回溯中的标记要在返回后恢复,否则会影响兄弟分支。引用参数 int &x 可以改动调用者的变量,值传递通常不能。
变式。 一个每层 O(1) 空间、深度为 n 的递归,辅助空间是多少?答:O(n),不是 O(1)。 数组长度 n、循环 i=0; i<=n; ++i 会访问多少项?答:n+1 项,最后一次越界。
对应:S2-3;S8-3。
F05【必看】复杂度:数操作,不看函数名字
定义。 O 表示渐近上界,Θ 表示紧确阶。选择题的“最准确”通常要求最紧的常见表达式。除非题目特别计大整数运算,一般把固定字长整数的一次加减、比较视为 O(1)。
| 代码/情境 | 推理 | 紧确阶 |
|---|---|---|
i++ 从 1 到 n |
n 次 | Θ(n) |
i*=2 从 1 到 n |
约 log₂n 次 | Θ(log n) |
| 外层 n 次,内层每次翻倍到 n | 次数相乘 | Θ(n log n) |
| 内层从 1 到 i,i 从 1 到 n | 1+…+n | Θ(n²) |
| 无序数组找最大值 | 比较 n−1 次 | Θ(n) |
| 两个先后执行的循环 | 相加,再取主导项 | 不是相乘 |
k=k+n/2 是一次算术更新,不代表做 n/2 次工作。分析双指针时,若右指针在整个程序中只前进 n 次,不能看到两个循环就判 O(n²)。
常见增长序:1 < log n < √n < n < n log n < n² < n³ < 2ⁿ < n!(表示渐近增长)。若 m=Θ(n),可以把复杂度中 m 的增长阶替成 n。S5-3 中 n√(log n)·log log n 比 n log n 小,因为 log log n / √(log n) →0。
变式。 for(i=1;i<=n;i++) for(j=1;j<=i;j*=2) 是什么阶?答:Σ log i=Θ(n log n)。 for(i=1;i<=n;i*=2) for(j=1;j<=i;j++) 呢?答:1+2+4+…+n=Θ(n)。
对应:S2-2;S5-3;S6-13、14;S9-7、14。
F06【必看】分治递推、重复计算与快速幂
先数每层产生几个递归调用,再数每个调用的规模和额外工作。
| 递推 | 每层或展开的工作 | 结果 |
|---|---|---|
T(n)=T(n/2)+Θ(1) |
每层常数,log n 层 | Θ(log n) |
T(n)=2T(n/2)+Θ(1) |
调用数 1+2+…+n | Θ(n) |
T(n)=2T(n/2)+Θ(n) |
每层 Θ(n),log n 层 | Θ(n log n) |
T(n)=2T(n/2)+Θ(n²) |
n²+n²/2+n²/4+… | Θ(n²) |
T(n)=T(n−1)+Θ(n) |
n+(n−1)+…+1 | Θ(n²) |
S5-15 虽然函数名叫 quick_power,却把同一个 quick_power(x,n/2) 调用了两次,因此时间 Θ(n)。缓存到局部变量只调用一次才是 Θ(log n)。
double original_power(double x, unsigned n) {
if (n == 0) return 1; // x的0次方
if (n == 1) return x; // 递归停止条件
return original_power(x, n / 2) // 第一次独立求半次幂
* original_power(x, n / 2) // 又从头计算同一子问题,没有复用
* ((n & 1) ? x : 1); // 奇指数多乘一个x
}
// 不能把“递归深度log n”直接当作“总时间log n”;本例总时间Theta(n)。
long long mod_pow(long long a, long long n, long long mod) {
// 假定 mod>0,且乘积不会溢出 long long;普通小模数适用。
long long ans = 1 % mod;
a = (a % mod + mod) % mod; // 把底数化到 [0,mod-1]
while (n > 0) {
if (n & 1) ans = ans * a % mod; // 当前二进制位为 1,乘入贡献
a = a * a % mod; // 下一位对应底数平方
n >>= 1; // 处理下一位,只循环 O(log n) 次
}
return ans;
}
朴素 Fibonacci 重复求相同子问题,时间 Θ(φⁿ),其中 φ≈1.618;O(2ⁿ) 是常见选项中的正确上界。记忆化搜索或递推使每项计算一次,时间 O(n);只保存前两项可把辅助空间降到 O(1)。
变式。 f(n){ if(n<=1)return 1; t=f(n/2); return t*t; } 的时间、栈空间?答:均为 O(log n)。 把 t*t 改为两次独立 f(n/2),时间变 Θ(n),栈深仍 O(log n)。
对应:S1-11、14;S5-15;S8-12。
F07【必看】比较次数的下界
找最大值。 每个不是最大值的元素至少要在某次比较中输一次,一次比较最多淘汰一个最大值候选,所以至少 n−1 次;打擂台法恰能达到。
同时找最大最小。 对 2n 个元素,先两两比较 n 次。每对的大者进入最大值候选组,小者进入最小值候选组;两组各用 n−1 次,合计 3n−2。一般 N 个元素的最坏最少比较次数为 ceil(3N/2)−2(N≥2)。
易错。 分别扫描找最大和最小是可行算法,但比较 2N−2 次,并非最少;题目如果问“以比较为基本运算”,不能用一次神秘函数调用当作 O(1)。
变式。 10 个数同时找最大和最小至少多少次?答:5+4+4=13。 9 个呢?答:12。
对应:S2-2;S6-14;S8-5。
F08【必看】排序:稳定性、最好/平均/最坏情况
稳定性是相等关键字的记录在排序后是否保持原相对顺序。把两个相等值标为 3甲、3乙,不能只看数值有序就判断稳定。
| 常规实现 | 平均时间 | 最坏时间 | 稳定性 | 关键机制 |
|---|---|---|---|---|
| 插入排序 | O(n²) | O(n²) | 稳定 | 向有序前缀插入;有序时 O(n) |
| 冒泡排序 | O(n²) | O(n²) | 稳定 | 相邻逆序交换;带提前退出时最好 O(n) |
| 选择排序 | O(n²) | O(n²) | 通常不稳定 | 远距离交换可能跨过相等记录 |
| 归并排序 | O(n log n) | O(n log n) | 可稳定 | 相等时先取左段;常用 O(n) 辅助数组 |
| 快速排序 | O(n log n) | O(n²) | 通常不稳定 | 划分不均衡会退化 |
| 堆排序 | O(n log n) | O(n log n) | 不稳定 | 堆顶与末尾远距离交换 |
| 基数排序 | O(d(n+b)) | 同左(给定 d、b) | 各位使用稳定分配时稳定 | d 位,每位基数 b |
固定取首元素作枢轴时,有序且键互异的数组会划分为 0 与 n−1 个待处理元素,递推 T(n)=T(n−1)+Θ(n)。随机枢轴改善期望时间,不承诺最坏一定 O(n log n)。
基数排序的前提。 低位优先排序必须保证每一位的排序稳定,才能保留已排低位的结果。若只有某个数据值在过程中改变,其他记录及控制数据不受破坏,则删掉该异常记录后,其他记录仍按正确规则有序;不能推广到任意内存故障。
变式。 既要求常规比较排序“稳定”,又要求最坏 O(n log n),选归并还是堆排?答:归并;合并时相等优先取左边。
对应:S5-10;S6-4、5;S8-4。
F09【必看】加法、乘法、排列、组合与去重
加法原理用于互不重叠的分类:方案总数相加。乘法原理用于完成一件事的连续步骤:各步可选数相乘。P(n,k)=n!/(n−k)! 用于选 k 个且区分顺序;C(n,k)=n!/[k!(n−k)!] 用于只选集合。
先问四个问题:对象可否重复?位置有无区别?有没有首位/相邻限制?同一结果是否被多算?
例:10 人分金银铜 10×9×8=720;8 人选一支两人队 C(8,2)=28。若是把 8 人全分成 4 支无标号两人队,答案是 8!/(2!⁴·4!)=105,这是另一问题。
从 0—4 各用至多一次组成四位数:首位 4 种,后面 4×3×2 种,共 96;若允许重复则是 4×5³=500。车牌两个字母、三个数字,均可重复,共 26²×10³。
变式。 6 人选正、副队长有多少方案?答:6×5=30。 只选两名队员呢?答:C(6,2)=15。
对应:S2-4;S5-2;S6-10、11。
F10【必看】相邻限制:插空法、递推与压缩位置
插空法。 r 个相同红球先排成一行,有 r+1 个空位。放 b 个相同蓝球并要求蓝球不相邻,答案 C(r+1,b),前提 b≤r+1;超出则 0。如果两种球内部都可区分,还要乘 r!·b!,不能漏看“除了颜色完全相同”。S1-1 为 C(6,5)=6。
固定选 k 个不相邻位置。 从 n 个线性位置选 x₁<…<x_k,要求 x_{i+1}≥x_i+2。令 y_i=x_i−(i−1),就变成从 n−k+1 个位置任取 k 个,答案 C(n−k+1,k)。
任意选、不可相邻。 令 a(n) 含空集。末位不选有 a(n−1) 种,末位选则倒数第二位不能选,有 a(n−2) 种。因此 a(n)=a(n−1)+a(n−2),a(0)=1,a(1)=2。8 个苹果至少选一个是 a(8)−1=55−1=54。
易错。 圆圈的首尾也相邻,不能直接套线性公式;要按第一个位置选/不选分类。
变式。 7 个位置选 3 个且互不相邻?答:C(5,3)=10。 5 个位置至少选 1 个且不相邻?答:a(5)−1=13−1=12。
对应:S1-1;S8-13。
F11【必看】圆排列、图中的环与棋盘计数
圆排列。 n 个不同对象围圆,只忽略旋转,方案为 (n−1)!。无向简单图中的环还忽略反向走法,因此 n≥3 时环数再除 2。
完全图 K_n 中长度为 k 的简单无向环,先选点,再排环:C(n,k)·(k−1)!/2 = P(n,k)/(2k)。长度为 k 指经过 k 条边、k 个互异顶点;不是允许重复点的闭合游走。S2-12:C(10,4)×3=630。
棋盘。 r×c 棋盘选两个不同行不同列的格子,无序选取:rc(r−1)(c−1)/2。也可先选两行两列,再选两条对角线之一:2 C(r,2) C(c,2)。两个式子相同。
变式。 K₆ 中三角形多少个?答:C(6,3)=20。 3×5 棋盘两个不同行不同列格子多少种?答:15×2×4÷2=60。
对应:S2-12;S6-9;S9-13。
F12【必看】容斥原理与整除计数
三集合公式。 |A∪B∪C|=|A|+|B|+|C|−|A∩B|−|A∩C|−|B∩C|+|A∩B∩C|。先加单集合,减重复加的两两交集,再补回被多减的三重交集。
1—N 中能被 d 整除的数有 floor(N/d) 个;同时被 a、b 整除要用 lcm(a,b),不是无条件 a×b。题目求“都不能被这些数整除”,用 N 减去并集。
S1-13:能被 2、3、5 至少之一整除的数量 500+333+200−166−100−66+33=734,都不整除为 266。
变式。 1—100 中既不能被 4 整除,也不能被 6 整除,有多少?答:100−25−16+8=67,其中交集除数是 lcm(4,6)=12。
对应:S1-13。
F13【必看】模运算、最大公约数、逆元与费马小定理
模运算。 a≡b (mod m) 表示 m 整除 a−b。加减乘可以先取模;“除以 a”不能简单写成除法,需要乘 a 的逆元,且逆元须存在。
gcd(a,b)=gcd(b,a%b),不断取余直到余数 0。lcm(a,b)=a/gcd(a,b)×b(正整数时),先除后乘有助于减少溢出。
逆元的条件。 m>1 时,a 的乘法逆元 x 满足 ax≡1 (mod m),存在当且仅当 gcd(a,m)=1。扩展欧几里得求 ax+my=gcd(a,m),若 gcd=1,则 x 取模后是逆元。m 不必是质数。
费马小定理。 p 为质数且 p 不整除 a 时,a^(p−1)≡1 (mod p),从而 a^(p−2) 是逆元,可用快速幂。S8-11 的递归起点 solve(1,n)=1,所以 solve(t,n)=5^(t−1) mod n,不是 5^t;solve(23,23)=1。
易错。 模数是质数也不能给 0 求逆元;模数合数时不能直接把指数写 m−2。
变式。 3 模 10 的逆元?答:7,因为 3×7≡1。 6 模 10 的逆元?答:不存在,gcd=2。
对应:S2-9;S8-11;S9-10。
F14【必看】数位和、数根与构造最小整数
数位和是各位数字相加;数根是反复求数位和直到只剩一位。b 进制下 b≡1 (mod b−1),所以数位和与原数模 b−1 同余。正整数的 b 进制数根为 1+(n−1) mod (b−1);n=0 的数根为 0,需单独处理。
S5-14 的区间 100₁₆=256 到 1A0₁₆=416,十六进制数根为 9 等价于 n≡9 (mod15)。第一个是264,最后一个414,数量 (414−264)/15+1=11。记住模的是 15,不是16。
固定十进制数位和 S 的最小正整数。 先尽量减少位数,每位最大9;在最少位数下让高位尽量小、低位尽量大。例如 S=19,至少3位,最小为199。若 f(f(x))=10,令 s=f(x),最小满足 f(s)=10 的 s 是19,故最小 x=199。
变式。 十进制 999 的数根?答:9,不是0。 数位和为20的最小正整数?答:299。 十六进制 FF 的数根?答:F,因为255是15的正倍数。
对应:S2-13;S5-14。
F15【必看】递推求值、等差求和与条件分类
递推求值。 先写初值,再按依赖顺序算,尤其注意整除。S2-6:f(1)=1, f(n)=f(n−1)+f(floor(n/2)),得到 1,2,3,5,7,10…。
嵌套递归。 Ackermann 函数先求内层,再把结果当外层参数;不是把 ack(m, n−1) 当作下标 n−1。由原题可推 ack(0,n)=n+1,ack(1,n)=n+2,ack(2,n)=2n+3,所以 ack(2,2)=7。
unsigned ack(unsigned m, unsigned n) {
if (m == 0) return n + 1; // 第一类边界:直接返回
if (n == 0) return ack(m - 1, 1); // 此时m>0,降低m并把n置1
return ack(m - 1, ack(m, n - 1)); // 先算内层,返回值作为外层第二个参数
}
// 手算先列ack(0,n),再列ack(1,n),最后求ack(2,2),避免乱展开。
等差求和。 1+…+t=t(t+1)/2。从第1层到第x层经过x−1段,因此楼梯题的总消耗是 10×x(x−1)/2。用“不够的最大值”和“够的最小值”夹住答案:到14层910、到15层1050,故15层。
三角形分类。 三条正边排序为 a≤b≤c,只需检查 a+b>c;等腰包括等边时,先单算等边,避免重复。S8-14 中等边9种;设相等边为 x,另一边 y≠x,需 1≤y≤min(9,2x−1),非等边共 3Σ[min(9,2x−1)−1]=156,合计165。
变式。 上述 f(6) 是多少?答:10。 ack(2,3) 呢?答:9。 边长 2、2、4 是否是等腰三角形?答:不是,等号使三角形退化。
对应:S2-6;S6-15;S8-14;S9-11。
F16【进阶】概率、条件平均与期望
定义。 等可能情形下,概率=有利情形数÷全部情形数;随机变量期望 E[X]=Σ x·P(X=x)。先列互斥完备的结果,不要把“赢一次的收益”当作总平均。
两次独立公平骰子有36个等可能有序结果。S5-8 第一次为x,第二次不同的概率5/6,因此 E=Σ_{x=1}^6 (1/6)×(5/6)×2x=35/6。条件平均必须再乘出现该条件的概率。
常用性质。 E[X+Y]=E[X]+E[Y] 不要求独立;E[XY]=E[X]E[Y] 一般需要独立或另有条件。至少一次发生常用“1−一次也没发生”。
变式。 两次掷公平骰子,至少一次为6的概率?答:1−(5/6)²=11/36。 若第一次得到 x 元、第二次相同归零,期望?答:35/12 元。
对应:S5-8。
F17【进阶】同余方程与小范围枚举
小范围题无需强背中国剩余定理代码,先利用一个条件产生少量候选,再筛剩余条件。S9-10 要求 n≡2(mod3), n≡3(mod5), n≡4(mod7), n<60。
先由模5得到 n=5k+3,代模3:2k≡2(mod3),即 k≡1(mod3),所以 n=15t+8。代模7得 t+1≡4(mod7),即 t≡3(mod7),因此 n=53+105s;在正整数小于60时只有53。
易错。 多个模数不互质时,未必有解;例如 n≡0(mod2) 与 n≡1(mod4) 冲突。不能机械相乘就称有唯一解。
变式。 小于30的正整数同时满足 n≡1(mod3), n≡2(mod5)?答:7、22;相邻解相差15。
对应:S9-10。
F18【进阶】构造题:给出可行方案,还要证明到此为止
S5-4 要依次把1、2、3……放到柱顶,相邻数之和必须是平方数。4根柱子放到11的一组正确方案(从下往上)是:
| 柱1 | 柱2 | 柱3 | 柱4 |
|---|---|---|---|
| 1→3→6→10 | 2→7→9 | 4→5→11 | 8 |
每一对之和分别为4、9、16或9、16等,且每根柱子编号递增,能按1到11的顺序执行。原资料的第一张表存在错位,不能照着背。
为什么12不行。 1、2、4没有合法的更小前驱,所以至少占3根柱底。3和8在1—12中都只能放在1上方,1不能同时接两条链,所以二者至少一个要另开柱;5和12都只能放在4上方,也至少一个要另开柱。至少需要 3+1+1=5 根柱,故4根最多11。
这类题的通用方法是“构造下界+必要条件给上界”;高级建模是 DAG 最小路径覆盖,但临考不必为了这道选择题背匹配模板。
变式。 只放1—4,需要至少几根柱?答:3根,可为1→3、2、4;1、2、4都必须是柱底。
对应:S5-4。
F19【进阶】贪心选择必须有适用条件
贪心每步选择当前最优动作;正确性通常来自交换论证或结构性质。能写出一个看似合理的排序规则,不等于已经证明全局最优。
经典可用。 分数背包可按价值/重量降序;Huffman 每次合并最小两权;Kruskal 选不成环的最轻边;Dijkstra 在非负边权下选当前最近未确定顶点。
不能照搬。 0-1背包不能切物品,单按性价比不保证最优。截止时刻最早优先可用于可行性及最大延迟等特定目标,不能笼统说“能最小化任意加权超时惩罚”。
S1-15 按 A3,A1,A2,A5,A4,完成时刻2、5、9、10、15,都不超过各自截止时刻,惩罚0;因为惩罚非负,这已经证明最优。只有A3先执行能使所有任务准时,因此该题选B,但不是证明了一般加权调度都能这样贪心。
反例变式。 两任务 (时长,截止)=(10,1),(1,2),超时罚时长。先做截止更早的长任务罚11;先做短任务罚10。因此最早截止优先不普遍最优。
对应:S1-15;S9-6。
F20【必看】栈、队列、后缀表达式与 BFS/DFS
栈后进先出,只从栈顶入/出;队列先进先出,队尾入、队首出。DFS 常用递归栈或显式栈;BFS 按层扩展,常用队列。优先队列按优先级出队,不保证先到先出。
验证出栈序列。 按给定入栈顺序推进,目标元素若还未入栈,就把它前面的未入栈元素依次压入;然后必须等于栈顶才能弹出。附加“不能连续三次出栈”时,同时记录连续出栈次数,入栈后清零。S6-3 的 afedcb 在 a 出栈、其余全部入栈后,必须连续弹5次,违背条件。
后缀式。 操作数在前、运算符在后;可按表达式树做后序遍历。a*(b+c)-d 为 abc+*d-。求值时遇操作数入栈,遇二元运算符先弹右操作数,再弹左操作数,压入计算结果。
变式。 后缀 8 3 2 * - 等于多少?答:8−3×2=2。 入栈1、2、3能否按3、1、2出栈?答:不能,3出后栈顶是2。
对应:S2-5;S6-3;S9-4、9、12。
F21【必看】哈希表:函数、冲突、线性探查
定义。 哈希函数把关键字映射到有限地址。不同关键字映射同地址叫冲突。解决方法包括链地址法、开放地址法;线性探查只是开放地址法的一种,二者不能直接画等号。
表长 M,线性探查依次试 h(x),(h(x)+1)%M,…。必须按输入顺序更新整张表;不能每次只算余数而忽略已占位置。
S6-12 正确插入位置为 71→1,23→3,73→4,99→9,44→5,79→0,89→2。原资料中间表格错位,最终答案D正确。S1-6 的74从9冲突到10再到11,选D。
复杂度。 装载因子 α=元素数/槽数。合理散列且装载适当时查找的期望可接近 O(1),但最坏可退化到 O(n)。查找与插入应沿同一探查序列;开放地址删除元素通常要做删除标记,不能简单改成“从未使用的空槽”,否则会截断后面的查找链。
变式。 表长7,h(x)=x%7,依次插入6、13、20,位置?答:6、0、1。 插入13后把6的位置直接标为从未使用,会有什么风险?答:查13从6开始时可能错误地提前判不存在。
对应:S1-6;S2-10;S6-12;S8-6;S9-5。
F22【必看】二叉树、完全树、满树与高度
一般二叉树每个结点最多两个孩子,区分左右;完全二叉树除最后一层外全满,最后一层从左向右连续;满二叉树在本讲义中指每层均满的完美二叉树。不同资料对“满”的叫法可能不同,计算前必须读题中定义。
根算第1层时,第i层至多 2^(i−1) 个,h层至多 2^h−1 个。n个结点的最小可能高度是 ceil(log₂(n+1)),最大为n。完全二叉树的高度 floor(log₂n)+1。2021个结点至少11层。
从1开始按层序编号的完全二叉树:父亲 i/2,左孩子2i,右孩子2i+1,越过n则不存在;这些公式不能用于任意前序编号。
一般二叉树若n₀为叶子数、n₂为有两个孩子的结点数,则 n₀=n₂+1:由总边数n−1等于孩子数总和 n₁+2n₂ 推得。
m叉树。 h层每层全满时结点数 (m^h−1)/(m−1)。S6-7 的答案97建立在满5层三叉树的理解上;若“完全”允许最后一层未满,仅知道深度并不足以唯一确定前序第100号的父号,见核查索引。
变式。 完全二叉树有100个结点,层数、叶子数?答:7层,50个叶子。 三层满三叉树结点数?答:1+3+9=13。
对应:S2-11;S6-7;S8-8。
F23【必看】遍历、二叉搜索树与重建
前序:根→左→右;中序:左→根→右;后序:左→右→根。不同结点有唯一标识时,“前序+中序”或“后序+中序”可唯一重建一般二叉树;只有前序+后序通常不够,例如一个孩子在左还是右无法区分。
BST。 键互异时左子树全小于根、右子树全大于根,中序为严格递增。允许重复键时必须另给重复值放哪边的规则,不能擅自同时放两边。
S1-8 后序末尾6是根;此前按大小分为左段 2,5,4、右段 8,12,10。再取各段末尾4和10作根,得前序 6,4,2,5,10,8,12。
相同遍历。 结点标识互异时,前序与中序相同当且仅当每个结点没有左孩子,即全是向右的链;只要求根无左子树远远不够。
变式。 BST 后序为 1,3,2,5,4,前序?答:4,2,1,3,5。 一般树前序AB、后序BA能唯一判断B是左孩子吗?答:不能。
对应:S1-8;S8-9。
F24【必看】堆与 Huffman 编码
堆在结构上是完全二叉树;小根堆满足父键≤孩子键,因此最小值在根,但左右子树之间、同层结点之间不要求全局有序。插入末尾再上浮,删除堆顶时用末尾补根再下沉,均 O(log n);看堆顶 O(1)。自底向上建堆 O(n),逐个插入建堆上界 O(n log n)。
S1-12 的所有元素为5、8、10、12、15、20,连删两个最小后堆顶必是10,不必为求堆顶重画所有内部结构。若题目问完整堆数组,就必须模拟上浮/下沉规则。
Huffman。 每次取最小两权相加,合并成新结点并放回;最小化带权路径长度 WPL=Σ(叶权×深度)。树边标0/1形成前缀编码,任一完整码字不是另一完整码字的前缀。所有合并产生的新权值之和等于 WPL。
变式。 权值1、2、4、8构造Huffman树,WPL?答:先1+2=3,再3+4=7,再7+8=15,WPL=3+7+15=25。
对应:S1-12;S5-5;S9-6。
F25【必看】逆序对与最少相邻交换
逆序对是 i<j 且 a[i]>a[j] 的一对位置。交换一对相邻逆序元素恰好减少一个逆序对;变为升序需要至少原逆序对数次,冒泡式交换能达到,所以这是最少次数。
S8-10 DACFEB:D贡献3对,A贡献0,C贡献1,F贡献2(E、B),E贡献1,总计7。原解析逐项列举有混乱,但答案7正确。
01串把全部1移到右侧,需交换的次数=每个1右边0的数量之和;有k个1、n−k个0,最坏串 11…100…0,答案 k(n−k)。
易错。 “可交换任意两位置”与“只能相邻交换”答案不同;有相等元素时不算严格逆序。大数据计数可用归并排序 O(n log n),计数需 long long。
变式。 10100 把1全放右边至少多少次?答:第一个1后有3个0,第二个1后有2个0,共5次。
对应:S2-14;S8-10。
F26【进阶】Trie 与 KMP 前缀函数
Trie。 从根出发,每条字符边扩展一个前缀;相同前缀共用路径。总结点数=不同非空前缀数量+根。单词结束标记通常只是结点属性,不额外占一个结点。
S1-4 的不同非空前缀是 c,ca,cat,car,cart,cas,case,d,do,dog,共10个,加根11。原文文字列表漏列 cart 对应的末尾t,但答案11正确。
KMP 前缀函数。 此处 pi[i] 为串 s[0..i] 的最长相等真前缀、真后缀长度,不能取整个串。abacaba 得 [0,0,1,0,1,2,3]。前后缀可以重叠,如 aaaa 的末项为3。
vector<int> prefix_function(const string &s) {
vector<int> pi(s.size(), 0); // pi[0]=0:一个字符没有非空真前后缀
for (int i = 1; i < (int)s.size(); ++i) {
int j = pi[i - 1]; // 先尝试沿用前一个位置的最长匹配
while (j > 0 && s[i] != s[j])
j = pi[j - 1]; // 失败后跳到更短的候选前后缀
if (s[i] == s[j]) ++j; // 新字符相同,匹配长度加一
pi[i] = j;
}
return pi; // 总时间 O(|s|),不是每次从头试
}
易错。 有些教材 next 从−1开始,或表示下一个比较位置,数值会不同;必须依据题给定义。模式串长m,令 p=m−pi[m−1],当 m%p==0 时可由长度p的块重复组成。
变式。 Trie 插入 a,ab,ac,ab 共几个结点?答:4(含根),重复插入不加结点。 aaaa 的pi数组?答:0,1,2,3。
对应:S1-2、4。
F27【进阶】线段树的区间分解与“访问结点数”
线段树把区间 [l,r] 递归分成 [l,mid]、[mid+1,r];若当前结点区间被查询完全覆盖,就直接使用该结点保存的合并信息,否则进入有交集的子区间。区间和、区间最值等可这样维护;单点修改及标准区间查询 O(log n),建树 O(n)。
S1-3:根 [0,15],查询 [3,11]。完整覆盖块为 [3,3]、[4,7]、[8,11];路径上的其他结点为 [0,15]、[0,7]、[0,3]、[2,3]、[8,15],合计8个。
定义陷阱。 “合并了几段”答案3;“最少访问几个有交集结点,含父结点”答案8。如果实现无条件调用两个孩子,再进入函数判断无交集,实际函数调用还会包括3个无交集结点,总计11。不能不看统计口径。
变式。 同一棵树查询 [0,7],只访问有交集的分支:访问多少结点,完整覆盖块多少个?答:根和左孩子共2个;完整覆盖块1个。
对应:S1-3。
F28【必看】图的基本量、存储与极值
简单无向图无自环、无重边;n点最大边数 n(n−1)/2。简单有向图若允许相反方向各一条边,最多 n(n−1) 条。无向图度数和2m,有向图入度和=出度和=m。
连通无向图至少n−1条边;树是连通无环无向图,恰n−1条边。森林有n点c个连通分量时,边数n−c。
非连通极值。 n点非连通简单无向图,边数最多 C(n−1,2),由n−1点完全图加孤立点达到。S8-7 有36条边,要 C(n−1,2)≥36,最小n=10。应写不等式,不是所有类似题都能刚好解等式。
存储。 邻接矩阵空间 O(n²),查两点是否有边 O(1);邻接表空间 O(n+m),遍历所有邻边更适合稀疏图。邻接表 DFS/BFS 总时间 O(n+m),无向边会存/扫描两次但仍同阶;邻接矩阵遍历通常 O(n²)。
变式。 7个点的非连通简单无向图最多多少边?答:C(6,2)=15。 10点森林有3个分量,多少边?答:7。
对应:S8-7;S9-7。
F29【必看】二分图、染色、奇环与边数
二分图可把顶点分成两部分,使每条边都跨两部分;等价于可用至多两种颜色使相邻顶点异色,也等价于不存在奇数长度的环。树没有环,必是二分图;树是几叉不影响结论。
两部大小p、q时最多pq条边。固定p+q=n,乘积在尽量均分时最大,所以n点二分图最大边数 floor(n²/4)。24点最大 12×12=144。
反例法。 三角形既是平面图、没有桥、每点偶数度,又需要3色。于是“平面图/边双连通图/欧拉图一定2色”均不成立。
变式。 5个顶点的二分图最多几条边?答:2×3=6。 四边形加任意一条对角线还是二分图吗?答:不是,出现三角形。
对应:S5-6;S9-8。
F30【必看】欧拉通路、欧拉回路与连通性
欧拉通路经过每条边恰好一次;回路还要求起终点相同。它讨论的是边不重复,不是每个顶点只能经过一次(后者是哈密顿问题)。
对有边的无向图,在所有非零度顶点位于同一连通分量的条件下:全为偶数度当且仅当有欧拉回路;恰两个奇度顶点当且仅当有起终点不同的欧拉通路。许多考试定义直接把欧拉图限定为连通图,按题目约定处理孤立点。
2正则图。 每点度为2的简单无向图是若干互不相交环的并,未必连通。若还要求一条欧拉回路经过全图所有边,才必须是覆盖所有n点的单环,n≥3时不同带标号图数 (n−1)!/2。S6-9 的答案D正确,但“偶数度自动是欧拉图”的原解释缺连通条件。
变式。 两个互不相连的三角形,每点度2,有覆盖全部6条边的一条欧拉回路吗?答:没有,不连通。 一个4边环的边数是偶数,是否欧拉图?答:是,边数奇偶不是判定条件。
对应:S2-7;S6-9。
F31【必看】强连通、DAG、拓扑排序与删环
强连通。 有向图中任意两点u、v,既能从u到v,也能从v到u;要求的是路径,不是直接边。对至少两点的强连通图,每点入度、出度都≥1。单个无自环顶点通常也视为强连通,不能漏掉这个定义边界。
拓扑序。 一个顶点排列,使每条有向边u→v中u都排在v前面。存在拓扑序当且仅当图是DAG,即无有向环。
Kahn算法:将所有入度0顶点放入队列,弹出一个,删它的出边并降低后继入度,出现新的0入度点就加入。若最终取出少于n个顶点,存在有向环。邻接表时间 O(n+m)。
数量。 线性有向链只有1种;完全无边有n!种;一般取决于偏序约束,不由n−m决定。每一步只有一个可选入度0点,则拓扑序唯一;某一步有两个可选点即可形成不同顺序。
删边。 要变成DAG,必须破坏所有有向环。只有一个环时删环上任意一边即可;多环时,删某个环的一条边未必够。S5-13 缺原图,资料称有3个候选,但不能独立验证图是否还有其他环。
变式。 边为1→3、2→3,拓扑序有几种?答:123、213,共2种。 1→2→3→1,删一边能否得到拓扑序?答:能,三条边任选其一。
对应:S1-5;S5-13;S6-8。
F32【必看】最短路、最小生成树与最小割
这三类题目标不同:最短路关心指定起终点的距离;生成树关心连接所有顶点的总边权;割关心删哪些边能使起终点不可达。
最短路。 无权/等权图可BFS。Dijkstra要求边权非负,每次选当前暂定距离最小的未确定点,放松其出边:dist[v]=min(dist[v],dist[u]+w)。不用堆,选点n轮每轮扫描n点,简单图总时间 O(n²);用二叉堆和邻接表常见上界 O((n+m)log n)。负边一般用Bellman–Ford等,不能机械套Dijkstra;DAG可以按拓扑序放松,即使有负边也可。
最小生成树 MST。 针对连通无向带权图,用n−1条边连接所有点且总权最小。Kruskal按边权升序,只有连接两个不同分量时才选边,常用并查集;Prim不断把树外到树内最轻的连接加入。允许负边。S1-7 所有边权至少1,任何生成树至少7;相邻编号串成链恰7,故最优7。
最小割。 指定s、t,将顶点分为S、T且s∈S、t∈T,割边是从S到T的有向边;删去后s不能到t。每边容量1时最小割值等于最少删边数,也等于最多条边不重叠s→t路径数。割的最小大小与达到最小大小的边集合数量是两问,不能用“有两条路径”直接得出有几种删法。S2-15 缺图,保留未核验标记。
变式。 三点边权AB=2、BC=2、AC=3,A到C最短距离、MST总权?答:3和4。 s→a→t、s→b→t两条独立链且无其他边,最少删几条、最小删法几种?答:2条、2×2=4种。
对应:S1-7;S2-15;S8-15;S9-6、14。
F33【必看】树的最近公共祖先 LCA 与重心
LCA。 在有根树中,u、v的公共祖先里深度最大的那个。结点也是自身祖先,因此 LCA(u,u)=u;如果a是u祖先,则 LCA(u,a)=a;与根求LCA必为根。两个点编号的大小本身不表示祖先关系。
朴素方法先把深的点向上提到同深,再同时上提直到相遇,O(h);倍增预处理 up[u][j] 为2^j级祖先,预处理 O(n log n)、每问 O(log n)。临考选择题先理解祖先关系,不必硬背完整模板。
重心。 删除点u后,把剩余连通块的最大大小记M(u),使M(u)最小的点是重心。等价地,每一块大小均≤n/2。用某个根做DFS时,需同时考虑孩子子树大小和“父方向”的 n−size[u],不能漏掉树外半边。
一棵树有1或2个重心;有两个时相邻且n偶数;n奇数必只有1个,但n偶数也可能只有1个。重心与“离其他点最远距离最小”的树中心不是同一定义。
变式。 6点星形树重心几个?答:1个,中心点。 6点链呢?答:2个,中间两点。 根是1,LCA(12,18)=4,那么LCA(12,1)?答:1。
对应:S1-10;S5-12。
F34【必看】动态规划:0-1背包与 LCS
动态规划的关键是状态含义、转移来源、初始值、计算顺序。存在重叠子问题时,把结果保存下来,避免重复计算;“用了数组”本身不等于动态规划。
0-1背包。 每物品最多选一次,容量W。dp[j] 定义为容量不超过j时的最大价值,对重量w、价值v的物品转移 dp[j]=max(dp[j],dp[j−w]+v)。压成一维后容量必须倒序,以保证右侧使用的是上一轮状态。
vector<long long> dp(W + 1, 0); // 不要求恰好装满;空选价值为0
for (int i = 0; i < n; ++i) { // 每件物品只处理一轮
for (int j = W; j >= weight[i]; --j) { // 倒序,阻止同一物品重复使用
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
// dp[W] 是最大价值;时间 O(nW),辅助空间 O(W)。
S1-9容量20,选重量7、4、3、6,价值15+9+7+13=44。对5件小数据也可枚举32个子集核查最优。若题目要求“恰好装满”,应令 dp[0]=0、其他状态为不可达,而不是全部0。完全背包每件无限次,常用容量正序。
LCS。 子序列可以不连续,但相对顺序不能变;子串必须连续。令 f[i][j] 为两个串前i、j个字符的LCS长度。若末字符相同,f[i][j]=f[i−1][j−1]+1;否则取 max(f[i−1][j],f[i][j−1]);空前缀初值0,时间O(nm)。S5-7答案6,一条公共子序列是ABCABA;仅给出长度6的例子说明至少6,用DP才能完整证明不可能7。
变式。 容量4,只有一件重量2价值3的0-1物品,答案多少?答:3;若错误正序更新会得到6。 ABC与AC的LCS和最长公共子串长度分别多少?答:2和1。
对应:S1-9、14;S5-7;S9-6。
F35【进阶】二分查找与答案的单调性
有序数组中比较中点后可以排除一半,所以二分查找 O(log n)。更一般地,二分依赖一个单调的真假分界,不要求对象必须是数组、整数或长度为2的幂。
int first_ge(const vector<int> &a, int x) {
int l = 0, r = (int)a.size(); // 答案位于闭区间 [l,r];r可等于n表示不存在
while (l < r) {
int mid = l + (r - l) / 2; // 避免直接 l+r 可能溢出
if (a[mid] >= x) r = mid; // mid可能是第一个满足条件的位置,保留它
else l = mid + 1; // mid已经确定不满足,排除它
}
return l; // 第一个>=x的下标;返回n表示不存在
}
易错。 查“第一个≥x”与“最后一个≤x”的边界不同;有重复元素时找到任意等于x的位置不等于找到首位置。对答案二分要先证明可行性单调,不能看到“最大最小”就盲用。
变式。 数组 [1,2,2,4],第一个≥2、最后一个≤2的0基下标分别是多少?答:1、2。
对应:S2-8。
F36【补充,5分钟】网络、编码与计算机常识
本节是考前补充,所给90道选择题没有系统覆盖网络,不把它冒充真题高频统计。
网络分工。 IP负责地址与分组转发;TCP提供有连接、可靠、有序的字节流;UDP提供数据报传输,不自行保证可靠、有序,适合由应用按需求处理的场景。DNS把域名解析到相应记录;HTTP是应用层协议,HTTPS通常是在TLS保护下的HTTP。浏览器与服务器不是靠文件后缀来决定网络层协议。
地址与单位。 IPv4地址32位,IPv6地址128位;MAC地址是链路层标识,常见48位,不是IP地址。带宽10 Mb/s中的b是bit,理想传10 MB至少8秒(按同一十进制口径,不计开销)。
编码。 ASCII基本码使用7位编码128个字符,常存于1字节;Unicode规定字符码点,UTF-8是其编码形式之一,单个Unicode字符在UTF-8中不必固定占1或2字节。字符个数与UTF-8字节数不能混为一谈。
人物与信息。 香农1948年的工作奠定信息论基础。等可能N种结果的信息量为log₂N比特;一般信息熵 H=−Σp_i log₂p_i,越不确定平均信息量越大。冯·诺伊曼常与存储程序体系联系;图灵常与图灵机、可计算性联系。
变式。 8种等可能符号,每次符号信息量?答:3 bit。 100 Mb/s理论传125 MB至少多久?答:10秒,不计协议开销。
对应:S9-15;其余为补充。
F37【临场方法】读题限定词与构造反例
选择题优先圈出“最坏/平均”“至多/至少”“有序/无序”“连通/非连通”“可重复/互异”“恰好/不超过”“包括根/不包括根”“边数/删法数”。这些词往往决定公式。
对于“一定成立”,找一个反例即可推翻;“可能成立”只需构造一个例子;“最大值”既要给出可达方案,又要排除更大值。复杂度题先写计数式,递归题先写递推式,栈题画栈,哈希题画地址表,图题标访问顺序和距离。
资料中有原图缺失时,不背未经验证的数字。 本批选择题缺图的是S2-15、S5-13、S8-15;能复习相应方法,但不能凭现有文字保证原答案正确。
变式。 “偶数个结点的树一定有两个重心”如何1秒反驳?答:4点星形树只有中心一个重心。 “度全偶就一定有覆盖全部边的欧拉回路”如何反驳?答:两个互不连通的三角形。
程序专题:S1与S2的10段程序
本部分覆盖 S1—S2 中全部 10 组程序。来源编号只定位你提供的材料,不据此推定年份。带“修订”的代码是为消除材料中的缺漏、类型错误或越界所作的教学版本;不能把修订后的行为冒充原程序行为。做阅读题时依次写下:输入约束→变量含义→循环不变量→输出意义→手算最小样例→复杂度。
PR01|排列 DFS、剪枝与恢复现场
来源:S1/T16。必会程度:高。 功能是统计 1 到 n 的排列,禁止相邻位置出现“后一个数=前一个数+1”。它不是禁止两个相邻数差的绝对值为 1:2,1 允许,1,2 不允许。dfs(k) 入口时,p[1..k-1] 已合法填好,flag 恰好标记这些数;这就是递归不变量。一个分支选用 i,递归回来必须释放 i,因为兄弟分支应看到相同的初始状态。p[k] 不必清零:下一次使用前会重新赋值;只有 flag 控制“是否已用”。短路求值保证 k=1 时不检查前一个位置。
原题结论与推导: T16-1 对,n=3 的合法排列为 132、213、321。T16-2 对,k 从 1 递增至 n+1。T16-3 错,删除释放标记会破坏兄弟分支。T16-4 A,n=4 时 11。T16-5 D,p 初值无影响。T16-6 C,删去判重后允许重复,n=3 有 16 个序列。可用结尾数计数验证:长度 1 的计数为 (1,1,1),长度 2 为 (3,2,2),长度 3 为 (7,4,5),共 16。
复杂度与边界: 可用 O(n·n!) 作直接安全上界,栈与标记 O(n)。这不是多项式算法;数组开 27 并不意味着 n=26 能在比赛时限内算完,int ans 也可能溢出。数组 1 开始使用,n 必须小于 27。下面保留原逻辑与类型,实际扩展需另定计数范围。
#include <algorithm>
#include <cstdio>
#include <cstring>
bool flag[27]; // 全局数组默认全 false:尚未使用
int n, p[27], ans = 0; // p 保存当前排列;ans 为合法排列数
void dfs(int k) {
if (k == n + 1) { // 所有位置均已合法填完
++ans;
return;
}
for (int i = 1; i <= n; ++i) {
if (flag[i]) continue; // 保证每个数只出现一次
if (k > 1 && i == p[k - 1] + 1) continue; // 排除递增 1 的邻接
p[k] = i;
flag[i] = true; // 做选择
dfs(k + 1); // 填下一个位置
flag[i] = false; // 撤销选择,恢复兄弟分支的入口状态
}
}
int main() {
scanf("%d", &n);
dfs(1); // 从第 1 个位置开始
printf("%d\n", ans);
return 0;
}
变式 1: 若禁止相邻数差的绝对值为 1,n=3 有多少种?答:0;合法邻接只剩 1↔3,数 2 无法连接到其他数。变式 2: 原条件下 n=5?答:53;容斥为 Σ(-1)^j C(4,j)(5-j)! =120-96+36-8+1。把选中的禁邻接 i→i+1 粘成块,即使相连成链也仍减少 j 个自由块。变式 3: 删除回溯释放,n=3 输出?答:1;得到 132 后三个数全被标记,其他分支无法继续。
PR02|两个鸡蛋、最坏情况与递减分段
来源:S1/T17。必会程度:中高。 k 是首次会碎的楼层,check(h) 当且仅当 h≥k 时损失一个蛋;检查次数和碎蛋次数是两回事。guess1 从 1 查到 k,恰好检查 k 次且只碎一个。guess2 先找满足 w(w+1)/2≥n 的最小 w,用 w,w−1,… 的间隔试第一个蛋。一旦碎,使用第二个蛋线性扫该段内部。若第 r 次试段端点时碎,段长至多 w−r+1,内部再试至多 w−r 次,总计至多 w。
审校后的原题答案: 对、错、对、B、C、A。n=6 时 w=3,实际检查楼层是 3→5→4,确定 k=5,共 3 次;材料把 w 写成 4、过程写成 4→6→5 是错的。n=100 的两种原程序最坏次数为 100、14。第二种并非对每个 k 都更少:k=1、n>1 时,第一种只用 1 次,第二种通常用 2 次。原程序复杂度为 O(√n)、额外空间 O(1),不是 O(log n)。
别混淆“该程序次数”与“理论最优次数”: 已知 k∈[1,n] 时,最高层必碎,可由排除法确定而不实际检查。于是 w 次、两个蛋最多区分 1+w(w+1)/2 个阈值,而不是只算三角数;例如 n=7 理论上 3 次可解,原代码取 w=4。若另有“所有 n 层都不碎”这一候选,传统三角数覆盖 n 层的公式才正好对应。原程序能正确求 k,但其“绝对最优”表述需注明模型。末段被 min 截短时会重查已知安全楼层,但仍不影响正确性和 w 次上界。
#include <algorithm>
#include <cstdio>
int cnt_broken = 0, cnt_check = 0, n, k;
bool check(int h) {
printf("now check:%d\n", h); // 原程序会输出每次试验轨迹
++cnt_check;
if (cnt_broken == 2) { // 已无蛋,不能再取得有效试验结果
printf("You have no egg!\n");
return false;
}
if (h >= k) { ++cnt_broken; return true; }
return false; // 低于阈值,可以继续使用该蛋
}
bool assert_ans(int h) {
if (h == k) {
printf("You are Right using %d checks\n", cnt_check);
return true;
}
printf("Wrong answer!\n");
return false;
}
void guess1(int n) {
for (int i = 1; i <= n; ++i)
if (check(i)) { assert_ans(i); return; } // 首次碎就是阈值
}
void guess2(int n) {
int w = 0;
for (w = 1; w * (w + 1) / 2 < n; ++w) ; // 原代码:找三角数上界
for (int ti = w, nh = w; ; --ti, nh += ti, nh = std::min(nh, n)) {
// 逗号表达式从左到右:先减间隔,再增加楼层,再截至 n
if (check(nh)) {
for (int j = nh - ti + 1; j < nh; ++j)
if (check(j)) { assert_ans(j); return; }
assert_ans(nh); // 段内更低层均不碎,端点就是答案
return;
}
}
}
int main() {
scanf("%d%d", &n, &k);
int t; scanf("%d", &t);
if (t == 1) guess1(n); else guess2(n);
return 0;
}
变式 1: n=15、k=11、t=2,检查序列?答:5、9、12、10、11,共 5 次。变式 2: 为什么不能直接普通二分?答:连续两次检查都碎后没有蛋,尚有多个候选时无法继续;普通二分未计入试验资源。变式 3: 推广 e 个蛋,w 次覆盖楼层数满足什么?答:F(w,e)=F(w-1,e-1)+1+F(w-1,e),碎与不碎分两侧,F(w,0)=F(0,e)=0;这是允许另有“顶层也不碎”的传统楼层覆盖模型。
PR03|折半搜索、快速幂与带重数的配对
来源:S1/T18。必会程度:中高。 原问题是数有序整数解:每个 xi∈[1,m],满足 Σ ki·xi^pi=0。两个变量取值交换通常算不同解。DFS 的 v 是已处理变量的贡献;把变量分两半,各列所有可能的和,再找相加为 0 的配对。mpow 用指数二进制展开,最低位为 1 时乘入当前底数,然后指数右移、底数平方。ans1 排序后压缩相等值,cntans1 保留频数;若只保留不同和而丢频数,会漏解。ans2 不压缩,每个重复项会单独贡献一次。
双指针不变量:ans2 从大到小,则目标 −ans2[i] 从小到大,故 ans1 的 las 只需向右移动。删 ans2 排序后此单调性消失。原题答案依次为:错、对、对、B(8)、D、D。勾股数据的无序直角边组合是 (3,4,5)、(6,8,10)、(9,12,15)、(5,12,13),两条直角边交换,得到 8 个有序解。
令 M=m^ceil(n/2)、P 为最大指数,m≥2 时可写时间 O(M(log M+log(P+1))),空间 O(M+n);材料使用 n/2 是约略写法,n 奇数时最大一半是向上取整。m=1 时只有一条搜索链,时间还包含 O(n log(P+1))。原代码中幂、单项、部分和都是 int,ans 才是 long long;仅将最终答案设为 long long 并不能防止中间溢出,快速幂最后一次无用平方也可能溢出。下面保留原代码数值类型,阅读结论以计算不溢出为前提。
#include <algorithm>
#include <cstdio>
#include <vector>
int n, m;
std::vector<int> k, p, ans1, ans2, cntans1;
int cnt1, cnt2; // 两半枚举出的项数,含重复和
int mpow(int x, int e) {
int ans = 1;
for (; e; e >>= 1, x = x * x)
if (e & 1) ans *= x; // 将指数二进制中的当前一位乘入
return ans;
}
void dfs(std::vector<int>& out, int& cnt, int l, int r, int v) {
if (l > r) { ++cnt; out.push_back(v); return; } // 保存一组部分和
for (int x = 1; x <= m; ++x)
dfs(out, cnt, l + 1, r, v + k[l] * mpow(x, p[l]));
}
int main() {
scanf("%d%d", &n, &m);
k.resize(n + 1); p.resize(n + 1);
for (int i = 1; i <= n; ++i) scanf("%d%d", &k[i], &p[i]);
dfs(ans1, cnt1, 1, n >> 1, 0); // 空半区间也产生一个和 0
dfs(ans2, cnt2, (n >> 1) + 1, n, 0);
std::sort(ans1.begin(), ans1.end());
int newcnt1 = 1;
cntans1.push_back(1);
for (int i = 1; i < cnt1; ++i) {
if (ans1[i] == ans1[newcnt1 - 1]) ++cntans1[newcnt1 - 1];
else { // 将新的不同值写到压缩前缀中,并记录初始频数 1
ans1[newcnt1++] = ans1[i];
cntans1.push_back(1);
}
}
cnt1 = newcnt1; // 之后只访问压缩后的前缀
std::sort(ans2.begin(), ans2.end());
int las = 0;
long long ans = 0;
for (int i = cnt2 - 1; i >= 0; --i) {
while (las < cnt1 && ans1[las] + ans2[i] < 0) ++las;
if (las < cnt1 && ans1[las] + ans2[i] == 0)
ans += cntans1[las]; // 匹配值的所有出现均为解
}
printf("%lld\n", ans);
return 0;
}
变式 1: 两个变量 x−y=0,均在 [1,3],输出?答:3,解为 (1,1)、(2,2)、(3,3)。变式 2: 两半和分别为 [−2,−2,1]、[2,2,−1],零和配对数?答:2×2+1×1=5。变式 3: 目标和改成 T,只需怎样改?答:匹配条件改成两和等于 T,移动条件改成两和小于 T;两边仍须有序。
PR04|分层状态最短路:至多一条边免费
来源:S1/T19。必会程度:高。 一个顶点应拆成“没用优惠”和“已用优惠”两个状态,d[u][0/1] 保存到该状态的最短距离。普通过边保持层数,费用加 w;免费过边由 0 层到 1 层,费用加 0。状态图边权仍非负,所以用 Dijkstra。不能只保存 d[u]:到同一点较便宜但已用优惠的路线,与较贵却保留优惠的路线,未来潜力不同,二者都要保留。
五空:A、B、B、C、C,内容分别为 0、d[u][used]、d[v][used]、d[u][0]、min(d[t][0],d[t][1])。起点初始未用优惠;弹出的距离比当前记录大则为过期堆元素;普通转移比较目标同层距离;免费转移不加边权。终点取两层最小,是因为题意“至多”一次,特别 s=t 时空路径费用 0。
复杂度与边界: 2n 个状态、O(m) 条状态边,二叉堆时间常写 O((n+m)log n),更一般堆元素界可写 O((n+m)log(n+m));空间 O(n+m)。负边权不能直接用这份 Dijkstra。原程序不规定不可达输出:不可达时原样输出 INF,不能自行声称输出 −1。d 使用 long long,但还应确保距离上界小于 INF。
#include <algorithm>
#include <functional>
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
const long long INF = 1000000000000000000LL;
struct Edge { int to, weight; }; // 邻接边终点和非负权重
struct State {
long long dist; int u, used_freebie;
bool operator>(const State& b) const { return dist > b.dist; }
};
int main() {
int n, m, s, t;
cin >> n >> m >> s >> t;
vector<vector<Edge>> adj(n + 1);
for (int i = 0; i < m; ++i) {
int u, v, w; cin >> u >> v >> w;
adj[u].push_back({v,w}); adj[v].push_back({u,w}); // 无向边
}
vector<vector<long long>> d(n + 1, vector<long long>(2, INF));
priority_queue<State, vector<State>, greater<State>> pq; // 小根堆
d[s][0] = 0; pq.push({0,s,0}); // ① 未使用免费次数
while (!pq.empty()) {
State cur = pq.top(); pq.pop();
int u = cur.u, used = cur.used_freebie;
if (cur.dist > d[u][used]) continue; // ② 忽略过期状态
for (const auto& edge : adj[u]) {
int v = edge.to, w = edge.weight;
if (d[u][used] + w < d[v][used]) { // ③ 正常付费走边
d[v][used] = d[u][used] + w;
pq.push({d[v][used],v,used});
}
if (used == 0 && d[u][0] < d[v][1]) { // ④ 此次免费
d[v][1] = d[u][0];
pq.push({d[v][1],v,1});
}
}
}
cout << min(d[t][0],d[t][1]) << endl; // ⑤ 至多使用一次
return 0;
}
变式 1: 三点边权 1–2=5、2–3=7、1–3=20,从 1 到 3 的最小费用?答:0,直接把 20 的边免费;“先求普通最短路再免去其最大边”会错误得到 5。变式 2: 最多 k 次免费?答:状态 d[u][c],0≤c≤k;正常走留在 c,免费走到 c+1,终点在所有 c 中取最小。变式 3: 必须恰好一次?答:只输出 d[t][1];s=t 时不再能直接用空路径满足次数条件。
PR05|组合编码、测试集合与二进制解码
来源:S1/T20。必会程度:中。 n 条生产线恰有一条坏线,预先设计 w 次集合测试;每条线对应长 w 的 01 码,第 i 位为 1 就把这条线放入第 i 次测试。于是坏线的码恰好等于测试结果。限制最多 k 次阳性,相当于每个码最多 k 个 1。可区分数量为 Σ(t=0..min(w,k)) C(w,t),需要这个数量至少为 n。零码也能分配:已保证恰有一条坏线,全阴性就定位零码生产线;若允许“没有坏线”,必须额外保留一个结果状态。
五空:B、B、D、A、B。 内容依次是 count_patterns(w,k)<n、prev_permutation(bits.begin(),bits.end())、code[j][i]==1、(signature>>i)&1、code[j]==sig_bits。初始码 111…000 已是同类位串的最大字典序,故要用上一个排列;若从 000…111 起步才使用 next_permutation。code[生产线][测试] 和 plan[测试] 的维度要对应。
源码缺口: test_subset 只有声明,缺实现,不能单独链接运行。下面保留其接口,明确这是“需环境提供测试接口”的完整题目骨架。原 w 从 1 开始,n=1 的理论最少测试次数实际为 0;k=0、n>1 无解,原循环会一直找下去。实际签名为 int,还要限制 w 小于其可安全移位位宽;组合数中间乘积也须防溢出。生成与建表 O(nw),空间 O(nw);原 comb 为 O(t),逐个试 w 的计数部分上界 O(w·min(w,k)^2)。
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
long long comb(int w, int i) {
if (i < 0 || i > w) return 0;
long long res = 1;
for (int t = 1; t <= i; ++t)
res = res * (w - t + 1) / t; // 每步组合数为整数;注意乘法溢出
return res;
}
long long count_patterns(int w, int k) {
long long total = 0;
for (int t = 0; t <= min(w,k); ++t) total += comb(w,t);
return total; // 至多 k 个 1 的所有码字
}
int test_subset(const vector<vector<int>>& plan); // 外部测试接口,原文未实现
int solve(int n, int k) {
int w = 1;
while (count_patterns(w,k) < n) ++w; // ① 容量不足则增加测试位
cout << w << endl;
vector<vector<int>> code(n, vector<int>(w,0));
int idx = 0;
for (int ones = 0; ones <= k && idx < n; ++ones) {
vector<int> bits(w,0);
fill(bits.begin(),bits.begin()+ones,1); // 此组最大字典序
do {
for (int b = 0; b < w; ++b) code[idx][b] = bits[b];
++idx;
if (idx >= n) break;
} while (prev_permutation(bits.begin(),bits.end())); // ② 枚举同重量码
}
vector<vector<int>> plan(w);
for (int i = 0; i < w; ++i)
for (int j = 0; j < n; ++j)
if (code[j][i] == 1) plan[i].push_back(j); // ③ 建第 i 组测试
int signature = test_subset(plan);
vector<int> sig_bits(w,0);
for (int i = 0; i < w; ++i)
if ((signature >> i) & 1) sig_bits[i] = 1; // ④ 取结果第 i 位
for (int j = 0; j < n; ++j)
if (code[j] == sig_bits) return j; // ⑤ 匹配完整码字
return -1; // 教学补充:若接口返回非法结果,避免非 void 函数无返回
}
int main() {
int n,k; cin >> n >> k;
cout << solve(n,k) << endl;
return 0;
}
变式 1: n=7、k=2,最少测试次数?答:3,因为 w=2 只有 4 个码,w=3 有 1+3+3=7 个。变式 2: k=1,n≥2 时需要多少次?答:n−1,每次至多一个阳性的码共有 w+1 个。变式 3: 若 k≥w,容量多少?答:2^w,回到普通二进制编码,需要 ceil(log2 n) 次。
PR06|位运算化简与有深度限制的快速排序
来源:S2/T16。必会程度:高。 logic(x,y)=(x&y)|(x^y)|(~x&y) 等于 x|y:前两项已经覆盖了“该位至少有一个 1”的情况,第三项不增加新位。generate 先按位或,再 % (b+1),不能漏取模。递归采用双指针划分,不是一次划分就保证内部全部有序。枢轴 x 是中间元素的值的副本,之后原位置发生交换,x 仍不变。循环维护已越过的左侧≤x、右侧≥x;扫描停下后交换错侧元素。
校正答案: T16-1 对,d≥b 足够完成排序。T16-2 应为对(A):输入 5 5 1,生成 [5,5,1,1,5],x=1,右指针先从 4 移到 3;两次交换得到 [1,5,1,5,5]、[1,1,5,5,5]。材料漏做右侧扫描,错误判成错。T16-3 错,一般不是 O(b)。T16-4 B,按位或。T16-5 C,充分排序后最大值是 95。
复杂度: 每一递归层所有不交区间的扫描和 O(b),最多 d 层,故 O(b·min(d,b)) 是安全上界;充分且较平衡的划分为 O(b log b),最坏为 O(b²)。d 是变量时不能把它偷偷当常数。额外递归栈 O(min(d,b))。原数组 c[10000] 要求 b 不超过容量;全局 d 数组被 main 中局部变量 d 遮蔽,实际传入的是局部深度。
#include <iostream>
#include <cstdlib>
#include <algorithm>
using namespace std;
int n, d[10000]; // 原全局变量,main 中并未使用
int logic(int x, int y) {
return (x & y) | ((x ^ y) | (~x & y)); // 等价于 x | y
}
void generate(int a, int b, int c[]) {
for (int i = 0; i < b; ++i) c[i] = logic(a,i) % (b+1);
}
void recursion(int depth, int* c, int l, int r) {
if (depth <= 0 || l >= r) return; // 深度耗尽或区间不足两个元素
int i=l,j=r,x=c[(l+r)/2]; // x 是值副本,不随交换改变
while (i <= j) {
while (c[i] < x) ++i; // 找到左侧不小于枢轴的元素
while (c[j] > x) --j; // 找到右侧不大于枢轴的元素
if (i <= j) { swap(c[i],c[j]); ++i; --j; }
}
recursion(depth-1,c,l,j); // 处理左边剩余区间
recursion(depth-1,c,i,r); // 处理右边剩余区间
}
int main() {
int a,b,d; cin >> a >> b >> d; // 局部 d 遮蔽全局同名数组
int c[10000];
generate(a,b,c);
recursion(d,c,0,b-1);
for (int i=0;i<b;++i) cout << c[i] << " ";
return 0;
}
变式 1: 输入 5 5 0?答:5 5 1 1 5,深度为零,不做划分。变式 2: 所有元素都等于 x,while 会死循环吗?答:不会;虽然内层扫描不移动,交换块仍执行 i++、j--。变式 3: (x&y)^(x^y) 等于什么?答:x|y,因为“同为 1”与“恰一位为 1”不会在同一位重叠。
PR07|子序列计数 DP、二进制值与倒序更新
来源:S2/T17。必会程度:高。 solve2 的掩码枚举“选哪些下标”,保持原顺序拼接;它统计长度≤m 的所有子序列数值之和,允许前导零,相同字符串由不同下标组成仍分别计数。solve1 中 dp[j] 统计二进制值为 j、首位为 1 的选法数;dp[0]=1 代表空序列,禁止把 0 接到空序列后,因此不计前导零。k=2j+bit 是在低位追加字符。倒序遍历 j,保证当前字符只能使用一次;若正序,更新出的更大状态可能立刻被再次转移。
严重源码问题: 原 dp[1<<10] 只有 1024 个元素,但 j 遍历到 1023,k=2j+bit 可达 2047;即使 dp[j]=0,执行 dp[k]+=0 也已越界。不能据此宣称原代码 m=10 时有确定输出。下面是明确修订版:增添 k<(1<<m) 条件,保留有意义状态,n、m 的移位和结果仍须在相应类型范围内。
在修复越界后的算法意义下,答案为: 第 1 题对(题面的 solve 指 solve1);第 2 题应判错,输出顺序是 23、32,材料把先后写反;第 3 题对;选择题 B(11)、C(665)、C(2059)。两结果相等需没有正贡献的前导零子序列,即不存在某个 0 后面还有 1,因此串形如 111…000,n=10 共 11 种,不是“完全不能有以零开头的子序列”。全 1 串且 m=n 时总和 ΣC(n,r)(2^r−1)=3^n−2^n,n=6 得 665,n=10 得 58025。n=8 最大差值为 2059,由 01111111 达到;已枚举全部 256 个串验证。
#include <iostream>
#include <string>
using namespace std;
int n,m;
string s;
int dp[1 << 10]; // 修订版约定 0<=m<=10
int solve1() {
int lim=1<<m;
for (int j=0;j<lim;++j) dp[j]=0;
dp[0]=1; // 空序列是开始选取的来源
for (int i=0;i<n;++i)
for (int j=lim-1;j>=0;--j) { // 倒序,避免本轮重复使用字符
int k=(j<<1)|(s[i]-'0');
if (k<lim && (j!=0 || s[i]=='1')) // 修订:原稿遗漏范围判断
dp[k]+=dp[j];
}
int ans=0;
for (int j=1;j<lim;++j) ans+=j*dp[j]; // 数值乘其出现次数
return ans;
}
int solve2() {
int ans=0;
for (int mask=0;mask<(1<<n);++mask) { // 适用于 n 很小的暴力枚举
int num=0,cnt=0;
for (int j=0;j<n;++j)
if (mask&(1<<j)) { // 选下标 j,保持原有相对顺序
num=num*2+(s[j]-'0'); ++cnt;
}
if (cnt<=m) ans+=num;
}
return ans;
}
int main() {
cin >> n >> m >> s;
cout << solve1() << endl; // 无前导零版本先输出
cout << solve2() << endl; // 允许前导零版本后输出
return 0;
}
复杂度:solve1 为 O(n·2^m),空间 O(2^m);solve2 为 O(n·2^n),额外空间 O(1)。变式 1: n=m=3、s=011,两个输出?答:5、10;首位为 1 的非零子序列为 1、1、11,前导零再贡献 01、01、011。变式 2: m=1 时两个输出是否总相等?答:是,都等于串中 1 的个数,单字符 0 的贡献为零。变式 3: 子串与子序列能互换吗?答:不能;子串要求下标连续,本题任意选择下标。
PR08|完全二叉树、字符串拼接哈希与去重
来源:S2/T18。必会程度:中高。 用编号 i 的素性作为节点 01 标签,2i、2i+1 是左右儿子。h[i] 表示该节点子树中序标签串的双哈希和长度。A+B 不是数值加法,而是拼接:hash(AB)=hash(A)·B^len(B)+hash(B) mod P。倒序处理节点,使儿子的哈希在父节点之前算好。随后按哈希三元组排序,数有多少个不同三元组。
原题答案:对、错、对、C、A(83)、C(10)。 n=10 的中序编号是 8,4,9,2,10,5,1,6,3,7,素性串为 0001010011,以 2 为底的第一哈希就是 83(未超过模数)。n=16 的不同标签串共 10 种:长度 1 的 0,1,长度 2 的 00,长度 3 的 001,010,011,长度 4 的 0000,长度 7 的 0011010,长度 8 的 00001011,长度 16 的 0000101100011010。
不能无限推广的结论: 对本题由完全二叉树数组形成的子树,节点数确定其完全二叉树形状,故长度+中序标签串可以描述该子树。但对于任意二叉树,仅中序序列不能确定树形,需要空儿子标记或结构哈希。双哈希降低碰撞机会,却不构成数学上的绝对无碰撞保证;operator< 只是让相同三元组相邻,并非原字符串的字典序比较。埃筛 O(n log log n)、哈希构造 O(n)、排序 O(n log n),总时间 O(n log n)、空间 O(n)。
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn=1000005;
const int P1=998244353,P2=1000000007,B1=2,B2=3,K1=0,K2=13;
int n,p1[maxn],p2[maxn];
bool p[maxn]; // p[i] 表示 i 是否为质数
struct H {
int h1,h2,l;
H(int b=0) { h1=K1+b; h2=K2+b; l=1; } // 一个标签字符的哈希
H operator+(const H& other) const {
H res;
res.l=l+other.l;
res.h1=(1LL*h1*p1[other.l]+other.h1)%P1;
res.h2=(1LL*h2*p2[other.l]+other.h2)%P2; // 左串整体左移右串长度
return res;
}
bool operator==(const H& other) const {
return h1==other.h1 && h2==other.h2 && l==other.l;
}
bool operator<(const H& other) const { // 排三元组,以便相等值相邻
if(h1!=other.h1) return h1<other.h1;
if(h2!=other.h2) return h2<other.h2;
return l<other.l;
}
} h[maxn];
void init() {
for(int i=2;i<=n;++i) p[i]=true;
for(int i=2;i*i<=n;++i)
if(p[i]) for(int j=i*i;j<=n;j+=i) p[j]=false; // 埃筛
p1[0]=p2[0]=1;
for(int i=1;i<=n;++i) {
p1[i]=1LL*p1[i-1]*B1%P1;
p2[i]=1LL*p2[i-1]*B2%P2; // 预处理幂,拼接时 O(1)
}
}
void solve() {
for(int i=n;i>=1;--i) { // 儿子编号更大,先于父亲处理
h[i]=H(p[i]);
if(2*i+1<=n) h[i]=h[2*i]+h[i]+h[2*i+1]; // 左、根、右
else if(2*i<=n) h[i]=h[2*i]+h[i];
}
cout<<h[1].h1<<endl;
sort(h+1,h+n+1); // 此后 h 的下标不再表示原节点
int cnt=0;
for(int i=1;i<=n;++i)
if(i==1 || !(h[i]==h[i-1])) ++cnt;
cout<<cnt<<endl;
}
int main() { cin>>n; init(); solve(); return 0; }
变式 1: n=3 的第一行、第二行?答:5、2。中序标签为 101;两片叶子标签均为 1,另有整树,共两类。变式 2: 为什么要把长度纳入相等判断?答:第一哈希里 1 与 01 数值相同;长度可区分前导零导致的歧义。第二哈希的字符偏移也能帮助区分,但仍不能省去对整体定义的理解。变式 3: 把合并顺序改为根+左+右,含义?答:先序标签串;倒序计算子树的处理顺序并不因此变成“后序字符串”。
PR09|两个有序数组的第 k 小和:计数后对答案二分
来源:S2/T19。必会程度:高。 N² 个和按“下标对”计重,不能去重。对阈值 X,固定 A[i],数有多少 B[j]≤X−A[i];upper_bound 返回首个大于阈值的位置,与首地址相减就是数量。总数量 C(X) 随 X 单调不减,答案是最小满足 C(X)≥K 的 X。若数量<K,需要增大答案;否则先记录候选,再继续向左找。
原材料的五个备选答案均为 A,但代码发生了类型拼接错误: 它把 upper_bound 写成返回 int、第二参数为 int,填空却要求 an-a 与 a+l(指针运算),调用又给 n 而不是 b+n。照抄无法编译。下面按备选空的指针意图统一修复,另将 k、rank 改为 long long,因为 N² 可达 10¹⁰;中点改为 l+(r-l)/2,避免两个近 2×10⁹ 的边界相加溢出。这是教学修订,不能当作缺失原稿的还原证据。
五空的实质分别是:长度 an-a;条件 a[mid]>ai;返回 a+l;答案上界 a[n-1]+b[n-1];右移条件 get_rank(mid)<k。下界 0 依赖元素非负。总时间 O(n log n log V),V 为答案取值范围;额外存储 O(n),一次计数对每个 A 做一次 O(log n) 二分。若已排序,可以双指针将一次计数优化为 O(n),那是补充算法,不是原程序的复杂度。
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn=100005;
int n,a[maxn],b[maxn];
long long k; // 修订:名次最多 n²,超出 int
int* upper_bound(int* a,int* an,int ai) { // 修订:末指针、返回指针
int l=0,r=int(an-a); // ① 搜索 [0,长度),也允许返回末尾
while(l<r) {
int mid=l+(r-l)/2;
if(a[mid]>ai) r=mid; // ② 找首个严格大于 ai 的位置
else l=mid+1;
}
return a+l; // ③ 指针差才是元素个数
}
long long get_rank(int sum) { // 修订:计数用 64 位
long long rank=0;
for(int i=0;i<n;++i)
rank+=::upper_bound(b,b+n,sum-a[i])-b; // 修订:传末指针
return rank;
}
int solve() {
int l=0,r=a[n-1]+b[n-1],ans=r; // ④ 合法最大和保证可行
while(l<=r) {
int mid=l+(r-l)/2; // 修订:防 l+r 溢出
if(get_rank(mid)<k) l=mid+1; // ⑤ 和过小,计数未达到 k
else { ans=mid; r=mid-1; }
}
return ans;
}
int main() {
scanf("%d%lld",&n,&k); // 修订:匹配 long long 格式
for(int i=0;i<n;++i) scanf("%d",&a[i]);
for(int i=0;i<n;++i) scanf("%d",&b[i]);
printf("%d\n",solve());
return 0;
}
变式 1: A=[1,2],B=[1,3],K=3?答:4,四个和排序为 2,3,4,5。变式 2: A=B=[1,1],K=4?答:2,不是没有第 4 项;四个不同下标对都产生 2。变式 3: 若元素可负,如何改?答:下界设 A[0]+B[0],并重新核对 sum-A[i]、边界和、答案的数据范围。
PR10|严格次短路:原稿缺损与可靠修订
来源:S2/T20。必会程度:高,但本份材料不能直接背代码。 严格次短路要求长度大于最短路;多条同长度最短路并不成为严格次短。每个顶点保留两个不同的最小距离 d1<d2。沿边产生候选 nd:若 nd<d1,将旧 d1 作为次短候选,再更新 d1;若 d1<nd<d2,更新 d2;nd=d1 时跳过。非负边权下,可在状态图上用 Dijkstra。题目还应区分允许重复点边的“游走”和不重复点的“简单路径”;下面求允许重复的严格次短游走,不冒充一般第 2 短简单路径算法。
原稿审校: 材料给答案 A、A、B、A、A,②③⑤却不在展示代码中;输入和路径输出省略。展示代码先 upd(a,b,...) 覆盖 dis[b],再用 dis[b] “保存旧最短”已经太晚,会把新最短复制进第二层。head[a] 对第二层状态未映射回原顶点,且未排除等长候选。memset 还缺 <cstring>。因此这些字母只能视为材料声称的答案,不能认证“填后正确”。确定的局部原理仍是:大根堆存 (-距离,状态) 可模拟小根堆;按字节填 0x1f 得到 int 的 0x1f1f1f1f;第二层需映射到原节点邻接表。
下面给另行修订的完整教学程序,约定输入 n m s t,接着 m 条 u v w,编号 0..n−1、权非负、累计长度小于 INF。用不可变的路线记录保存每次改进的前驱:如果只记“前驱顶点”,该顶点后来被改进时,可能破坏当初次短路线的还原。优先队列时间 O((n+m)log(n+m)),路线记录及图 O(n+m) 量级(以有效松弛数计),输出长度另计。
#include <array>
#include <functional>
#include <iostream>
#include <queue>
#include <tuple>
#include <vector>
using namespace std;
using ll=long long;
struct Edge { int v; ll w; };
struct Record { int vertex, parent; ll dist; }; // 不可变的路线前缀
int main() {
int n,m,s,t; cin>>n>>m>>s>>t;
vector<vector<Edge>> g(n);
for(int i=0;i<m;++i) {
int u,v; ll w; cin>>u>>v>>w;
g[u].push_back({v,w}); // 本题为有向图
}
const ll INF=4000000000000000000LL;
vector<array<ll,2>> d(n,array<ll,2>{INF,INF});
vector<array<int,2>> id(n,array<int,2>{-1,-1});
vector<Record> rec;
using Item=tuple<ll,int,int>; // 距离、顶点、路线记录编号
priority_queue<Item,vector<Item>,greater<Item>> pq;
auto save=[&](int v,ll value,int parent,int layer) {
int r=int(rec.size());
rec.push_back({v,parent,value});
d[v][layer]=value; id[v][layer]=r;
pq.push({value,v,r});
};
save(s,0,-1,0); // 起点空路线,最短距离为 0
while(!pq.empty()) {
auto [du,u,r]=pq.top(); pq.pop();
if(r!=id[u][0] && r!=id[u][1]) continue; // 已被更优记录淘汰
for(auto e:g[u]) {
if(du>INF-e.w) continue; // 防溢出;约定有效答案小于 INF
ll nd=du+e.w;
if(nd<d[e.v][0]) {
// 先保存旧最短:它自动严格大于新最短且不大于旧次短
d[e.v][1]=d[e.v][0]; id[e.v][1]=id[e.v][0];
save(e.v,nd,r,0);
} else if(d[e.v][0]<nd && nd<d[e.v][1]) {
save(e.v,nd,r,1); // 严格大于最短,才能成为次短
}
}
}
if(d[t][1]==INF) { cout<<-1<<'\n'; return 0; }
cout<<d[t][1]<<'\n';
vector<int> path;
for(int r=id[t][1];r!=-1;r=rec[r].parent) path.push_back(rec[r].vertex);
for(auto it=path.rbegin();it!=path.rend();++it) cout<<*it<<' ';
cout<<'\n';
return 0;
}
变式 1: 0→1 有两条权 5 的平行边,另有一条权 7 的边,严格次短多长?答:7;两条 5 并列最短。变式 2: 仅有 0→1 权 2、1→0 权 3,从 0 到 1 的严格次短游走?答:7,路线 0→1→0→1;若要求简单路径则不存在。变式 3: 普通最短路更新成功时,旧最短为什么要留?答:它依然是一条合法路线,并可能恰好是第二小的不同长度;覆盖前不保存就丢失候选。
专项讲义:位运算、二分、DAG 与区间贡献
本章覆盖 S4 的3段阅读程序与 S3 的2段完善程序。P0 内容先读“本质”和“手算”;P1/P2 推导用于查漏。
C01 固定字长异或变换与整数提升〔P0;S4-阅读1〕
本质。 x ^= x << 6; x ^= x >> 8; 是固定字长上的位变换。异或满足同为0、异为1;同一数异或两次会消去。这里每一步赋回16位变量时会截去超出16位的部分,因此不能先按无限精度把两步都算完再截断。
#include <cstdint>
#include <iostream>
using namespace std;
uint16_t transform16(uint16_t x) {
// 先在足够宽的无符号数里计算,再转回16位:保留低16位。
x = static_cast<uint16_t>(uint32_t(x) ^ (uint32_t(x) << 6));
// 第二步必须使用第一步已经截断后的x。
x = static_cast<uint16_t>(uint32_t(x) ^ (uint32_t(x) >> 8));
return x;
}
int main() {
unsigned x; cin >> x; // 题设0 <= x <= 65535
cout << transform16(static_cast<uint16_t>(x)) << '\n';
}
此为显式字长的教学等价写法。原码 unsigned short 在题设中按16位理解。在常见32位 int 平台上,x << 6 的计算先经历整数提升,截断发生在复合赋值写回时;“移位表达式本身就只有16位”不是严格的 C++ 解释。改为32位参数后,中间高位可能右移回低16位,即使返回类型仍是 unsigned short,结果也可能改变。
手算表。 每一行写出赋值后的结果,不把十进制和位串混记。
| 输入 | 第一条赋值后 | 最终输出 |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 65 | 65 |
| 64 | 4160 | 4176 |
| 512 | 33280 | 33410 |
| 65535 | 63 | 63 |
| 32768 | 32768 | 32896 |
对输入32768,如果把函数参数改为32位无符号,第一步留下第21位;第二步使它进入第13位。最终即使转回16位,也是41088,与32896不同。
为什么非零不会变为零。 对 y=x XOR (x<<s),最低的s位保持不变,再由这s位逐块恢复上面的位,因此变换可逆;对右移异或则从最高位向下恢复。两个可逆变换复合仍可逆,且0映到0,所以只有0会输出0。不要误写成“只要异或了自己就等于0”,第二个操作数已经移位。
常考变式。 修改字长、只问第一行执行后、交换两步顺序、将异或换成或、求某一位、问输出是否一一对应。若移位量不小于提升后左操作数的位宽,则移位行为不合法;有符号溢出也不能当作可靠的循环取模。时间与空间均为 O(1)。
原题答案复核。 判断为对、错、对、错;单选为33410(B)、4160(D)。第二个单选问的是第一条赋值后,不是最终输出。
C02 约数和、筛法与交换求和〔P1;S4-阅读2〕
先认数学意义。 若 x=p^e * r 且p不整除r,则
sigma(x)=sigma(r)*(1+p+...+p^e)=sigma(r)*(p^(e+1)-1)/(p-1)。
原码前期 f[x] 表示最小质因子p,g[x] 表示这个质因子的最高整除幂p^e;后期同一个 f[x] 被覆盖为约数和。变量含义会在阶段之间变化,这是阅读题陷阱。
#include <algorithm>
#include <cmath>
#include <iostream>
#include <vector>
using namespace std;
long long solve1(int n) { // 题意需n>=1;n=0不能访问f[1]
vector<bool> unseen(n + 1, true);
vector<long long> f(n + 1), g(n + 1);
f[1] = 1;
for (int p = 2; 1LL * p * p <= n; ++p) {
if (!unseen[p]) continue; // 能进入者是质数
vector<int> powers;
for (long long q = p; q <= n; q *= p) powers.push_back(int(q));
reverse(powers.begin(), powers.end()); // 最高幂先抢占,保证g[x]=p^e
for (int q : powers) {
for (int x = q; x <= n; x += q) {
if (unseen[x]) {
unseen[x] = false;
f[x] = p; // 先记最小质因子
g[x] = q; // 再记其中该质因子的最高幂
}
}
}
}
for (int x = int(sqrt(n)) + 1; x <= n; ++x) {
if (unseen[x]) f[x] = g[x] = x; // 剩下的大质数
}
long long ans = 1;
for (int x = 2; x <= n; ++x) {
long long p = f[x], q = g[x];
// x/q < x,所以递增枚举时f[x/q]已经是约数和。
f[x] = f[x / q] * (q * p - 1) / (p - 1);
ans += f[x];
}
return ans;
}
long long solve2(int n) {
long long ans = 0;
for (int d = 1; d <= n; ++d)
ans += 1LL * d * (n / d); // d在1..n中出现为约数共floor(n/d)次
return ans;
}
int main() {
int n; cin >> n;
if (n < 1) return 0;
cout << solve1(n) << '\n' << solve2(n) << '\n';
}
为什么两种算法相同。 左边按“被除数”枚举,右边按“约数”枚举:
sum(x=1..n) sum(d|x) d = sum(d=1..n) d * floor(n/d)。
例n=5,五个约数和分别为1、3、4、7、6,总数21。删除 reverse 时4先被2标记,g[4]错误变为2,错误公式会得到9而不是7;n=4即可反驳“输出不变”。
复杂度。 筛法只对质数p做倍数循环,总工作量为 sum_p sum_e n/p^e = O(n log log n),并使用 O(n) 空间。不能写成“对所有整数的调和和也是 log log n”:对所有整数求和会是 O(n log n)。第二种算法 O(n) 时间、O(1) 额外空间。原范围n<=10^6中 d*(n/d)<=n,这个乘积即使用int也不溢出;推广范围后主动写 1LL 更稳妥。
变式。 约数个数用 tau(x)=tau(r)*(e+1);所有约数个数之和为 sum floor(n/d);所有约数平方和为 sum d*d*floor(n/d)。进一步按 floor(n/d) 相同的区间分组,可以用整除分块把部分求和降为 O(sqrt(n)),属于有余力再看的拓展。
原题答案复核。 判断错、错、对;复杂度选择D、B;n=5选择B。题设“自然数”若包含0,则源代码不覆盖该边界,复习时应明确n>=1。
C03 第 k 小数对距离:二分答案+双指针〔P0;S4-阅读3〕
从输出倒推。 在所有无序下标对 i<j 中,取 |a[i]-a[j]|,排序后找第k小。即使数值相等,只要下标对不同就分别计数。先排序把绝对差化成右值减左值;然后二分距离d,问“差不超过d的数对是否至少k个”。
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
bool enough(const vector<long long>& a, long long d, long long k) {
long long count = 0;
int left = 0;
for (int right = 0; right < int(a.size()); ++right) {
while (a[right] - a[left] > d) ++left;
// left..right-1共right-left个下标,与right组成合法数对。
count += right - left;
if (count >= k) return true; // 只判断阈值,可提前结束
}
return false;
}
long long kthDistance(vector<long long> a, long long k) {
sort(a.begin(), a.end());
long long lo = 0, hi = a.back() - a.front();
// 不变量:答案始终属于闭区间[lo,hi];hi肯定可行。
while (lo < hi) {
long long mid = lo + (hi - lo) / 2;
if (enough(a, mid, k)) hi = mid; // mid可能恰好是答案,保留
else lo = mid + 1; // mid不可行,连同左边排除
}
return lo;
}
int main() {
int n; long long k; cin >> n >> k;
vector<long long> a(n);
for (auto &x : a) cin >> x;
cout << kthDistance(a, k) << '\n';
}
前提n>=2且1<=k<=n(n-1)/2。双指针每个指针只向右走,单次判断 O(n),而不是 O(n²)。令 A=max(a)-min(a)+1,总复杂度 O(n log n+n log A),可写 O(n log(nA)),空间取决于排序实现;上面的按值传参版本额外复制 O(n)。原题使用 vector<int>&,不要把这份教学版复制成本反套到原题。
正确手算。 输入 5 7 2 -4 5 1 -3,排序应为 -4,-3,1,2,5,不是附件的 -4,1,2,3,5。十个差值排序为 1,1,3,4,4,5,5,6,8,9,第7小仍为5,结论碰巧没变。该负数样例与附件写出的 a_i>=1 也矛盾,按这段代码本身可算。另例 5 8 2 -5 3 8 -12 的第8小为14。
改代码题要先检查内存与不变量。
hi=mid改为hi=mid-1:套用了另一种二分模板,可能跳过答案。在单调阈值模型中输出可能正确,也可能比正确值小1,不保证始终正确。lo+(hi-lo)/2与(lo+hi)>>1:在本题非负、和不溢出的范围内等价;推广到大整数边界不能直接替换。- 把
while (a[i]-a[j]>m)的>改为>=:原解析只讨论了不等式严格性,却忽略当m=0时j会越过i,甚至越界。例如a=[1,2]、k=1,首轮m=0,i走到最后时j会继续走出数组。不能背成无条件选B。
如果要真正统计“严格小于d”的数对,应写 while (left < right && a[right]-a[left] >= d) ++left,并把二分可行上界扩到 max-min+1。整数距离的正确答案才是原第k小距离+1。仅改比较符号但不改边界,并不是这个正确算法。
变式。 第k大转为总数-k+1小;两数组元素和第k小改用另一种计数函数;问“恰好等于d”可用 count(<=d)-count(<=d-1);求满足距离限制的数对数则只运行双指针。读完一段陌生程序,先找“排序→单调check→二分”这一组合。
C04 DAG 路径计数与字典序第 k 小〔P1;S3-完善1〕
路径与字典序。 一条单顶点路径也计数。同一前缀下,“直接结束”排在继续走之前,例如 [0] < [0,1] < [0,1,3] < [0,2]。这里不是最短路,也不是按路径长度排序。
状态。 f[u] 是从u出发的非空路径数,有 f[u]=1+sum(u→v) f[v]。因为图无环,按逆拓扑序计算,后继状态都已完成。数字大于10^18时截断:k不超过10^18,只要区分“够不够大”即可,无需知道真实超大值。
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
const long long LIM = 1000000000000000000LL;
// 每组cand已按编号升序;k是当前候选集合内部的1起始排名。
int choose(const vector<int>& cand, const vector<long long>& f, long long& k) {
for (int u : cand) {
if (k <= f[u]) return u; // 落在u这一块,保留块内排名
k -= f[u]; // 跳过整块
}
return -1; // 合法k不应走到这里
}
int main() {
int n, m; long long k; cin >> n >> m >> k;
vector<vector<int>> edge(n);
vector<int> indegree(n);
while (m--) {
int u, v; cin >> u >> v;
edge[u].push_back(v); ++indegree[v];
}
vector<int> topo;
for (int u = 0; u < n; ++u) if (!indegree[u]) topo.push_back(u);
for (int i = 0; i < int(topo.size()); ++i) {
int u = topo[i];
for (int v : edge[u])
if (--indegree[v] == 0) topo.push_back(v); // 这里先减后判
}
if (int(topo.size()) != n) return 0; // 教学版对非DAG防护
vector<long long> f(n, 1);
for (int i = n - 1; i >= 0; --i) {
int u = topo[i];
for (int v : edge[u]) f[u] = min(LIM, f[u] + f[v]);
sort(edge[u].begin(), edge[u].end());
}
vector<int> all(n); iota(all.begin(), all.end(), 0);
int u = choose(all, f, k);
while (u != -1) {
cout << u << '\n';
if (k == 1) break; // 当前前缀本身就是答案
--k; // 排除“现在结束”的方案
u = choose(edge[u], f, k);
}
}
假定边表没有导致同一顶点序列被重复计数的平行边,或题目按边区分路径。若要求相同顶点序列只算一次且允许平行边,要先将每个出边表去重。题目是顶点序列排名时,不能忽略这个约定。
逐空说明。 原码在 --deg[v] 前判断,所以②是 deg[v]==1,与教学版先减后判0等价。① k<=f[u];③ min(f[u]+f[v],LIM);④ k>1;⑤ --k。答案 B、A、A、D、C。每次相加最多2×10^18,仍小于有符号64位上限,随后及时截断安全。
手算。 边0→1、0→2、1→2:f[2]=1,f[1]=2,f[0]=4。全体顺序是 [0]、[0,1]、[0,1,2]、[0,2]、[1]、[1,2]、[2]。第4条先落到0块,k从4减至3;跳过1块的2条,落到2块的第1条,得 [0,2]。
复杂度。 拓扑与计数 O(n+m),出边排序 O(sum deg(u)log deg(u)),选路径只沿一条DAG路径前进,其访问的出边表总长不超过m。教学版总时间 O(n+m+sum deg log deg),空间 O(n+m)。原码 next 按值接收vector,还会复制候选集合;首次给所有顶点排序有 O(n log n) 成本,不能漏掉。
变式。 改为到指定终点的路径数,初始化与合法终止点改变;只算至少一条边就不能直接每点+1;改为按边权距离排名必须换算法;字典序第k小字符串、括号序列、排列也用“按前缀计数→扣整块→缩到块内”的思路。若图有环,不能套拓扑DP。
C05 所有子数组最大值之和:分治、双指针与贡献法〔P1;S3-完善2〕
题意。 子数组必须连续;相同数值出现在不同下标区间仍是不同对象。[1,2,1,2]共10个非空子数组,最大值总和18。答案上界约 10^8*n(n+1)/2,n=10^5时约5×10^17,必须用64位。
分治拆成三类。 完全在左、完全在右、跨中点。前两类递归;跨中点固定左端i,右端分成“最大值由左侧提供”和“由右侧提供”两段,利用右侧前缀最大值的前缀和求和。
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
vector<int> a;
long long answer = 0;
void solve(int l, int r) { // 左闭右开[l,r),要求非空
if (r - l == 1) { answer += a[l]; return; }
int mid = l + (r - l) / 2;
vector<int> pre(a.begin() + mid, a.begin() + r);
for (int t = 1; t < int(pre.size()); ++t)
pre[t] = max(pre[t], pre[t - 1]); // 右侧从mid到mid+t的最大值
vector<long long> sum(pre.size() + 1, 0);
for (int t = 0; t < int(pre.size()); ++t) sum[t + 1] = sum[t] + pre[t];
int j = mid, leftMax = 0; // 原范围a[i]>=1,故0作为初值合法
for (int i = mid - 1; i >= l; --i) {
// 保留原题更新顺序:若a[i]不是新纪录,j原本已在正确位置。
while (j < r && a[j] < a[i]) ++j;
leftMax = max(leftMax, a[i]);
answer += 1LL * (j - mid) * leftMax; // 右端<j,左侧最大
answer += sum[r - mid] - sum[j - mid]; // 其余右端,用右前缀最大
}
solve(l, mid); solve(mid, r);
}
int main() {
int n; cin >> n; a.resize(n);
for (int& x : a) cin >> x;
if (n) solve(0, n);
cout << answer << '\n';
}
难点是②为什么可填 a[j]<a[i]。 保持不变量:上一次处理后,j是右侧第一个 a[j]>=旧leftMax 的位置,或等于r。如果a[i]不大于旧最大值,那么a[j]当然也不小于a[i],j不动仍正确。如果a[i]刷新最大值,就继续向右扫描直到第一个不小于新最大值的位置。j从不回退。附件解析曾绕回说应填旧max,这是错误的中间推理:while在更新max之前,填旧max会漏掉当前a[i]。
另一种更直观的等价写法,是先 leftMax=max(leftMax,a[i]),再 while(j<r && pre[j-mid]<leftMax) ++j;但不能只替换原题的一个比较式而忽略更新顺序。
逐空答案。 D、B、A、C、A。pre的局部下标t对应原数组mid+t;区间是[0,n),不是[0,n-1];1LL要放乘法之前。时间递推 T(n)=2T(n/2)+O(n),故 O(n log n)。每条递归链保留的临时vector长度形成几何级数,峰值辅助空间 O(n),递归栈 O(log n)。
手算跨中点。 对[1,2 | 1,2],右侧pre=[1,2],sum=[0,1,3]。i=1时左最大2,j由2走到3,贡献1×2+(3-1)=4;i=0时左最大仍2,j不动,再贡献4。左右两边内部各贡献5,所以总计18。
拓展:单调栈贡献法〔P2〕。 每个a[i]作为若干区间的“指定最大值”,贡献 a[i]*(i-L[i])*(R[i]-i)。一种不重复分配法:L是左边最近的严格更大元素,R是右边最近的大于等于元素,边界缺失取-1、n。重复最大值必须一边严格、一边非严格。例如[2,2]如果两边都找严格更大,会把[0,1]算两遍。
// 函数片段:需#include <vector>,并using namespace std。
long long sumMax(const vector<int>& a) {
int n = int(a.size());
vector<int> L(n), R(n), st;
for (int i = 0; i < n; ++i) {
while (!st.empty() && a[st.back()] <= a[i]) st.pop_back();
L[i] = st.empty() ? -1 : st.back(); // 左侧严格更大
st.push_back(i);
}
st.clear();
for (int i = n - 1; i >= 0; --i) {
while (!st.empty() && a[st.back()] < a[i]) st.pop_back();
R[i] = st.empty() ? n : st.back(); // 右侧大于等于
st.push_back(i);
}
long long ans = 0;
for (int i = 0; i < n; ++i) ans += 1LL * a[i] * (i - L[i]) * (R[i] - i);
return ans;
}
每个下标进栈、出栈至多一次,所以 O(n) 时间、O(n) 空间。求最小值之和则翻转比较方向;求极差之和等于最大值之和减最小值之和。原分治初始化0依赖正数范围,单调栈版本本身可以处理负数。
程序专题精讲:S6—S9
这部分按“读出程序在做什么—写出不变量—检查边界—推导填空—迁移变式”的顺序学习。P0 表示考前必须掌握,P1 表示高频进阶,P2 表示最后有余力再看。来源码对应用户材料;S6 自带 2022 标题、S8 自带 2021 标题,S7、S9 未凭印象推定年份。
**材料质量说明:**S8 有两段程序被原材料主动删节,还有若干答案与代码矛盾。以下明确区分“所给代码的注释版”“修正版”和“教学重写”;没有把缺失代码伪装成完整原题。考前应记住算法和可验证的推导,不背损坏文本中的选项字母。
O01|Sunday 字符串匹配:跳的是窗口后面的字符【P1】
来源:S6 阅读程序(1)。目标是在文本串 s 中找模式串 t 第一次出现的 0 基下标,找不到返回 -1。
知识点与不变量
设文本长度 n、模式长度 m。当前比较窗口是 s[i..i+m-1]。本次失败后看的是 s[i+m],即窗口右边紧挨着的字符 c。
- c 不在模式串中:下一次有效匹配不能覆盖 c,窗口可直接右移 m+1。
- c 在模式串中:将模式中最右边的 c 与它对齐,右移
m-last[c]。 - 因为预处理从左到右覆盖
shift[t[j]],最终留下最右位置,因此不会跳过可能的第一次匹配。 j是本次窗口中已成功匹配的字符数。j++的次数不是全部字符比较次数;最后失败的那次比较不会增加 j。
所给程序主干注释版
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int f(const string &s, const string &t) {
int n = (int)s.size(), m = (int)t.size();
vector<int> shift(128, m + 1); // ASCII,默认“模式中不存在”
for (int j = 0; j < m; ++j)
shift[t[j]] = m - j; // 同字符会被后面的出现位置覆盖
for (int i = 0; i <= n - m; i += shift[s[i + m]]) {
int j = 0;
while (j < m && s[i + j] == t[j]) ++j;
if (j == m) return i; // 从左向右,首次成功立即返回
}
return -1;
}
int main() {
string s, t;
cin >> s >> t; // 输入不能直接包含空格
cout << f(s, t) << '\n';
}
在现代 C++ 中读取 s[s.size()] 得到末尾空字符;原题最后一次失败后可能恰好读它,随后跳出。如果使用裸字符数组或改写循环,最稳妥的方式是失败后先判断 i+m==n 并结束。扩展到任意字节时,用 256 个槽并将字符转成 unsigned char,防止负下标。
手算与复杂度
s="baaabaaabaaabaaaa"、t="aaaa":窗口起点依次 0、5、6、7、8、13,成功匹配数分别 0、3、2、1、0、4,j++ 共 10 次,返回 13。
最坏时间 O(nm),额外空间 O(字符集大小)。**纠错:**材料中 s=aaaa...、t=bbbba 每次第一位就失败,不能证明 O(nm)。可改用 t=aaa...aba:末尾 a 使跳步为 1,而大量前缀 a 比较成功后才失败。
原题答案核对:16 对;17 错,首次下标 3;18 对,共 2 次;19 D;20 A(类似 find,但 C++ find 失败返回 string::npos);21 B。
易错与变式
- 变式:
s="aaaaa"、t="aaa",若要求所有可重叠出现位置?答案为 0、1、2;首次成功不能直接 return。只找互不重叠匹配时才在成功后跳 m。 - 变式:模式
abaca的 shift 值?答案a→1,b→4,c→2,其他字符→6。若预处理从右到左仍直接覆盖,会留下最左位置,可能跳过答案。
O02|LSD 基数排序:稳定性连接每一位【P0】
来源:S6 阅读程序(2)。k 是进制,m 是最大数的 k 进制位数,base=k^i;每一趟按 (val/base)%k 稳定计数排序。
知识点与不变量
第 i 趟结束后,数组按低 i+1 位组成的数字有序。下一趟相同高一位的元素必须保持旧顺序,低位排序才不会被破坏。这就是必须稳定的原因。
cnt[d] 先存数字 d 的出现次数,再做前缀和,变为“数字不超过 d 的元素个数”。于是 d 组最后一个空位置是 cnt[d]-1。从右往左遍历原数组,每放一个就将 cnt[d] 减 1,能保持同组相对顺序。
注释修正版(补非负前提,放大 base 类型)
材料只写 val 在 int 范围内;原算法实际上要求非负数。负数取余可能产生负下标,最后一轮 base*=k 也可能超出 int。下面保留原流程,用 long long 保存 base。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, k;
cin >> n >> k; // n>=1,k>=2
vector<int> val(n), temp(n), cnt(k);
int maximum = 0;
for (int &x : val) { cin >> x; maximum = max(maximum, x); }
int m = 1;
for (int x = maximum; x >= k; x /= k) ++m;
long long base = 1;
for (int i = 0; i < m; ++i) {
fill(cnt.begin(), cnt.end(), 0);
for (int x : val) ++cnt[(x / base) % k];
for (int d = 1; d < k; ++d) cnt[d] += cnt[d - 1];
for (int j = n - 1; j >= 0; --j) {
int d = (val[j] / base) % k;
temp[cnt[d] - 1] = val[j]; // 当前数字组的最后一个空位
--cnt[d];
}
val = temp;
base *= k; // long long 避免原 int 的溢出
}
for (int x : val) cout << x << ' ';
cout << '\n';
}
手算、复杂度、考点
98、26、91、37、46,k=3。第一趟余数分别为 2、2、1、1、1,所以结果为 91、37、46、98、26。注意不是立即变成全部有序。
时间 O(m(n+k)),额外空间 O(n+k)。k 增大使位数 m 减少,但计数数组处理变多,不能只比较 k 大小就断言更快。若 k 大于最大元素,则 m=1,退化为计数排序。
原题三个判断:错、错、对;三道选择:D、D、C。
变式
- 将回填循环改为
j=0..n-1,但仍从每组末尾向前填,会怎样?不稳定。例[21,11]十进制第一趟得到[11,21],第二趟可排序正确,但一般不能保证;更直接例[12,11]第一趟为[11,12],第二趟同为十位1又反转,错误得到[12,11]。 - 32 位非负整数按 8 位一组排序,每趟 256 桶,做几趟?最多 4 趟,时间 O(4(n+256))。若改为有符号整数,须单独处理符号,不能把负数直接用原公式当下标。
O03|负进制:余数非负,商会变号【P1】
来源:S6 阅读程序(3)。程序求整数 n 的 -k 进制表示,数字仍为 0..k-1。
核心等式
每一步必须保持 旧 n = (-k) × 新 n + 当前位 r。先求唯一满足 0<=r<k 的余数,再令 新 n=(r-旧n)/k。C++ 负数的 % 可能为负,因此要二次取模 (n%(-k)+k)%k。
最先求到的是最低位,输出时倒序。十到三十五用 A..Z。'0'+3 是 int 表达式,直接输出是字符编码值 51,强制转 char 后才输出字符 '3'。
注释修正版
#include <bits/stdc++.h>
using namespace std;
int main() {
long long n; // 原题 n 为 int;计算时放大防止 r-n 溢出
int k;
cin >> n >> k; // 2<=k<=36
if (n == 0) { cout << "0\n"; return 0; }
vector<int> digit;
while (n != 0) {
int r = (n % (-k) + k) % k; // 保证数字在 [0,k-1]
digit.push_back(r);
n = (r - n) / k; // 由 n_old=(-k)*n_new+r 推出
}
for (int i = (int)digit.size() - 1; i >= 0; --i)
cout << char(digit[i] < 10 ? '0' + digit[i] : 'A' + digit[i] - 10);
cout << '\n';
}
手算与边界
n=-255,k=8:依次 (旧n,r,新n)=(-255,1,32),(32,0,-4),(-4,4,1),(1,1,0),输出 1401。校验:1×(-8)^3+4×(-8)^2+0×(-8)+1=-255。
时间和空间均 O(log_k(|n|+1))。负数不能写 log n;负进制位数也不直接等于正进制的 floor(log_k|n|)+1。例如 -1 的二进制长度为 1,但 -2 进制写作 11,长 2。原代码 int 计算 r-n 在 n=INT_MIN 时可能溢出,不能忽略。
原题数值:100 7 → 202;-255 8 → 1401;1000000 19 → 87GIB。强转 char 不能删除。
变式
- 把 -13 转为 -2 进制。余数依次 1、1、1、0、1、1,答案 110111;代回
-32+16+4-2+1=-13。 - 已知 -3 进制数 1201,求十进制值。答案
1×(-27)+2×9+0×(-3)+1=-8。可用 Horner 法从左往右v=v*(-3)+digit,O(位数)。
O04|两个有序数组的第 k 小:边界是算法的一部分【P0】
来源:S7 完善程序(1)。第 k 小按重复次数计算,k 从 1 开始;数组下标从 0 开始。
填空意图与必须修正的边界
所给空的预期依次 C、B、C、C、A,即 m1+m2、a1[m1]<=a2[m2]、left1>right1、y=a2[k-left1-1]、y=a1[k-left2-1]。
把前 k 个数分成两个前缀,长度 p 和 q,p+q=k。当两边都非空,第 k 小为两个前缀末尾的较大值。程序通过比较两边中点,缩小前缀长度的候选范围。m1+m2 数的是两个中点之前的元素数,不是含两个中点的元素数。
**材料缺陷:**只判断 p=0,不判断 q=0,会读 a[-1]。例 a1=[0],a2=[1],k=1,循环结束 p=1、q=0;原代码取 a2[-1]。不能把这种代码作为无条件正确的模板。
原主干补边界注释版
#include <bits/stdc++.h>
using namespace std;
int solve(const vector<int> &a1, const vector<int> &a2, int k) {
int n = a1.size();
int l1 = 0, r1 = n - 1, l2 = 0, r2 = n - 1;
while (l1 <= r1 && l2 <= r2) {
int m1 = (l1 + r1) / 2, m2 = (l2 + r2) / 2;
int cnt = m1 + m2; // ① 两个中点之前共多少个数
if (a1[m1] <= a2[m2]) { // ② 当前 a1 中点较小
if (cnt < k) l1 = m1 + 1;
else r2 = m2 - 1;
} else {
if (cnt < k) l2 = m2 + 1;
else r1 = m1 - 1;
}
}
int p = (l1 > r1) ? l1 : k - l2; // ③ 确定 a1 前缀长度
int q = k - p;
if (p == 0) return a2[q - 1];
if (q == 0) return a1[p - 1]; // 新增:材料遗漏的边界
return max(a1[p - 1], a2[q - 1]); // 对应 ④⑤
}
int main() {
int n, k; cin >> n >> k;
vector<int> a(n), b(n);
for (int &x : a) cin >> x;
for (int &x : b) cin >> x;
cout << solve(a, b, k) << '\n';
}
每轮至少让一个尚未结束的区间减半,时间 O(log n),额外空间 O(1),输入存储除外。并非两个二分嵌套,所以不是 O(log²n)。
更容易证明的教学方法
标准划分二分只枚举第一数组取 p 个,第二数组取 q=k-p 个。合法 p 范围为 [max(0,k-n),min(k,n)]。要求:A[p-1]<=B[q] 且 B[q-1]<=A[p];空前缀末尾当作负无穷,空后缀开头当作正无穷。若 A[p-1]>B[q],p 太大;若 B[q-1]>A[p],p 太小。
例 A=[1,4,8],B=[2,3,9],k=4。p=2,q=2,左边最大 max(4,3)=4,右边最小 min(8,9)=8,合法,答案 4。若 k=1,允许某一前缀长度为零,这是前面越界错误的根源。
变式
- A=[1,2,2],B=[2,3,5],第 4 小是多少?答案 2;重复值必须分别占名次,不能去重。
- 两数组总长度为偶数,求中位数。答案是第 N/2 小和第 N/2+1 小的平均值;用
((long long)x+y)/2.0,防加法溢出和整数除法截断。
O05|容器分水:状态图、最短步数、恢复路径【P0】
来源:S7 完善程序(2)。状态为 (x,y),六条操作边的代价都为 1,因此标准模型是无权图最短路。
六个转移与填空
灌满1 (a,y);灌满2 (x,b);倒空1 (0,y);倒空2 (x,0);2倒1令 t=min(a-x,y),到 (x+t,y-t);1倒2令 t=min(x,b-y),到 (x-t,y+t)。
原空依次 A、C、A、A、C:①④ dfs(x+t,y-t)+1;②⑤ dfs(x-t,y+t)+1;③ x==c || y==c。加 1 是本次操作的代价;目标是任意一个杯子恰好 c,不要求两杯同时等于 c。
所给 DFS 与路径恢复的完整注释主干
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int a, b, c, init, f[N][N];
int dfs(int x, int y) {
if (f[x][y] != init) return f[x][y]; // 已进入过:含尚未完成的状态
if (x == c || y == c) return f[x][y] = 0;
f[x][y] = init - 1; // 临时标记,阻断递归环
f[x][y] = min(f[x][y], dfs(a,y) + 1);
f[x][y] = min(f[x][y], dfs(x,b) + 1);
f[x][y] = min(f[x][y], dfs(0,y) + 1);
f[x][y] = min(f[x][y], dfs(x,0) + 1);
int t = min(a-x, y);
f[x][y] = min(f[x][y], dfs(x+t,y-t) + 1); // ①
t = min(x,b-y);
f[x][y] = min(f[x][y], dfs(x-t,y+t) + 1); // ②
return f[x][y];
}
void go(int x, int y) {
if (x == c || y == c) return; // ③
if (f[x][y] == dfs(a,y)+1) { cout<<"FILL(1)\n"; go(a,y); }
else if (f[x][y] == dfs(x,b)+1) { cout<<"FILL(2)\n"; go(x,b); }
else if (f[x][y] == dfs(0,y)+1) { cout<<"DROP(1)\n"; go(0,y); }
else if (f[x][y] == dfs(x,0)+1) { cout<<"DROP(2)\n"; go(x,0); }
else {
int t = min(a-x,y);
if (f[x][y] == dfs(x+t,y-t)+1) { // ④ 与距离转移相同
cout<<"POUR(2,1)\n"; go(x+t,y-t);
} else {
t = min(x,b-y);
if (f[x][y] == dfs(x-t,y+t)+1) { // ⑤
cout<<"POUR(1,2)\n"; go(x-t,y+t);
} else assert(false);
}
}
}
int main() {
cin >> a >> b >> c;
memset(f,127,sizeof(f));
init=f[0][0]; // 每个字节为127,不是整数127
int ans=dfs(0,0);
if (ans==init-1) cout<<"impossible\n";
else { cout<<ans<<'\n'; go(0,0); }
}
**不要误学不变量:**这个程序用临时“无穷大”阻断环,不足以证明所有 f 都是最终最短距离。独立核对 a,b=1..30、全部合法 c 共 18445 组,dfs(0,0) 与 BFS 一致;没有据此宣称根答案有反例。但 a=1,b=5,c=2 时,从根 DFS 后 f[1][5]=6,独立最短路只有 4 步。因而材料“每一个 f[x][y] 都是最短距离”的解释不成立,也不能把该写法推广到一般有环图。
BFS 教学重写:有明确最短路保证
#include <bits/stdc++.h>
using namespace std;
int main() {
int a,b,c; cin>>a>>b>>c;
int W=b+1, N=(a+1)*W; // 状态(x,y)编码为x*W+y
vector<int> dis(N,-1), pre(N,-1), used(N,-1);
string name[]={"FILL(1)","FILL(2)","DROP(1)","DROP(2)",
"POUR(2,1)","POUR(1,2)"};
queue<int> q; q.push(0); dis[0]=0;
int goal=-1;
while(!q.empty()) {
int id=q.front(); q.pop();
int x=id/W,y=id%W;
if(x==c || y==c) { goal=id; break; }
int t=min(a-x,y),u=min(x,b-y);
pair<int,int> nx[]={{a,y},{x,b},{0,y},{x,0},
{x+t,y-t},{x-u,y+u}};
for(int j=0;j<6;++j) {
int v=nx[j].first*W+nx[j].second;
if(dis[v]!=-1) continue; // 入队就标记,避免重复入队
dis[v]=dis[id]+1; pre[v]=id; used[v]=j;
q.push(v);
}
}
if(goal==-1) { cout<<"impossible\n"; return 0; }
cout<<dis[goal]<<'\n';
vector<int> path;
for(int v=goal;v!=0;v=pre[v]) path.push_back(used[v]);
reverse(path.begin(),path.end());
for(int op:path) cout<<name[op]<<'\n';
}
BFS 不变量:队列中的距离非降;每个状态第一次入队时就得到最短距离。状态最多 (a+1)(b+1),每状态至多六个邻居,所以时间和空间均 O(ab)。实际上从空杯出发可到达的状态常少得多,但考试写安全上界即可。
手算 (a,b,c)=(3,5,4):(0,0)→(0,5)→(3,2)→(0,2)→(2,0)→(2,5)→(3,4),6 步。
变式
- a=4,b=6,c=3 能否完成?不能。可量水量必须是 gcd(4,6)=2 的倍数。一般 c<=max(a,b) 且 gcd(a,b)|c 时可行。
- 灌满代价为 2,倒水代价为 1,倒空代价为 0,还能直接 BFS?不能;用非负边权 Dijkstra。若所有边仅0或1,可以用 0-1 BFS。
O06|两个球的交体积:先分情况,再代公式【P2】
来源:S8 阅读程序(1)。**原材料缺失严重:**显示的代码读入八个三维点,共需24个数;试题输入却只有8个数,判断题中的 t、x、y 也不在显示代码里。因此不能确认原题第16—18题的行号判断,也不能从这段残缺程序推出体积答案。
所给残缺代码能确定的部分
#include <bits/stdc++.h>
using namespace std;
double sq(double x) { return x*x; } // 当前贴文版本接受double
struct Point { double x,y,z; };
double dist(Point a,Point b) {
return sqrt(sq(a.x-b.x)+sq(a.y-b.y)+sq(a.z-b.z));
}
int main() {
Point p[8];
for(int i=0;i<8;++i) cin>>p[i].x>>p[i].y>>p[i].z;
double d=1e9;
for(int i=0;i<8;++i)
for(int j=i+1;j<8;++j) d=min(d,dist(p[i],p[j]));
// 上面只算出了八个点中的最近点对距离。
if(d<1e-6) { cout<<"0.0000\n"; return 0; }
double r=d/2;
(void)r;
// 材料此处缺失体积计算;不能凭空声称它就是完整原题。
}
这段可以判断点对枚举不重不漏:i<j,总共 C(8,2)=28 对。但“后续计算两个球的交体积”需要额外的球心、半径定义,材料没有给出。
独立教学补充:八个数表示两球
下面另约定输入 x1 y1 z1 r1 x2 y2 z2 r2,两个球心加两个非负半径。这是按“两个球相交”知识点重写的完整实现,不是对原题缺失代码的逐字恢复。
球心距离 d,球半径 R、r:
d>=R+r:外离或外切,交体积0。d<=|R-r|:内含或内切,交体积等于较小球体积4πmin(R,r)^3/3;也覆盖同心情形,避免除 d=0。- 其余为两个球冠。相交平面距第一个球心
x=(d²+R²-r²)/(2d),第一球冠高h1=R-x,第二球冠高h2=r-(d-x)。 - 半径 R、冠高 h 的球冠体积为
πh²(R-h/3),两球冠体积相加。
#include <bits/stdc++.h>
using namespace std;
struct Ball { double x,y,z,r; };
double cap(double r,double h) {
return acos(-1.0)*h*h*(r-h/3.0); // 3.0保证实数除法
}
int main() {
Ball a,b;
cin>>a.x>>a.y>>a.z>>a.r>>b.x>>b.y>>b.z>>b.r;
double dx=a.x-b.x,dy=a.y-b.y,dz=a.z-b.z;
double d=sqrt(dx*dx+dy*dy+dz*dz),ans;
if(d>=a.r+b.r) ans=0;
else if(d<=abs(a.r-b.r)) {
double r=min(a.r,b.r);
ans=4.0*acos(-1.0)*r*r*r/3.0;
} else {
double x=(d*d+a.r*a.r-b.r*b.r)/(2*d);
ans=cap(a.r,a.r-x)+cap(b.r,b.r-d+x);
}
cout<<fixed<<setprecision(4)<<ans<<'\n';
}
常数个变量、运算,所以时间空间均 O(1)。例:两个单位球心距1,每个球冠高1/2,总体积 2π(1/2)^2(1-1/6)=5π/12≈1.3090。同心、半径分别1和2,交体积 4π/3≈4.1888。这些数值与贴文列出的体积一致,但不能代替原题代码核验。
类型题如何稳拿分
int a; a/2/sqrt(t) 中第一次除法是整数除法;a/sqrt(t)/2 从第一次除法起就是浮点。是否能交换次序必须看每个中间表达式的类型。若 sq 接受 int,传 double 会截断;若如当前贴文接受 double,则这一截断不存在。不能背“x*x 改 sq(x) 必定不同”。
变式
- 两单位球心距2,交体积多少?0,外切面为一点,三维体积为0。
- 两单位球心距1,求体积并?答案
8π/3-5π/12=9π/4。交与并通过容斥联系,不要把球冠公式直接当作并的体积。
O07|最大子段和:不连续的子序列与连续子段要分清【P0】
来源:S8 阅读程序(2)。所给两段 solve1/solve2 都是分治最大非空连续子段和。
分治的三种位置
区间 [l,r] 的最佳子段:完全在左半、完全在右半、跨越中点。跨中点必须包含 a[mid] 与 a[mid+1],所以等于“左半的最大后缀和 lm”加“右半的最大前缀和 rm”。不是左半任意最优子段加右半任意最优子段,因为那样中间可能留空隙。
所给程序主干注释版(和使用 long long,保留两种初值方式)
#include <bits/stdc++.h>
using namespace std;
using LL=long long;
vector<LL>a;
LL solve1(int l,int r) {
if(l==r) return a[l]; // 非空子段,即使负数也不能返回0
int mid=(l+r)/2;
LL ans=max(solve1(l,mid),solve1(mid+1,r));
LL sum=0,lm=a[mid],rm=a[mid+1];
for(int i=mid;i>=l;--i) {
sum+=a[i]; lm=max(lm,sum); // 枚举所有左半后缀
}
sum=0;
for(int i=mid+1;i<=r;++i) {
sum+=a[i]; rm=max(rm,sum); // 枚举所有右半前缀
}
return max(ans,lm+rm);
}
LL solve2(int l,int r) {
if(l==r) return a[l];
int mid=(l+r)/2;
LL ans=max(solve2(l,mid),solve2(mid+1,r));
LL sum=0,lm=LLONG_MIN,rm=LLONG_MIN;
for(int i=mid;i>=l;--i) { sum+=a[i]; lm=max(lm,sum); }
sum=0;
for(int i=mid+1;i<=r;++i) { sum+=a[i]; rm=max(rm,sum); }
return max(ans,lm+rm); // 两侧循环非空,已覆盖极小初值
}
int main() {
int n;cin>>n;a.resize(n+1);
for(int i=1;i<=n;++i)cin>>a[i];
cout<<solve1(1,n)<<'\n'<<solve2(1,n)<<'\n';
}
复杂度纠错与优化
两函数递归式都为 T(n)=2T(n/2)+Θ(n),每层总工作 Θ(n),共有 Θ(log n) 层,因此都是 Θ(n log n),递归栈 O(log n)。**贴文第25题应选 C,写 O(n) 的答案及主定理推导错误。**第26题 C。原 solve2 用 -1e9 假装负无穷,若输入或子段和可小于它,则不能保证与 solve1 等价;上面的修正版消除了这一初值问题。
线性算法 Kadane:ending=max(a[i],ending+a[i]),表示恰好以 i 结尾的非空最佳子段;ans=max(ans,ending)。也可让分治每个节点返回总和、最大前缀、最大后缀、最大子段和四项,每次合并 O(1),整体降到 O(n)。不要用后一种复杂度解释当前贴文中的扫描版。
手算 [-10,11,-9,5,-7]:ending 为 -10、11、2、7、0,ans 最终11。[-3,2,10,0,-8,9,-4,-5,9,4] 最佳和17。源第23题依赖缺失对应行号,不把其“一次”的说法当作循环执行次数规则。
变式
- 全为负数
[-5,-2,-9]:非空子段答案 -2;若允许空段,答案0。初始化不能混用。 - 求最大连续子段积,能否只维护最大积?不能。负数会把最小积变最大,需同时维护以当前位置结尾的最大积和最小积。例
[-2,3,-4]答案24。
O08|Base64:三字节拆四组,每组六位【P1】
来源:S8 阅读程序(3)。Base64 是编码,不是加密:无密钥、可逆,并不隐藏内容。
位运算与长度
3 字节共24位,分成4个6位数,范围0..63,查字符表 A-Z,a-z,0-9,+,/。完整标准格式长度为 4ceil(n/3),末尾按不足部分补 '=';本材料 encode 没有补 '=',输出无填充格式,长度 ceil(4n/3)。
每组待编码字节不足3个时补零,只输出有实际信息的前2或前3个字符。解码时从每个字符取得6位,累计满8位就输出一个字节。循环不变量是缓冲区最后 bits 位尚未输出。
保留源“无填充编码”行为的注释修正版
#include <bits/stdc++.h>
using namespace std;
const string tab="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
string encode(const string&s) {
string ret;
for(size_t i=0;i<s.size();i+=3) {
unsigned val=0;
for(size_t j=0;j<3;++j)
val=val*256+(i+j<s.size()?(unsigned char)s[i+j]:0);
ret+=tab[(val>>18)&63]; // 最高6位
ret+=tab[(val>>12)&63];
if(i+1<s.size())ret+=tab[(val>>6)&63];
if(i+2<s.size())ret+=tab[val&63];
// 源程序无 '=';若需标准填充,最后补到4的倍数。
}
return ret;
}
string decode(const string&s) {
string ret; unsigned val=0; int bits=0;
for(char c:s) {
if(c=='=')break;
size_t pos=tab.find(c);
if(pos==string::npos)continue; // 保留源逻辑:跳过无效字符
val=(val<<6)|(unsigned)pos;bits+=6;
if(bits>=8) {
bits-=8;
ret+=char((val>>bits)&255);
}
val&=(1u<<bits)-1; // 只保留未输出位,避免无限累积溢出
}
return ret;
}
int main() {
int op;string s;cin>>op>>s;
string ans=op==0?encode(s):decode(s);
cout<<ans.size()<<'\n'<<ans<<'\n';
}
复杂度 O(n),查表长64,是常数;输出空间 O(n)。源 encode 中 char 若有符号,处理128..255字节会出问题,教学版转 unsigned char;源 decode 使用有符号 int 持续左移可溢出,教学版保留有限缓冲位。
手算与材料纠错
ASCII 'M'=77=01001101,补零后分组 010011=19(T)、010000=16(Q),源输出 TQ,标准填充为 TQ==。Ma 为 TWE/TWE=,Man 为 TWFu。
SGVsbG93b3JsZA== 解出 Helloworld,w 小写。CSP2021csp 的源代码实际编码为 Q1NQMjAyMWNzcA,长度14;标准填充才是 Q1NQMjAyMWNzcA==,长度16。贴文选项与代码不一致。第32题缺输入与完整题干,不能确认答案;string::npos 是无符号类型的最大值,也不能笼统说“cout npos 输出 -1”。
变式
- 输入7字节,标准格式输出多少字符、几个等号?12字符、2个等号;无填充格式10字符。
- 若字符表仍64个符号,但把每组取成7位会怎样?下标可能达127,数组越界;Base64 的64来自
2^6,掩码63即二进制六个1。
O09|魔法数字:按最少材料数确定状态【P1】
来源:S8 完善程序(1)。允许 +、-、能整除时的除法,所有中间结果为1..10000。F[x] 表示构造 x 所需的最少4的个数,而不是运算次数。
填空及正确不变量
预期空为 D、A、D、C:F[4]=1;!Vis[n];!Vis[i]&&F[i]<F[x];Vis[i]。
每次确定未确定状态中 F 最小的 x,再把 x 与所有已确定 i 合并,代价 F[x]+F[i]。这是表达式树的合并:例如 (4+4)/4 的两棵子表达式用2个和1个4,总用3个4。
证明要点:合并代价是两个正整数代价之和,严格大于各孩子代价,因此一个最优表达式的两个孩子都会先确定。与普通 Dijkstra 的“非负边松弛”相似,但这里是双操作数合并,不要硬画成固定权重的普通一元边。
原主干修正版,明确修两处文本缺陷
原贴文 F[0] 是全局0,却设 x=0 做最小值哨兵,所以没有正 F 能比 F[0] 小;并且选中第一个候选后立刻 break,没扫到真正最小值。下面将 F[0] 设为 INF,并删除扫描里的 break。
#include <bits/stdc++.h>
using namespace std;
const int M=10000,INF=1000000000;
int F[M+1];bool Vis[M+1];
int main() {
int n;cin>>n;
fill(F,F+M+1,INF); // 修正:连F[0]一起设为无穷大
F[4]=1; // ① 1个4构成4
while(!Vis[n]) { // ② 直到目标被正式确定
int x=0;
for(int i=1;i<=M;++i)
if(!Vis[i]&&F[i]<F[x]) x=i; // ③ 必须扫完整段,不能break
if(x==0)break;
Vis[x]=true;
for(int i=1;i<=M;++i) if(Vis[i]) { // ④ 两边都是已确定的值
int t=F[i]+F[x];
if(i+x<=M) F[i+x]=min(F[i+x],t);
if(i!=x) F[abs(i-x)]=min(F[abs(i-x)],t); // 不允许中间为0
if(i%x==0) F[i/x]=min(F[i/x],t);
if(x%i==0) F[x/i]=min(F[x/i],t);
}
}
cout<<(F[n]==INF?-1:F[n])<<'\n';
}
最多确定 M 个状态,每次两次 O(M) 扫描,总 O(M²),空间 O(M)。用堆只能减少挑最小的成本,合并全部已确定状态仍可能 O(M²)。
手算:F[4]=1;用4和4得到8、1,F[8]=F[1]=2;再用8和4得到2,F[2]=3。所有表达式只用2个4时仅能得到8、1(不许0且无乘法),所以2不能更少于3个4。
变式
- 如果问“运算次数最少”而不是“4的个数最少”,如何改?对于只用二元运算的完整表达式,叶子数K对应运算K-1,因此答案减1;但若增加一元运算或允许免费常数,此等价关系不一定保留。
- 允许向零截断的整数除法,2最少用多少4?仍是3,但转移不必要求
%==0,只须结果>0;其他目标的最优值可能改变。题目写“整除”时必须看清是“仅整除才合法”还是语言中的整数除法。
O10|RMQ、笛卡尔树、LCA、±1 分块【P2】
来源:S8 完善程序(2)。这题最有价值的得分点是单调栈建树和位掩码。完整线性 RMQ 属于拓展,考前先掌握前两部分。
从数组到树:两个性质同时成立
最大笛卡尔树的中序遍历顺序等于原数组位置顺序,父值不小于子值。因此区间 [l,r] 的最大值等于结点 l、r 的最近公共祖先 LCA 的值。
单调栈保存当前树的最右链,栈值非增。新元素 p 到来时,连续弹出小于 p 的栈顶,被弹出的最后一棵子树作为 p 的左孩子;弹完后若栈非空,p 成为新栈顶的右孩子。每个结点进栈、出栈至多一次,所以 O(n)。
例数组 [3,1,4,2]:根为位置3(值4),左子树根位置1(值3),其右孩子位置2(值1),右孩子位置4(值2)。询问位置2..4,LCA是位置3,最大值4。
从 LCA 到 ±1 RMQ
DFS 进入结点记录一次,每从孩子返回再记录父亲,得到长度 t=2n-1 的 Euler 序;不是只写每个结点一次的先序遍历。相邻记录沿一条树边移动,深度差必为+1或-1。两个结点首次出现位置之间的最浅结点就是LCA。
块长取约 (log2 t)/2。整块最浅位置用 ST 表查;两端零散部分用块的深度差状态查。每块 b 个深度,有 b-1 个±1差分,只需 b-1 位。若位1表示下降1,位0表示上升1,那么区间 [l,r] 包含 r-l 个差分,要用 (1<<(r-l))-1,不是 r-l+1。
原填空依次 A、D、A、D、D、C:
p->son[0]=stack[top--]:弹出的旧树放左侧。stack[top]->son[1]=p:p 是剩余右链的新右孩子。x->dep<y->dep:Euler RMQ 比较深度,取最小。A[i*b+j]->dep<A[i*b+j-1]->dep:下降时置位。v+=(S>>(i-1)&1)?-1:1:按差分恢复相对深度。(Dif[p]>>(l-p*b))&((1<<(r-l))-1):提取需要的连续差分。
贴文只有建树代码,后四个空对应函数缺失。下面提供完整教学重写,采用同样的建树、Euler、±1分块方案,使用下标替代指针,避免把缺失的代码当成已经恢复。
完整注释教学实现
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);cin.tie(nullptr);
int n,m;cin>>n>>m;
vector<long long>a(n);
for(auto&x:a)cin>>x;
vector<int>L(n,-1),R(n,-1),stk;
for(int i=0;i<n;++i) {
int last=-1;
while(!stk.empty()&&a[stk.back()]<a[i]) {
last=stk.back();stk.pop_back();
}
L[i]=last; // 最后弹出的整棵子树挂左侧
if(!stk.empty())R[stk.back()]=i; // 挂到剩余最右链末尾
stk.push_back(i);
}
int root=stk.front();
vector<int>first(n,-1),dep(n),euler;
auto record=[&](int u) {
if(first[u]==-1)first[u]=(int)euler.size();
euler.push_back(u);
};
// 显式栈,防笛卡尔树退化成链时递归栈溢出。
vector<pair<int,int>>todo{{root,0}};
while(!todo.empty()) {
int u=todo.back().first,phase=todo.back().second;
if(phase==0) {
record(u);todo.back().second=1;
if(L[u]!=-1) { dep[L[u]]=dep[u]+1;todo.push_back({L[u],0}); }
} else if(phase==1) {
if(L[u]!=-1)record(u); // 左孩子返回,重新记录父亲
todo.back().second=2;
if(R[u]!=-1) { dep[R[u]]=dep[u]+1;todo.push_back({R[u],0}); }
} else {
if(R[u]!=-1)record(u);
todo.pop_back();
}
}
int t=euler.size();
vector<int>lg(t+1);
for(int i=2;i<=t;++i)lg[i]=lg[i/2]+1;
int b=max(1,lg[t]/2),blocks=(t+b-1)/b;
auto better=[&](int x,int y) { // 返回两个Euler位置中较浅的
if(x==-1)return y;if(y==-1)return x;
return dep[euler[x]]<=dep[euler[y]]?x:y;
};
vector<int>dif(blocks,0),blockMin(blocks,-1);
for(int p=0;p<blocks;++p)
for(int j=0;j<b&&p*b+j<t;++j) {
int k=p*b+j;
blockMin[p]=better(blockMin[p],k);
if(j&&dep[euler[k]]<dep[euler[k-1]]) dif[p]|=1<<(j-1);
}
// best[mask][len]:相对深度从0起,len个位置中最浅者的偏移。
int states=1<<(b-1);
vector<vector<int>>best(states,vector<int>(b+1));
for(int mask=0;mask<states;++mask) {
int v=0,mn=0,pos=0;best[mask][1]=0;
for(int len=2;len<=b;++len) {
v+=((mask>>(len-2))&1)?-1:1;
if(v<mn) { mn=v;pos=len-1; }
best[mask][len]=pos;
}
}
auto inside=[&](int l,int r) { // 保证l、r处于同一块
int p=l/b;
int mask=(dif[p]>>(l-p*b))&((1<<(r-l))-1);
return l+best[mask][r-l+1];
};
// 每个整块只保留最浅Euler位置,用ST表进行块间RMQ。
vector<vector<int>>st(lg[blocks]+1,vector<int>(blocks,-1));
st[0]=blockMin;
for(int k=1;(1<<k)<=blocks;++k)
for(int i=0;i+(1<<k)<=blocks;++i)
st[k][i]=better(st[k-1][i],st[k-1][i+(1<<(k-1))]);
auto whole=[&](int l,int r) {
if(l>r)return -1;
int k=lg[r-l+1];
return better(st[k][l],st[k][r-(1<<k)+1]);
};
while(m--) {
int l,r;cin>>l>>r;--l;--r; // 输入位置从1开始
l=first[l];r=first[r];
if(l>r)swap(l,r);
int p=l/b,q=r/b,k;
if(p==q)k=inside(l,r);
else {
k=better(inside(l,(p+1)*b-1),inside(q*b,r));
k=better(k,whole(p+1,q-1));
}
cout<<a[euler[k]]<<'\n'; // 最浅结点的值就是原数组区间最大值
}
}
建树与 Euler 为 O(n)。整块表 O((t/b)log(t/b))=O(t),小块模式表 O(b2^(b-1))=O(√t log t),不超过 O(t) 量级;每次询问只查两端和中间,O(1),总 O(n+m),空间 O(n)。贴文把预处理写成 O(m) 混用了变量,预处理应依赖数组长度而非询问次数。
易错与变式
- Euler 深度
[0,1,2,1,0],以“降1置1”编码差分,低位对应最早差分,状态是多少?差分+1,+1,-1,-1,位从低到高0、0、1、1,所以状态12。查询位置1..3(0基)深度1、2、1,最小深度1。 - 将 RMQ 从最大改成最小,建树弹栈条件如何改?由
<改>,建最小堆笛卡尔树;LCA 的 Euler 查询仍取最小深度,这部分不能跟着改成最大深度。
O11|按位恒等式与枚举:把陌生表达式化简【P0】
来源:S9 阅读程序(1)。在满足 d[i]<d[j] 的数对中,求 d[i]+d[j]-(d[i]&d[j]) 最大值。
逐位证明与循环含义
每一二进制位中,两个数的位相加等于异或位加上两倍的与位,所以 a+b=(a^b)+2(a&b)。又因为异或部分与相交部分不重叠,a|b=(a^b)+(a&b)。因此 a+b-(a&b)=a|b。
这是整数数学恒等式;C++ 有符号 int 的 a+b 如果先溢出,不能用代数化简证明原程序安全。假设非负输入且相关算术不溢出,程序是在求不同数值数对的最大按位或。
主干注释版(保留原式,扩大中间类型)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;cin>>n;
vector<int>d(n);for(int&x:d)cin>>x;
long long ans=-1; // 没有合法数对时保持-1
for(int i=0;i<n;++i)
for(int j=0;j<n;++j)
if(d[i]<d[j]) // 比较数值,不是比较下标
ans=max(ans,1LL*d[i]+d[j]-(d[i]&d[j]));
cout<<ans<<'\n'; // 非负输入也可直接更新d[i]|d[j]
}
不变量:扫描到某数对时,ans 是已扫描合法数对的最大表达式值,或者在不存在合法对时为-1。复杂度 O(n²),除输入外额外空间 O(1)。原数组 d[1000] 可存 n=1000 的0..999下标,不能说 n 必须严格小于1000。
手算与纠错
输入 [5,2],合法有序对是 (i,j)=(1,0),答案 2|5=7;若改 j=i+1 但保留 d[i]<d[j],唯一考虑的下标对不满足条件,错误输出-1。若将比较改成 !=,同一无序数对算两遍,但表达式对称,最大值不变。
若输出127,不能有128:若128与任何不同的非负数配对,或值至少128;若全是128则根本无合法对,输出-1。不能说“因为128|127=255”就完成证明,因为127不一定是输入值。
输出正偶数,达到最大值的两数都必须为偶数;输出奇数,只需至少一数奇数,不必两数都奇。
原题判断:错、错、对、对;选择 C、C,均按非负且不溢出的通常题意理解。
变式
[6,6,6]输出多少?-1;如果改条件为i!=j,允许不同位置相等值配对,则输出6。- 求最大按位与,能否用“两个最大数”直接回答?不能。例
[8,7,6],8&7=0,但7&6=6。位运算大小与普通数值大小不能简单互换。
O12|随机快速选择:只递归答案所在的一边【P0】
来源:S9 阅读程序(2)。选择一个随机基准 p,划分比 p 小和不小于 p 的元素,根据 p 的名次只递归一侧。
变量含义与原程序边界缺陷
基准暂放 d[L];a 从左向右找不小于基准的数,b 从右向左找小于基准的数,然后交换。循环结束后 a 调整到“右组第一个位置”,比基准小的数位于 [L+1,a-1],共 a-L-1 个,故基准的相对名次为 a-L。
- k 等于名次:返回基准。
- k 大于名次:去
[a,R]找第k-(a-L)小。 - k 小于名次:去
[L+1,a-1]找第 k 小。
**原材料缺失 L==R 的结束条件。**当 n=1、k=1、d[0]=5,a=1,原全局 d[1]=0,比较后把 a 加到2,再递归错误区间,最终可能输出0。虽然没越出长度10000的数组,却已经越出了当前有效数据区间,不能说“没崩溃就正确”。
原主干修正版(只补单元素基例)
#include <bits/stdc++.h>
using namespace std;
vector<int>d;
int kth(int L,int R,int k) {
if(L==R)return d[L]; // 原贴文遗漏的必要边界
int x=rand()%(R-L+1)+L; // 范围为[L,R],包括L
swap(d[L],d[x]); // 基准暂存在区间首位
int a=L+1,b=R;
while(a<b) {
while(a<b&&d[a]<d[L])++a;
while(a<b&&d[b]>=d[L])--b;
swap(d[a],d[b]);
}
if(d[a]<d[L])++a; // 现在a是第一个“不小于基准”的位置
int rank=a-L;
if(rank==k)return d[L];
if(rank<k)return kth(a,R,k-rank);
return kth(L+1,a-1,k); // 左侧递归不包含基准
}
int main() {
int n,k;cin>>n>>k;d.resize(n);
for(int&x:d)cin>>x;
cout<<kth(0,n-1,k)<<'\n';
}
手算与复杂度
数组 [4,1,5,2,3],以4为基准,左边小于4的1、2、3共3个,4为第4小。求第2小只去这3个数内继续;求第5小只去右边找第1小。
元素互异、随机基准理想均匀时,期望时间 O(n),最坏 O(n²)。原因是单次划分 O(n),平均递归规模按比例缩小,形如 n+n/2+n/4+...=O(n);这只是理解期望的直观式,不能断言每次实际恰好减半。最坏每次只排除一个元素,求和 n+(n-1)+...。
递归栈期望 O(log n),最坏 O(n)。全相等时两组严重不平衡,k=1 一趟即返回 Θ(n),k=n 才退化为 Θ(n²)。原“全相等一律 O(n²)”应理解为关于 k 的最坏情况,不能说每个 k 都如此。
易错与变式
资料称某道“递增输入时 swap 平均次数”没有正确选项,但未提供可核验官方出处,本讲义不把这当成官方结论。记住:某条 swap 的执行次数不等于整个程序运行时间;即使交换很少,指针扫描仍可能线性。
- 所有 n 个数相等,要快速找到任意第 k 小,如何避免二次退化?使用三路划分为
<p、=p、>p;k 落在等于组就直接返回,本例一趟 O(n)。 - 快排为何通常 O(n log n),快速选择却期望 O(n)?快排要递归左右两侧,选择只递归一侧。若题目要前 k 小有序排列,单次选择后还须排序该部分,总期望 O(n+k log k)。
O13|字符串旋转状态图:逆操作、双向搜索与奇偶性【P1/P2】
来源:S9 阅读程序(3)。P1 掌握旋转含义与逆操作;P2 再看排列奇偶性。
先认清两个正向操作
LtoR(s,L,R) 把区间最左字符搬到最右端,其余字符向左移一格;RtoL 反过来。正向从起点可进行:后缀 [m,n-1] 左旋、前缀 [0,m] 右旋。反向从终点应进行它们的逆操作:前缀左旋、后缀右旋。不能从终点仍沿原方向走,因为状态图是有向的。
p^1 在0和1之间切换;映射表保存每边见过的状态到该边起点的距离;相遇状态的两个距离相加得到候选总步数。
原程序主干的完整注释转写
为避免原 maxl=20000000 导致极大静态内存,下面用 vector/deque 保存同样的表和队列。映射表仍然线性查找,保留原代码的复杂度特征;调度仍为每边轮流弹出一个状态,另加空队列保护。此处用于对照阅读原算法,不把这种调度推广成通用的双向 BFS 模板。
#include <bits/stdc++.h>
using namespace std;
class LinearMap {
vector<pair<string,int>>d;
public:
int find(const string&s)const {
for(const auto&item:d)if(item.first==s)return item.second;
return -1; // 查找失败
}
void insert(string s,int v) { d.push_back({move(s),v}); }
} seen[2];
deque<string>q[2];
string LtoR(string s,int L,int R) {
char c=s[L];
for(int i=L;i<R;++i)s[i]=s[i+1];
s[R]=c;return s;
}
string RtoL(string s,int L,int R) {
char c=s[R];
for(int i=R;i>L;--i)s[i]=s[i-1];
s[L]=c;return s;
}
bool check(const string&s,int p,int step) {
if(seen[p].find(s)!=-1)return false;
++step; // 从当前状态再走一条边
int other=seen[p^1].find(s);
if(other==-1) {
seen[p].insert(s,step);q[p].push_back(s);return false;
}
cout<<other+step<<'\n';return true;
}
int main() {
string s,t;int m;cin>>s>>t;
if(s.size()!=t.size()){cout<<-1<<'\n';return 0;}
if(s==t){cout<<0<<'\n';return 0;}
cin>>m;int n=s.size();
seen[0].insert(s,0);seen[1].insert(t,0);
q[0].push_back(s);q[1].push_back(t);
for(int p=0;!q[0].empty()||!q[1].empty();p^=1) {
if(q[p].empty())continue; // 防止在空队列上读取front
string u=q[p].front();q[p].pop_front();
int d=seen[p].find(u);
if(p==0) {
if(check(LtoR(u,m,n-1),p,d)||check(RtoL(u,0,m),p,d))return 0;
} else {
if(check(LtoR(u,0,m),p,d)||check(RtoL(u,m,n-1),p,d))return 0;
}
}
cout<<-1<<'\n';
}
复杂度与正确使用双向 BFS
若字符互异,状态最多 V=n!;每次字符串操作/比较 O(n),线性 Map 查找 O(Vn),总安全上界 O(V²n),空间 O(Vn)。标准哈希表可把状态查找期望降为 O(n)(计算字符串哈希),整体期望 O(Vn)。类名叫 Map 并不代表使用了平衡树;一定读 find 的实现。
通用写法要按完整的一层扩展,可以每次选择较小的一侧前沿,记录该层的最佳相遇距离,再结合两侧已扩展深度作终止判断。不要把“轮流各弹一个结点,一见相遇就结束”当作无需证明的通用最短路定理。本讲义的数值核对使用独立分层 BFS。
手算、可达性与材料纠错
若 m=0,第二个正向操作只动长度1的前缀,是原地不变;唯一有效操作就是整串左旋。abcd→bcda→cdab→dabc→abcd。所以 abcd 到 dcba 不可达,输出-1;即使字符组成完全相同也不保证可达。
m=n-1 时唯一有效操作是整串右旋,因此与 m=0 的最短步数可能不同。例如长度101互异串,目标是左旋一次;m=0需1步,m=100需100步。
材料第5题前几组输入长度不等,按字面程序立即输出-1,不能同时声称输出4、14、28。对字符正确且 m=1 的逆序问题,独立 BFS 得长度4、6、8的最短距离4、14、28;**字面长度10的 0123456789→9876543210 答案为46,不是材料的68,且所列选项没有46。**不要用损坏题干练猜数列。
排列奇偶性
长度 L 的循环旋转等价于 L-1 次相邻交换,其奇偶性为 (-1)^(L-1)。两个操作区间长度为 n-m 和 m+1。
当 n 为奇数、m 为偶数时,两段长度都是奇数,所以每个操作都是偶排列,只能维持排列奇偶性。若起终点字符互异且两排列奇偶性不同,就不可能到达。这给出原选择 C 的阻碍证据;若有重复字符,交换相同字符看不出来,不能直接把字符状态硬判为唯一奇偶性。
变式
abcd→bcda,m=0需要几步?1。改m=3需要几步?3。这也是“逆操作存在,但步数不相等”的最小直观例子。- n=5,m=2,起点
abcde,终点bacde能否到达?不能。两个操作都是3循环,为偶排列;目标只交换a、b,是奇排列。
O14|分数背包:单位价值排序与精确分数【P0】
来源:S9 完善程序(1)。物品可任意切分,取走体积占比 α 就获得同等占比价值,所以按价值密度 w/v 从高到低装。
贪心证明与填空推导
如果方案中取了低密度物品的一小部分,同时还有高密度物品可取,把前者的等体积替换为后者,不增加体积而增加或保持价值。不断交换后,最优方案可表现为若干个高密度完整物品,加至多一个部分物品。
原填空依次 D、B、D、D、B:
w[j]*v[j+1]<w[j+1]*v[j]:当前密度更低就交换。分母为正,交叉相乘保持不等号;整数w/v会截断,不能用于比较。v[1]<=B:体积能否装下,不是价值能否装下。curV=v[1];curW=w[1];:第一件完整装入,后续从第二件开始。curW*v[i]+(B-curV)*w[i],v[i]:curW+(剩余容量/v[i])*w[i]通分后的分子、分母。curW,1:全部完整放下时,答案是整数,不是平均价值。
原主干完整注释版
#include <bits/stdc++.h>
using namespace std;
using LL=long long;
LL gcd2(LL a,LL b) { return b==0?a:gcd2(b,a%b); }
void print(LL w,LL v) {
LL g=gcd2(w,v);w/=g;v/=g;
cout<<w;if(v!=1)cout<<'/'<<v;cout<<'\n';
}
int main() {
int n;LL B;cin>>n>>B;
vector<LL>w(n+1),v(n+1);
for(int i=1;i<=n;++i)cin>>w[i]>>v[i];
for(int i=1;i<n;++i)
for(int j=1;j<n;++j)
if(w[j]*v[j+1]<w[j+1]*v[j]) { // ① 密度降序
swap(w[j],w[j+1]);swap(v[j],v[j+1]); // 两属性一起交换
}
LL curV,curW;
if(v[1]<=B) { curV=v[1];curW=w[1]; } // ②③
else { print(B*w[1],v[1]);return 0; }
for(int i=2;i<=n;++i)
if(curV+v[i]<=B) { curV+=v[i];curW+=w[i]; }
else {
print(curW*v[i]+(B-curV)*w[i],v[i]); // ④ 只切当前这一件
return 0;
}
print(curW,1); // ⑤ 所有物品都装下
}
原排序为冒泡式,O(n²),装载 O(n),整体 O(n²)。如果改 sort,整体 O(n log n)。额外存储 O(n)。扩大输入范围时交叉乘积必须用足够宽类型;w和v要一起交换,否则把别人的价值与体积配对了。
手算 B=8,物品(w,v)为(4,5),(4,3),(2,2)。密度顺序为4/3、1、4/5,先取后两件,体积5、价值6;剩余3体积取第一件的3/5,价值12/5;总价值42/5。
变式
- 不允许切割,仍按密度贪心可以吗?不可以。容量50,物品(价值,体积)=(60,10),(100,20),(120,30);密度法选前两件得160,而后两件得220。这是0/1背包,通常用DP。
- 两件密度相同,改变先后会影响分数背包最优值吗?不会。在总取入体积相同时,价值都等于密度乘体积;但具体取哪些部分可以不同。
O15|最优子序列:位拆分 DP,以及本题更强的三角不等式【P1/P2】
来源:S9 完善程序(2)。令 w(x)=x+popcount(x),相邻选择元素 u、v 的收益为 w(u^v)。P1 先懂朴素DP和两个8位拆分;P2 再看状态预付贡献的设计。
第一步:写出不依赖优化的正确 DP
令 dp[i] 表示选取若干递增下标、且最后一个恰好为 i 的最大分值。只有一个元素没有相邻边,值为0。
dp[i]=max(0,max_{j<i}(dp[j]+w(a[j]^a[i])))。
直接枚举前驱 j 需要 O(n²),n=40000 时偏大。此时应先认出转移代价能按位拆开,而不是盯着数组 Max 猜其含义。
第二步:高低8位的贡献可加
写 a=(H<<8)|L,其中 H=a>>8,L=a&255。两个数异或的高低位互不相交,所以
w(a^b)=w((Ha^Hb)<<8)+w(La^Lb)。
注意 w(h<<8) 不等于 w(h)<<8。前者是 256h+popcount(h),后者是 256h+256popcount(h),会把1的个数也错乘256。
第三步:Max 的精确定义
处理第 i 个数之前,
Max[X][Y] = max_{j<i, Lj=Y}(dp[j]+w((Hj^X)<<8))。
也就是:前驱元素的低8位固定为Y;提前假定下一个元素的高8位是X,将高位转移收益预先加进去。第一维属于未来,第二维属于前驱,二者不是同一个数的高低位。
当前数的高低位为 x,y:
- 查询:枚举前驱的低位 z,
v=max(v,Max[x][z]+w(y^z))。预付了高位,现在补低位,v最终就是dp[i]。 - 更新:当前数成为未来的前驱,枚举未来的高位z,
Max[z][y]=max(Max[z][y],v+w((x^z)<<8))。 - 必须先完成全部查询再进行更新,保持表里只包含 j<i 的前驱。
v=0对应单独选择当前元素,不是说“所有 Max 都还没更新过”。Max 中可以已有大量旧状态。
填空与逐个排除
预期 D、B、C、A、B:
① x^=x&(x^(x-1)) 每次删掉最低一个1,等价 x&=x-1,循环次数就是 popcount。若仅右移1,循环次数会是位数而不是1的数量。
② a>>B 取高8位;a&(MS<<B) 虽保留高位,却没移回0..255,不能直接作为这一维下标。
③ 0。④ Max[x][z]+w(y^z)。⑤ to_max(Max[z][y],v+w((x^z)<<B))。其中⑤若误用 w(a^(z<<B)) 会顺便把当前低位贡献算进去,之后查询又算一次。
原主干完整注释版
#include <bits/stdc++.h>
using namespace std;
using LL=long long;
const int B=8,MS=(1<<B)-1;
const LL INF=1000000000000000LL;
LL Max[MS+1][MS+1];
int w(int x) {
int s=x;
while(x) {
x^=x&(x^(x-1)); // ① 删最低位的1,等价x&=x-1
++s; // 每删一个1,分值加1
}
return s;
}
void to_max(LL&x,LL y) { if(x<y)x=y; }
int main() {
int n;cin>>n;LL ans=0;
for(int x=0;x<=MS;++x)
for(int y=0;y<=MS;++y)Max[x][y]=-INF;
for(int i=1;i<=n;++i) {
int a;cin>>a;
int x=a>>B,y=a&MS; // ② 当前元素的高低位
LL v=0; // ③ 单元素序列,没有边
for(int z=0;z<=MS;++z)
to_max(v,Max[x][z]+w(y^z)); // ④ 查询所有前驱低位
for(int z=0;z<=MS;++z)
to_max(Max[z][y],v+w((x^z)<<B)); // ⑤ 预付给未来的高位收益
to_max(ans,v);
}
cout<<ans<<'\n';
}
空间是256×256个 long long,约512 KiB。若 w 通过逐个删1计算,每次最多8次(这里传入的数只占高8位或低8位),时间 O(n·2^8·8),固定16位时可写 O(256n) 并说明常数。若预处理256个低位权和256个高位权,查询与更新每项就严格O(1)。一般m位平均拆分,时间约 O(n·2^(m/2)·m),空间 O(2^m)。
手算一轮,避免只记字母
序列 [0,256,257]。
第一项0:dp=0;更新 Max[z][0]=w(z<<8)。第二项256的高位1、低位0:查询 Max[1][0]+w(0)=257,所以dp=257。第三项257的高位1、低位1:以前一项256结尾的状态提供257,加低位差 w(1)=2,得到259。若直接从0跳到257,收益 257+2=259,两种方案同值。
本题还能 O(n):插入中间元素不会减分
这不是原填空考点,但能帮助理解“优化前先研究性质”。把 w(u^v) 按二进制每一位展开,等于所有不同位的权重之和,其中第t位的权重是 2^t+1,全部为正。
对任意三个位值 p,q,r∈{0,1},[p≠r] <= [p≠q]+[q≠r]。每位乘上正权重再相加得到:
w(a^c) <= w(a^b)+w(b^c)。
所以若子序列跳过中间元素b,把它插入不会降低分数;在首尾补元素也只会加上非负边。因此完整原序列总是一个最优解,答案就是所有相邻原元素收益之和。原位拆分DP仍能算对,但本题本身有更简单的结构。若换成不满足三角不等式的可拆分分值,才可能必须用DP。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;cin>>n;
unsigned prev=0;long long ans=0;
for(int i=0;i<n;++i) {
unsigned x;cin>>x;
if(i) {
unsigned z=prev^x;
ans+=(long long)z+__builtin_popcount(z);
}
prev=x;
}
cout<<ans<<'\n';
}
时间 O(nm)(按逐位计数的一般m位分析),固定16位或硬件popcount视作 O(n),额外空间 O(1)。最大单边分值65535+16=65551,n=40000时上界为2621974449,超过有符号32位范围,ans必须用long long。
变式
[1,2,3]的最优值?w(1^2)+w(2^3)=w(3)+w(1)=5+2=7;只取1和3则w(2)=3,明显更小。- 如果恰好选两个元素,完整序列的结论还能用吗?不能。例
[0,1,0],原题最大4;恰好选两个时最大2。若要求恰好选K个,应给DP增加选择数量这一维,且按阶段维护状态,不能在同一表里混用不同长度。
本部分的考前自测清单
看到陌生程序,按下面顺序读,比从第一行逐字猜快:
- 先找输入范围、下标起点和输出变量;数组长度n能否容纳下标n?返回下标还是排名?
- 识别状态:i是“当前位置”、k是“剩余排名”、F是“代价”还是“个数”?至少用一句中文写出定义。
- 每个循环/递归写一个不变量。只会说“这里用了DP/二分”不足以填空。
- 检查四种边界:最小输入、全相等、全负数或0、刚好用完一边。
- 数复杂度时分别算状态数、每状态工作、递归树层数。一个函数叫Map不代表 O(log n),一段有二分不代表整个算法 O(log n)。
- 填空先看“后面怎样用它”:数组下标必须合法,数量必须与下标差1,路径恢复条件必须与DP转移同向。
- 带“必然、任意、总是”的判断,优先找最小反例;遇到损坏题干,明确不确定处,不背错结论。
闭卷自测:24题检查能否迁移
建议明晚开始前限时20分钟完成前16题,后天早上完成17—24题。先做再看答案。题目是根据材料知识点重新编写,不是所谓押题;做错回到相应讲义。
题目
5 & 3、5 | 3、5 ^ 3分别是多少?- 在题设16位无符号模型中,x=1,先执行
x ^= x<<6,再执行x ^= x>>8,最终是多少? int a=100000,b=100000; long long c=a*b;为什么不可靠?改一个表达式使乘法安全。- 当k=7、n=-10时,C++中
n%k和(n%k+k)%k分别是多少? for(i=1;i<=n;i*=2) for(j=0;j<n;++j)的复杂度?默认i不会因越界溢出。- 有序数组两指针各只右移、不回退,虽然内外两层循环,累计复杂度为什么可为O(n)?
- 有4个相同红球、3个相同蓝球,任意蓝球不相邻,排列数是多少?
- 从10人中选3人组成不分工小组,与选金银铜各一人,分别多少种?
- 1—30中不能被2或3整除的整数有多少个?
- K6中有多少个无向简单四环?不同起点、反向走视为相同。
- 根为第1层,最多4层的二叉树最多多少个点?10个点的二叉树高度至少多少?
- 空小根堆插入8、3、7、1,删掉两次最小值,堆顶多少?
- 两个互不相连的三角形组成的图,每个顶点度为2,是否有一条走遍全部边的欧拉回路?
- 一条含5个顶点的有向链有几种拓扑序?5个点无边的DAG有几种?
- BFS可否直接解决边权分别为1、5、10的最短路径?Dijkstra能否一般性处理负边?
- 0/1背包一维dp更新中,容量从小到大遍历会有什么问题?
- 已排序[1,1,3,4],差不超过2的无序下标对有几个?第3小的差是多少?
- 求最小可行整数,区间[lo,hi]且hi已知可行,
while(lo<hi),check(mid)为真时应该更新哪边? - DAG边0→1、0→2、1→2,计入单点路径,字典序第3条路径是什么?从0出发多少条?
- [2,2]所有非空子数组最大值之和多少?贡献法为什么不能左右都找严格更大?
- 0和负数能否直接套“非负整数LSD基数排序”的原代码?分别解释。
- 两个瓶子容量3与5,从(0,0)到某一瓶恰有4,写出一条6步操作序列。
- 两点间只能使用一次“边免费”优惠,为什么
dist[u]一维状态不够? - 为n个对象编不同二进制码,每个码最多k个1。w次测试最多能区分多少对象?n=5、k=1时最少w是多少?
答案与回查
| 题 | 答案与关键理由 | 回查知识 |
|---|---|---|
| 1 | 1、7、6。二进制101与011逐位操作 | F03、C01F03 C++ 类型、运算符、短路和整数边界 C01 固定字长异或变换与整数提升 |
| 2 | 65。第一步得到1+64,第二步右移8为0 | C01C01 固定字长异或变换与整数提升 |
| 3 | 乘法先按int计算并溢出;改为1LL*a*b |
F03F03 C++ 类型、运算符、短路和整数边界 |
| 4 | -3、4。C++余数随被除数符号,归一化后非负 | F13、负进制F13 模运算、最大公约数、逆元与费马小定理 O03 负进制 |
| 5 | O(n log n);外层次数log n,内层每次n | F05F05 复杂度 |
| 6 | 总指针移动不超过2n,不是每个外层都完整重扫n次 | C03C03 第 k 小数对距离 |
| 7 | C(5,3)=10,4红形成5个空位 | F10F10 相邻限制 |
| 8 | C(10,3)=120;A(10,3)=720 | F09F09 加法、乘法、排列、组合与去重 |
| 9 | 30−15−10+5=10 | F12F12 容斥原理与整除计数 |
| 10 | C(6,4)×3=45 | F11F11 圆排列、图中的环与棋盘计数 |
| 11 | 15;至少4层,因为3层最多7点 | F22F22 二叉树、完全树、满树与高度 |
| 12 | 7。删除1与3后剩7、8 | F24F24 堆与 Huffman 编码 |
| 13 | 没有。非零度部分不连通 | F30F30 欧拉通路、欧拉回路与连通性 |
| 14 | 1;5!=120。偏序约束不同 | F31F31 强连通、DAG、拓扑排序与删环 |
| 15 | 都不能直接保证。BFS按边数分层;Dijkstra的定值依赖非负权 | F32F32 最短路、最小生成树与最小割 |
| 16 | 当前物品更新后的状态再次被使用,等效允许重复选 | F34F34 动态规划 |
| 17 | 4对;距离序列0,1,2,2,3,3,第3小为2 | C03C03 第 k 小数对距离 |
| 18 | hi=mid;mid可能就是答案,不排除。为假则lo=mid+1 | C03C03 第 k 小数对距离 |
| 19 | [0,1,2];4条,即[0]、[0,1]、[0,1,2]、[0,2] | C04C04 DAG 路径计数与字典序第 k 小 |
| 20 | 6。两边都严格会让两个位置都把[0,1]归给自己 | C05C05 所有子数组最大值之和 |
| 21 | 0可表示且按0桶处理;负数会出现负余数与负桶下标,需另外设计 | 基数排序O02 LSD 基数排序 |
| 22 | 装满5→倒入3→清空3→将5中剩2倒入3→再装满5→倒入3至满;终态(3,4) | 容器分水O05 容器分水 |
| 23 | 还要区分优惠已用/未用,后续可选择操作不同 | PR04PR04 分层状态最短路 |
| 24 | sum(i=0..min(w,k)) C(w,i);k=1时为1+w,所以w=4 |
PR05PR05 组合编码、测试集合与二进制解码 |
按错因分流。 1—6错得多,先补类型与复杂度;7—14错得多,补选择题基础;17—20错得多,补不变量与手算;22—24错得多,补状态建模。分数只用于定位弱点,不代表真实考试成绩。
三个可反复填写的复习模板
读一段代码: 输入范围______;输出意义______;数组/变量含义______;循环不变量______;一个最小样例______;时间/空间______;危险边界______。
分析一道填空: 空位要的类型______;前后语句的联系______;排除选项的理由______;回代后的边界例______。
记录一道错题: 来源题号______;当时想法______;具体错因______;正确规则及适用条件______;自己改一个数据后的答案______。下一次先遮住答案复述,再看能否说明为什么。
附录:90 道独立选择题的来源与核查索引
本表只列各套前15道独立选择题,阅读程序、完善程序中的小题另见程序篇。“原答案”指用户粘贴资料中的答案,不等于已核对官方答案。“核查相符”表示按当前题干独立推导结果相同;缺图、术语有歧义或依赖隐含条件时单独标明。
| 来源 | 标识说明 | 本表题数 |
|---|---|---|
| S1 | 开头“完整原版 Markdown”,T1—T20;未可靠标年份 | 15 |
| S2 | 开头“第一部分:单项选择题”;未可靠标年份 | 15 |
| S5 | 资料标题自标“CSP-S 2023” | 15 |
| S6 | 资料标题自标“CSP-S 2022” | 15 |
| S8 | 资料标题自标“CSP-S 2021” | 15 |
| S9 | 开头“竞赛题目与解析整理”;未可靠标年份 | 15 |
S1:未标年份资料,T1—T15
| 题号 | 考点与讲义位置 | 原答案 | 核查结果/必要纠正 |
|---|---|---|---|
| S1-T1 | 相同球不相邻、插空,F10F10 相邻限制 | C(6) | 相符。5红形成6空,选5空,C(6,5)=6。 |
| S1-T2 | KMP前缀函数,F26F26 Trie 与 KMP 前缀函数 | A | 相符。按题给0基、真前后缀长度定义为0,0,1,0,1,2,3。 |
| S1-T3 | 线段树访问计数,F27F27 线段树的区间分解与“访问结点数” | B(8) | 有条件相符。只访问有交集分支且含祖先为8;完整覆盖块3;若先递归再判无交集则函数调用11。题问“最少”对应8。 |
| S1-T4 | Trie不同前缀计数,F26F26 Trie 与 KMP 前缀函数 | D(11) | 相符,但原文字结点清单漏了cart末尾t。10个不同非空前缀+根=11。 |
| S1-T5 | 拓扑序数量,F31F31 强连通、DAG、拓扑排序与删环 | D | 相符。链为1,无边为n!,数量不只由n、m简单决定。 |
| S1-T6 | 哈希线性探查,F21F21 哈希表 | D(11) | 相符。74%13=9,9、10已占,放11。 |
| S1-T7 | 最小生成树下界与构造,F32F32 最短路、最小生成树与最小割 | A(7) | 相符。7条边每条至少1,相邻点链达到7。 |
| S1-T8 | BST后序转前序,F23F23 遍历、二叉搜索树与重建 | A | 相符。根6,左根4,右根10;前序6,4,2,5,10,8,12。 |
| S1-T9 | 0-1背包,F34F34 动态规划 | D(44) | 相符。枚举全部32个子集核验,最大44;重量7+4+3+6=20。 |
| S1-T10 | LCA与根,F33F33 树的最近公共祖先 LCA 与重心 | D | 相符。根1必为LCA(12,1),不可能为4。 |
| S1-T11 | 分治递推,F06F06 分治递推、重复计算与快速幂 | C(O(n²)) | 相符。每层费用n²,n²/2,…;原“f(n)更优”措辞不准确,应说额外费用n²占主导。题写O(n²)额外开销,只能保证该上界,若要Θ需额外费用Θ(n²)。 |
| S1-T12 | 小根堆删除最小值,F24F24 堆与 Huffman 编码 | A(10) | 相符。最小两数5、8删去,剩余最小10。 |
| S1-T13 | 容斥,F12F12 容斥原理与整除计数 | A(266) | 相符。并集734,补集1000−734=266。 |
| S1-T14 | Fibonacci重叠子问题,F06、F34F06 分治递推、重复计算与快速幂 F34 动态规划 | C | 相符。根本原因是复用子问题结果,不能只归结于函数调用开销。 |
| S1-T15 | 截止时刻调度、贪心条件,F19F19 贪心选择必须有适用条件 | B | 本例相符,原泛化结论有误。A3,A1,A2,A5,A4可使全部准时、总罚0;最早截止优先不普遍最小化加权超时惩罚。 |
S2:未标年份资料,第1—15题
| 题号 | 考点与讲义位置 | 原答案 | 核查结果/必要纠正 |
|---|---|---|---|
| S2-1 | Linux,F01F01 Linux 命令、编译与运行计时 | A(pwd) | 相符。pwd显示路径;ls默认不一定列隐藏项,原“所有文件”表述需收窄。 |
| S2-2 | 找最大值复杂度,F05、F07F05 复杂度 F07 比较次数的下界 | A(O(n)) | 相符。n−1次比较,紧确阶Θ(n)。 |
| S2-3 | 无限递归与栈,F04F04 递归、调用栈、初值与数组边界 | C | 按考试通常执行模型相符。无终止条件会不断递归;实际编译器可能优化,不能保证所有平台均表现为栈溢出。 |
| S2-4 | 排列,F09F09 加法、乘法、排列、组合与去重 | B(720) | 相符。10×9×8,金银铜有顺序。 |
| S2-5 | FIFO队列,F20F20 栈、队列、后缀表达式与 BFS/DFS | B | 相符。 |
| S2-6 | 递推与下取整,F15F15 递推求值、等差求和与条件分类 | B(5) | 相符。f(1..4)=1,2,3,5。 |
| S2-7 | 欧拉回路,F30F30 欧拉通路、欧拉回路与连通性 | D | 按欧拉图采用连通定义相符,4边环即反例;若允许额外孤立点,连通性须按非零度部分表述。 |
| S2-8 | 二分查找前提,F35F35 二分查找与答案的单调性 | A | 对通常按值查找的数组二分相符。更一般的二分只要求可判定条件单调。 |
| S2-9 | 一般模数的逆元,F13F13 模运算、最大公约数、逆元与费马小定理 | B | 相符。扩展欧几里得适用一般模数,逆元存在还须gcd(n,m)=1且m>1。 |
| S2-10 | 哈希最坏查找,F21F21 哈希表 | D(O(n)) | 相符。开放地址最坏线性;原“开放地址法就是线性探查”错误,线性探查只是其一种。 |
| S2-11 | 完全二叉树容量,F22F22 二叉树、完全树、满树与高度 | A(2^h−1) | 相符。这是“至多”而非每棵h层完全二叉树的确切结点数。 |
| S2-12 | 完全图四元环,F11F11 圆排列、图中的环与棋盘计数 | C(630) | 相符。C(10,4)×(4−1)!/2=630。 |
| S2-13 | 两次数位和、最小整数,F14F14 数位和、数根与构造最小整数 | B(199) | 相符。f(199)=19,f(19)=10;更小x不能达到至少19的所需数位和。 |
| S2-14 | 逆序对最坏数量,F25F25 逆序对与最少相邻交换 | C(k(n−k)) | 相符。所有1在所有0左边达到上界。 |
| S2-15 | 最小割及删法数,F32F32 最短路、最小生成树与最小割 | D(4) | 缺图,不能独立核验。 原文列4组删边集合,但缺少完整边集,不能据此证明最小性、完备性。 |
S5:资料自标2023,第1—15题
| 题号 | 考点与讲义位置 | 原答案 | 核查结果/必要纠正 |
|---|---|---|---|
| S5-1 | Linux建目录,F01F01 Linux 命令、编译与运行计时 | B(mkdir) | 相符。 |
| S5-2 | 四位数排列,F09F09 加法、乘法、排列、组合与去重 | A(96) | 按每个给定数字至多用一次相符。首位4种,其余4×3×2。若允许重复,答案500;需读清题意。 |
| S5-3 | 渐近增长比较,F05F05 复杂度 | A | 相符。m=Θ(n)后A比n log n小,C受n²/log n主导。 |
| S5-4 | 平方和柱子构造,F18F18 构造题 | C(11) | 相符,原第一张摆放表不合法。正确四柱为1→3→6→10;2→7→9;4→5→11;8。放到12至少要5根柱,证明见F18。F18 构造题 |
| S5-5 | 数据结构用途,F20、F21、F22、F24F20 栈、队列、后缀表达式与 BFS/DFS F21 哈希表 F22 二叉树、完全树、满树与高度 F24 堆与 Huffman 编码 | B | 相符。Huffman用于最优前缀编码等,不是为DFS构造。 |
| S5-6 | 二分图与树,F29F29 二分图、染色、奇环与边数 | A | 相符。所有树均可两染色;三角形同时反驳B、C、D。 |
| S5-7 | LCS,F34F34 动态规划 | C(6) | 相符。二维DP独立核验为6;ABCABA是可行公共子序列。 |
| S5-8 | 期望,F16F16 概率、条件平均与期望 | B(35/6) | 相符。36个等可能有序结果,每种首点有5次保留收益。 |
| S5-9 | 位运算、逻辑短路、bool,F03F03 C++ 类型、运算符、短路和整数边界 | A(true) | 结果为true。原5&3的二进制结果误写成101,应为001;左边真使右边短路。原“bool必1字节、int必4字节”“1完全不等同true”表述过强,默认输出true确实为1;按布尔字面量选A。 |
| S5-10 | 快排退化,F08F08 排序 | C(Θ(n²)) | 常规首枢轴、有序互异输入下相符。原求和n+…+1=n(n−1)/2是笔误,应为n(n+1)/2,不影响复杂度。 |
| S5-11 | g++编译命令,F01F01 Linux 命令、编译与运行计时 | A | 相符。-o后接输出名,源文件可在-o之前或之后。 |
| S5-12 | 树重心,F33F33 树的最近公共祖先 LCA 与重心 | C(7个结点) | 相符。奇数点树唯一重心;偶数点不一定两个。 |
| S5-13 | 删除边破坏有向环,F31F31 强连通、DAG、拓扑排序与删环 | C(3) | 缺图,不能独立核验。 资料称1→3→4→1是唯一需破坏的环,须原图才能确认。 |
| S5-14 | 十六进制数根,F14F14 数位和、数根与构造最小整数 | B(11) | 相符。256—416中n≡9(mod15),264至414共11个,独立计数核验。 |
| S5-15 | 重复递归调用,F06F06 分治递推、重复计算与快速幂 | A(O(n)) | 相符。T(n)=2T(floor(n/2))+Θ(1),紧确Θ(n)。不是标准缓存半次结果的O(log n)快速幂。 |
S6:资料自标2022,第1—15题
| 题号 | 考点与讲义位置 | 原答案 | 核查结果/必要纠正 |
|---|---|---|---|
| S6-1 | Linux切换目录,F01F01 Linux 命令、编译与运行计时 | B(cd) | 相符。ls默认不包括所有隐藏项。 |
| S6-2 | time计时,F01F01 Linux 命令、编译与运行计时 | A(约30秒) | 相符。秒表接近real=30.721s;原“real大于CPU就一定I/O密集”等扩展概括不严谨。 |
| S6-3 | 栈序列及连续弹出限制,F20F20 栈、队列、后缀表达式与 BFS/DFS | D | 相符。afedcb需要末尾连续弹5次;其余可安排每次最多连弹2次。 |
| S6-4 | 排序最坏复杂度,F08F08 排序 | C(归并) | 相符。归并O(n log n),常规插入、冒泡、快排最坏O(n²)。 |
| S6-5 | 基数排序稳定分配,F08F08 排序 | A | 在仅改变一个记录值、未破坏算法控制和其他数据的模型下相符。剩余未变记录仍有序。 |
| S6-6 | 大小端,F02F02 进制、位权、容量单位与字节序 | B(EF、DE) | 按题默认32位unsigned相符。原“*p即低8位”不准确,*p是最低地址字节,大端时是高位字节。 |
| S6-7 | 三叉树前序编号,F22、F23F22 二叉树、完全树、满树与高度 F23 遍历、二叉搜索树与重建 | C(97) | 依赖“每层都满”的理解。 满5层三叉树可得97,原文97是第4层而非第5层。若按通常“完全树允许末层不满”,101个结点的完全三叉树深度仍5,前序第100号的父号却为98;仅现题干不唯一。 |
| S6-8 | 强连通不等于完全图,F31F31 强连通、DAG、拓扑排序与删环 | B | 按至少2个点的通常出题背景相符。单点无边图可强连通却度0,原A、D的概括需排除此边界。 |
| S6-9 | 2正则图与欧拉环,F11、F30F11 圆排列、图中的环与棋盘计数 F30 欧拉通路、欧拉回路与连通性 | D((n−1)!/2) | n≥3、简单图且欧拉回路覆盖所有边时相符。原“全偶度即欧拉”“2正则必单环”缺连通条件,已更正。 |
| S6-10 | 两人团队组合,F09F09 加法、乘法、排列、组合与去重 | A(28) | 按选一支两人队相符。若分完8人为4支无标号队则是105,不能混淆。 |
| S6-11 | 乘法原理、允许重复,F09F09 加法、乘法、排列、组合与去重 | C(676000) | 相符。26²×10³。 |
| S6-12 | 哈希线性探查,F21F21 哈希表 | D(2) | 答案相符,原中间插入表多处列错。正确位置依次1、3、4、9、5、0、2。 |
| S6-13 | 倍增循环,F05F05 复杂度 | B(O(n log n)) | 相符。内层约log₂n次,更新k的数值大小不增加运算次数。 |
| S6-14 | 最大值比较下界,F07F07 比较次数的下界 | B(n−1) | 相符。每个非最大值至少输一次比较。 |
| S6-15 | Ackermann递归,F15F15 递推求值、等差求和与条件分类 | B(7) | 相符。ack(1,n)=n+2,ack(2,n)=2n+3。 |
S8:资料自标2021,第1—15题
| 题号 | 考点与讲义位置 | 原答案 | 核查结果/必要纠正 |
|---|---|---|---|
| S8-1 | Linux列目录,F01F01 Linux 命令、编译与运行计时 | A(ls) | 相符。若要求隐藏项,要ls -a。 |
| S8-2 | 二进制加法,F02F02 进制、位权、容量单位与字节序 | B(01000000) | 相符。42+22=64;原A、C文本重复不影响正确答案。 |
| S8-3 | 调用栈溢出,F04F04 递归、调用栈、初值与数组边界 | A | 相符。 |
| S8-4 | 排序稳定性,F08F08 排序 | C(堆排序) | 对常规实现相符;稳定归并需相等时优先取左段。 |
| S8-5 | 同时找最大最小,F07F07 比较次数的下界 | C(3n−2) | 相符。n对内部比较+n−1找最大+n−1找最小。 |
| S8-6 | 平方哈希、线性探查,F21F21 哈希表 | C(7) | 相符。插入6落6,7从5探查到7。 |
| S8-7 | 非连通简单图极值,F28F28 图的基本量、存储与极值 | C(10) | 相符。最多C(n−1,2)边,n=9不足,n=10可由K₉加孤立点达到36。 |
| S8-8 | 二叉树最小高度,F22F22 二叉树、完全树、满树与高度 | B(11) | 相符。1023<2021≤2047。 |
| S8-9 | 前序与中序相同,F23F23 遍历、二叉搜索树与重建 | D | 结点标识互异、比较访问顺序时相符,所有非叶子仅有右孩子。 |
| S8-10 | 最少相邻交换、逆序对,F25F25 逆序对与最少相邻交换 | A(7) | 相符,原解析列举混乱。各位置逆序贡献3、0、1、2、1、0,总7。 |
| S8-11 | 递归指数与费马,F13F13 模运算、最大公约数、逆元与费马小定理 | A(1) | 相符。实际为5²² mod23,不是5²³。 |
| S8-12 | Fibonacci朴素递归复杂度,F06F06 分治递推、重复计算与快速幂 | C(O(2ⁿ)) | 相符。更紧确为Θ(φⁿ),O(2ⁿ)是题给正确上界。 |
| S8-13 | 不相邻选取、斐波那契计数,F10F10 相邻限制 | C(54) | 相符。含空集55,减1。 |
| S8-14 | 等腰三角形分类,F15F15 递推求值、等差求和与条件分类 | C(165) | 相符。等边9+非等边156;对1—9三元组独立枚举核验165。 |
| S8-15 | 有向图最短路,F32F32 最短路、最小生成树与最小割 | B(19) | 缺图,不能独立核验。 没有完整边集与权值,无法重算A到J。 |
S9:未标年份资料,第1—15题
| 题号 | 考点与讲义位置 | 原答案 | 核查结果/必要纠正 |
|---|---|---|---|
| S9-1 | 进制数比较,F02F02 进制、位权、容量单位与字节序 | C(2¹⁰) | 相符。四数分别556、511、1024、559。 |
| S9-2 | 操作系统功能,F01F01 Linux 命令、编译与运行计时 | B | 相符。控制管理软硬件资源;编译器另负责源程序翻译。 |
| S9-3 | 原始视频存储量,F02F02 进制、位权、容量单位与字节序 | B(90G) | 按二进制容量约定相符,严格应90 GiB,约96.637 GB。原从bit直接写GB的等式漏展示÷8与单位换算。 |
| S9-4 | 栈模拟,F20F20 栈、队列、后缀表达式与 BFS/DFS | B(a) | 相符。操作后栈底到顶a,c;e、f未进栈。 |
| S9-5 | 哈希函数冲突,F21F21 哈希表 | D | 相符。D四个地址为1、3、5、9;其余均有重复地址。 |
| S9-6 | 贪心适用性,F19、F32、F34F19 贪心选择必须有适用条件 F32 最短路、最小生成树与最小割 F34 动态规划 | B(0-1背包) | 按经典算法背景相符;D的Dijkstra式贪心必须补非负边权,任意带负边的单源最短路不能如此概括。 |
| S9-7 | 邻接表DFS复杂度,F28F28 图的基本量、存储与极值 | A(O(n+e)) | 相符。顶点各一次,边扫描常数次。 |
| S9-8 | 二分图最大边数,F29F29 二分图、染色、奇环与边数 | A(144) | 相符。两部12、12的完全二分图达到。 |
| S9-9 | BFS与队列,F20F20 栈、队列、后缀表达式与 BFS/DFS | C | 相符,指常规逐层BFS实现。 |
| S9-10 | 同余方程,F17F17 同余方程与小范围枚举 | C(50<n<60) | 相符。正整数范围内n=53。 |
| S9-11 | 等差和、楼层与段数,F15F15 递推求值、等差求和与条件分类 | C(15层) | 相符。14段楼梯总1050,13段910。 |
| S9-12 | 后缀表达式,F20F20 栈、队列、后缀表达式与 BFS/DFS | D(abc+*d-) | 相符。 |
| S9-13 | 棋盘不同行列选格,F11F11 圆排列、图中的环与棋盘计数 | B(72) | 相符。16×9/2=72。 |
| S9-14 | 朴素Dijkstra复杂度,F32F32 最短路、最小生成树与最小割 | D(O(n²)) | 复杂度相符。若要求算法正确求最短路,还应具备非负边权条件。 |
| S9-15 | 信息论人物,F36F36 网络、编码与计算机常识 | C(香农) | 相符。 |
本批选择题最应防止背错的结论
- 欧拉回路不能只看度全偶;还要检查有边部分的连通性。
- Dijkstra需要非负边权;MST不要求边权非负,二者也不是同一问题。
- 最早截止优先不能保证一般加权超时惩罚最小;S1调度题是通过构造0惩罚证明。
- 名为quick_power不保证O(log n);相同递归调用写两次仍要算两次。
- 线段树“完整覆盖块”“访问结点”“函数调用”不是同一计数。
- S6-7的“完全三叉树”有术语歧义;用97时必须知道是在满层解释下。
- S2-15、S5-13、S8-15缺图,现阶段不能把资料答案当成独立验证结果。
独立计算核验记录。 对S1背包枚举32个子集;对S5 LCS做二维DP;对S8等腰三角形枚举729个数位三元组;对S5数根按模15计数;对S8逆序对直接数15对位置;对S6三叉树用不同结点数的完全三叉树反查前序父号。这些核验用于审校资料,不要求考场写程序完成选择题。
程序审校与答案索引:S1—S4
审校依据是用户提供的展示代码,不能用“预想真题可能长什么样”替代当前代码。来源年份未另行认证;S1 只称练习材料。以下代码修訂的含义、原题意图与字面结果已在各专题分开说明。
| 来源 | 审校结论 | 证据或处理 |
|---|---|---|
| S1/T16PR01 排列 DFS、剪枝与恢复现场 | 主答案一致;判重删除后共有 16 项 | 穷举长度 3 的 3 元序列,按结尾统计为 7、4、5。n=3、4、5 原排列答案为 3、11、53 |
| S1/T17 第1判断解析PR02 两个鸡蛋、最坏情况与递减分段 | n=6 的最小 w 是 3,不是 4;给定次数 3 仍正确 | 实际编译运行输入 6 5 2,检查轨迹 3、5、4 |
| S1/T17“最优”概念PR02 两个鸡蛋、最坏情况与递减分段 | 原方法不是“已知 k∈[1,n]”模型下所有 n 的绝对最优 | 可排除最高候选而不用真实投蛋;w 次两个蛋可区分 1+w(w+1)/2 个阈值,n=7 理论 3 次,原代码取 4 |
| S1/T18PR03 折半搜索、快速幂与带重数的配对 | 快速幂本意正确,原变量类型有中间溢出风险 | x*x 是 int,最后一轮即使平方结果不再使用仍执行;signed 溢出为未定义行为,不能声称必然模 2³²。专题以不溢出为前提保留原类型 |
| S1/T18 复杂度PR03 折半搜索、快速幂与带重数的配对 | 两半规模在奇数 n 时应有 ceil(n/2) | 增补奇数 n 与 m=1 的范围解释,不改原选项 D 的意图 |
| S1/T20PR05 组合编码、测试集合与二进制解码 | 外部接口缺定义,不能独立链接;特殊参数缺约束 | 仅编译为目标文件检查语法;n=1 理论可 0 次,k=0且n>1无解;注明签名位宽、组合数溢出 |
| S2/T16-2PR06 位运算化简与有深度限制的快速排序 | 原判错应改对 | 快排枢轴为1,右指针先从4移到3;实测 5 5 1 输出 1 1 5 5 5 |
| S2/T17-2PR07 子序列计数 DP、二进制值与倒序更新 | 原判对应改错 | main 先输出 solve1 再 solve2,输入 11 2 10000000001 实际算法结果顺序 23、32;不是32、23 |
| S2/T17 m=10PR07 子序列计数 DP、二进制值与倒序更新 | 原代码数组越界,不能严格断言确定输出 | dp 仅1024项,j=1023且字符为1时访问dp[2047];即使加数为0仍越界。教学修订加 k<lim |
| S2/T17 相等条件解析PR07 子序列计数 DP、二进制值与倒序更新 | “没有任何0开头子序列”说法过强 | 全0串显然含前导0子序列,但其值全0,不影响和;应说不存在“有正贡献的前导零子序列”,等价于无0在1之前 |
| S2/T17 最大差值PR07 子序列计数 DP、二进制值与倒序更新 | 2059 正确 | 对 n=m=8 的256个01串完整枚举,最大由01111111达到 |
| S2/T18PR08 完全二叉树、字符串拼接哈希与去重 | 数值83、10正确;泛化解释须限于该完全二叉树 | 直接生成实际中序标签串交叉核验;任意二叉树仅中序不能识别结构,双哈希仍可能碰撞 |
| S2/T19PR09 两个有序数组的第 k 小和 | 展示代码类型拼接错误,照填无法编译;还会计数溢出 | 形参an为int而选项使用an-a,返回int而选项返回a+l,调用却传n。修复为末指针接口;k/rank改long long,中点防溢出 |
| S2/T20PR10 严格次短路 | 缺失原程序且展示骨架逻辑错误,不能认证五空答案 | ②③⑤未出现在代码;输入、路径输出略;最短更新后才转存“旧最短”;第二层head未映射;未排除相等长度;缺cstring。标明材料声称A,A,B,A,A,另给完整教学修订,未擅造缺失原稿 |
| S3/完善程序2 | 五空正确,解析多处表述错 | 选项A本来就是a[j]<max;max实际int。用循环不变量证明B;全部3279个小数组与暴力吻合 |
| S4/阅读程序1 | 主答案正确,整数提升和返回窄化须解释 | 只把参数改unsigned int,返回类型仍unsigned short;x=32768时原32896、修改后41088 |
| S4/阅读程序2 | 主答案正确,n=0不受代码支持 | f[1]会越界;正整数范围内两算法一致。删reverse时n=4第一行17,正确15 |
| S4/阅读程序3 判断3 | 结论5正确,但排序解析抄错 | 正确排序−4,−3,1,2,5,而不是−4,1,2,3,5;同时负数示例违反前述ai≥1 |
| S4/阅读程序3 选择5 | 材料B不能对全部合法输入成立 | 将>改为>=后m=0可令j越过i并越界,例[1,1,2],k=1;修复严格计数并把上界扩大1后新答案恒为D+1 |
原题子题答案索引(S1/S2 共55个子题)
“算法意图”指在已说明的必要修复与数值范围下;遇到原稿不完整者明确不认证。
| 来源 | 子题顺序 | 答案/审校后结论 |
|---|---|---|
| S1/T16PR01 排列 DFS、剪枝与恢复现场 | 1、2、3、4、5、6 | 对、对、错、A(11)、D、C(16) |
| S1/T17PR02 两个鸡蛋、最坏情况与递减分段 | 1、2、3、4、5、6 | 对、错、对、B(1)、C(O√n)、A(100,14) |
| S1/T18PR03 折半搜索、快速幂与带重数的配对 | 1、2、3、4、5、6 | 错、对、对、B(8)、D、D |
| S1/T19PR04 分层状态最短路 | ①②③④⑤ | A(0)、B(d[u][used])、B(d[v][used])、C(d[u][0])、C(min两层) |
| S1/T20PR05 组合编码、测试集合与二进制解码 | ①②③④⑤ | B(容量<n)、B(prev_permutation)、D(code[j][i]==1)、A(第i位)、B(码字相等) |
| S2/T16PR06 位运算化简与有深度限制的快速排序 | 1、2、3、4、5 | 对、对、错、B(按位或)、C(95) |
| S2/T17PR07 子序列计数 DP、二进制值与倒序更新 | 1、2、3、4、5、6 | 算法意图:对、错、对、B(11)、C(665)、C(2059);m=10原代码越界 |
| S2/T18PR08 完全二叉树、字符串拼接哈希与去重 | 1、2、3、4、5、6 | 对、错、对、C(中序)、A(83)、C(10) |
| S2/T19PR09 两个有序数组的第 k 小和 | ①②③④⑤ | 指针接口修订后均A:an-a;a[mid]>ai;a+l;两数组最大值之和;rank<k |
| S2/T20PR10 严格次短路 | ①②③④⑤ | 原材料标A、A、B、A、A;由于程序缺失和骨架错误,不认证为可执行程序的正确填空 |
验证记录
- 独立计算复核:排列/可重复序列穷举、子序列求和、256个串最大差值、树中序串核验、分治最大值3279例穷举;执行通过。
- 编译及样例复核:PR01—PR15共15个C++代码块均通过g++ C++17编译,其中外部接口PR05只编译目标文件;其余14个程序共31组输入输出样例全部通过。
- PR10教学修订另外在100个固定种子随机正权有向图中,与独立的“每顶点弹出两个不同距离”参考算法比较;严格次短距离和还原路径权重均通过。允许零权的逻辑保留,随机验证范围为正权图。
- 修订版程序的编译成功不等于恢复了缺失的官方原稿;S2/T19、T20特别保留此区分。
程序审校:S6—S9
- S8 阅读(1)源文本明确把原程序删节成八个三维点、24 个输入,而题目样例均只有 8 个数,引用的 t、x、y 等也不在所给代码中。无法据此还原原题;讲义将给出球体积交的独立教学实现,明确不是原代码复原,原题 16—18 行号判断不作确认。
- S8 阅读(2)所给 solve1 与 solve2 均为两次递归加线性跨中点扫描,均 O(n log n)。第 25 题应 C,不是文本 B。第 23 题原行号对应不明,不采纳“其他情况执行一次”的解释。F 值初始 -1e9 若没有下界约束也不能普遍等同于真实负无穷。
- S8 阅读(3)encode 没有补等号,实际 CSP2021csp 编码应 Q1NQMjAyMWNzcA,源答案多出 ==;第 32 题题干残缺,且 size() 输出无符号长度,不可能直接得到 -1。decode 的 int 累积移位可能溢出,教学版改 unsigned。
- S8 魔法数字原代码 F[0] 全局初始化为 0,使 !Vis[i] && F[i] < F[x](x=0)一次也选不到;同时 break 错误地提前结束取最小值扫描。修正版设置 F[0]=INF 且删除 break;空答案意图仍 D A D C。原 RMQ 代码只提供建树前半段,后续 4 个空缺代码,需明确教学补全。
- S6 Sunday O(nm) 结论正确,反例 s=aaaa...、t=bbbba 不能证明该界,因为第一字符即失败。应使用 t=aaa...aba,末字符为 a(跳步 1)且近尾部失败。
- S6 基数排序题只说 val 在 int 范围内,未排除负数;负余数会成为负下标。base 最后一轮乘 k 也可能溢出。讲义补非负前提,教学代码用 long long base。
- S6 负基数 n 为负时不可写 log_k n;应 O(log_k(|n|+1))。负基数位数不等于正基数位数,原 int 在 INT_MIN 时 ans-n 可能溢出,教学版 long long。
- S7 归并第 k 小按所给 C B C C A 填完,n=1、a1=[0]、a2=[1]、k=1,会读 a2[-1]。对 n=1..5、元素0..3全部非降数组对和所有 k 共43992例,发现4877例负下标;其余值与排序真值相同。讲义保留选项意图,补另一侧所选数量为0的处理,并提供标准划分二分教学版。
- S7 分水 DFS 不可未经核验断言 dfs(0,0) 错误。独立 BFS 比较 a,b=1..30、全部合法 c 共18445例,根答案全部一致;(3,5,4) 均6步。但“f[x][y]一律是该状态最短距离”不成立:(a,b,c)=(1,5,2),从根DFS后 f[1][5]=6,而独立BFS真值4,根仍正确输出4。因此不将一次性有环DFS记忆化推广为最短路算法;提供BFS教学代码,未宣称发现根答案反例。
- S9 快速选择缺少 L==R 基例,按所给全局数组 n=1,k=1,d[0]=5 时读取 d[1]=0 并递归,输出0而非5;教学版补基例。全相等时复杂度取决于 k:k=1为Theta(n),k=n为Theta(n^2);源题选D只能按最坏k理解。源所谓“官方无正确选项”尚未核验官方,不当作官方结论。
- S9 位或题“有128则128|127=255”假设127在输入中,不成立;可直接说明128与任何不同的非负数相或至少128,且若全等128则输出-1,不会127。恒等式需中间加法无溢出,教学代码直接用 |。
- S9 字符串旋转第5题三组前置输入已OCR损坏(长度不等),按字面均输出-1。独立BFS验证长度4,6,8逆序最短距离4,14,28;分层双向BFS验证字面长度10逆序目标为46(两侧各86576状态),不是68。68可能对应长度12,但未作证明,不凭差分猜规律。
- S9 最优子序列按分值的高低位可加性推导 Max 状态;随机3000组4位(高低各2位)、长度1..19,与O(n^2)DP逐例相同。还应补充题目分值本身满足逐位三角不等式,若允许任意长度子序列,选完整序列就最优;此事实可由 |bit_x-bit_z| <= |bit_x-bit_y|+|bit_y-bit_z| 乘每位权重 2^bit+1 逐位求和证明。原优化DP可作通用可分离转移教学,但本题有O(n)更简方法。
编译及运行验证
- 讲义全部18个完整C++代码块均以g++ -std=c++17 -O2 -Wall -Wextra编译通过,无诊断。
- 已验证Sunday首匹配与失败、基数排序、负进制-13及INT_MIN、归并零前缀、分水可行与不可行、球交体积两样例、最大子段全负且低于-1e9、Base64无填充及解码、魔法数字、位或、quickselect单元素、旋转方向、分数背包42/5、两种子序列算法259。
- C++ RMQ完整Four Russians教学版与直接区间max对拍4000个随机询问(80个数组,含n=1、重复值、负值);C++快速选择80例、修正归并第k小80例与排序对拍通过。
- 修正归并边界的算法进一步用Python逐例穷举43992例,全部与排序真值一致。