CSP-S · FIRST ROUND

把真题变成知识点
把知识点用到新题上

明晚与后天早上的冲刺路线,加上可随时查阅的完整讲义。每段程序说明关键逻辑、复杂度、边界与变式,遇到原资料错误明确纠正。

37 个基础专题30 段程序90 道选择题索引24 道自测
从下方“两时段冲刺”开始。表格中的绿色按钮可直达对应知识点;点击“返回刚才的位置”回到原表格。搜索会按小节筛选;打印只包含当前显示内容。点击“已复习”在此浏览器保存进度。资料中缺图、缺码、原错解与教学重写均单独说明。
没有找到这些关键词。可以缩短词语,或清空搜索查看完整目录。
先看这里

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新目录 不运行危险命令来练习记忆
先看这里

阅读程序的六步方法

  1. 圈出输入范围和数据类型:是否允许0、负数、相等元素、空集合?容量是否包含最后一个下标?
  2. 找输出式,然后给关键变量写中文含义。不要第一遍就逐行硬算所有代码。
  3. 写入口不变量。例如二分“答案在[l,r]”,DFS“前k−1位已填好”,DP“本轮只用上轮状态”。
  4. 用最小样例模拟:1个元素、全相等、递增、递减、答案在边界;手算时记录更新前后。
  5. 改代码题先找反例。一个合法反例就能推翻“一定不变”,几十个正常样例不能证明永远不变。
  6. 最后分析复杂度:统计每层实际工作、重复状态和指针总位移;空间包括数组与递归栈。
先看这里

完善程序的五个检查点

先定量纲。 空里应是下标、数量、权值、布尔条件还是一整个状态?很多选项无需跑代码就能排除。再定方向。 左右转移是否守恒,入度先减还是后减,排名从0还是1开始。再定边界。 [l,r]长度r−l+1,[l,r)长度r−l;指针访问前应先判范围。再用小例。 第1小、最后1小、只有一条边、无法到达。最后回代。 连续两个空要联合检查,不能逐空各选一个“看起来对”的表达式。

先看这里

120分钟训练配速

参考安排:单选25分钟、阅读45分钟、完善35分钟、检查与答题卡15分钟。具体题目难度不同可以调整,不要求按题号死磕。单选卡住2分钟先标记;读代码先拿能直接模拟的小问;完善程序能凭局部状态确定的空先做。最后优先查:题号错位、判断题涂法、问正确还是不正确、复杂度漏排序、答案是次数还是数值。以考场规则为准,不假设能使用电脑运行代码。

先看这里

最值得避免的十种临考误记

  1. “资料写了答案就可信”:附件确有代码残缺和错解,主讲义纠错优先。
  2. “某算法的结论等于给定代码一定做到”:漏基例、越界会改变行为。
  3. “只有输出变量需要long long”:中间乘法先溢出仍会错。
  4. “排序后相等元素没有区别”:计数和稳定性题必须保留原下标含义。
  5. “记忆化DFS能处理任意有环最短路”:依赖尚未定值时不成立。
  6. “见二分就把右边界改mid−1”:边界模板必须整套一致。
  7. “所有偶度图都是欧拉图”:必须满足相应连通条件。
  8. “贪心排一个看着合理的顺序就行”:先读目标,最大延迟与加权迟交罚金不同。
  9. “复杂度一样,实际次数也一样”:常数、输入分布和数据范围仍有影响。
  10. “两天要学完所有高级内容”:先完成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题目能独立解释,不以看过多少页为目标。

37个基础知识点

第一篇:选择题知识体系与考前变式

本篇把所给六组、共 90 道独立选择题合并为知识点。S1、S2、S9 没有可靠年份标注;S5、S6、S8 分别是资料自标的 2023、2022、2021。题号可在后面的“选择题核查索引”反查。这里的“必看”表示短期复习优先级,不是对今年出题的预测。

明晚先读: F01—F08、F09—F15、F20—F25、F28—F34。**后天早上:**遮住每节变式答案,独立做一遍,再回看错误;F16—F19、F26—F27、F35—F37 在有余力时补。不要为了临考背熟陌生模板而挤掉模拟程序、计算复杂度和排除错误选项的时间。

37个基础知识点

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 也成立

timereal 是从开始到结束的实际经过时间,最接近秒表;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。

37个基础知识点

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=55900101010₂+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 EFunsigned 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。

37个基础知识点

F03【必看】C++ 类型、运算符、短路和整数边界

值与类型。 整数除法截去小数,5/2=2;正数的取模是余数,17%5=2bool 只有 falsetrue 两个值,整数转布尔时 0 为假、非 0 为真;默认 cout << true 输出 1,使用 boolalpha 后输出 true。不能由此说数值 1 与真完全无关。intbool 的字节数依实现而定,竞赛常见环境 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 && ba || 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³¹−1long 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;也是全部程序题的基础。

37个基础知识点

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。

37个基础知识点

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 nn 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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

F14【必看】数位和、数根与构造最小整数

数位和是各位数字相加;数根是反复求数位和直到只剩一位。b 进制下 b≡1 (mod b−1),所以数位和与原数模 b−1 同余。正整数的 b 进制数根为 1+(n−1) mod (b−1);n=0 的数根为 0,需单独处理。

S5-14 的区间 100₁₆=2561A0₁₆=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。

37个基础知识点

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+1ack(1,n)=n+2ack(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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

F20【必看】栈、队列、后缀表达式与 BFS/DFS

后进先出,只从栈顶入/出;队列先进先出,队尾入、队首出。DFS 常用递归栈或显式栈;BFS 按层扩展,常用队列。优先队列按优先级出队,不保证先到先出。

验证出栈序列。 按给定入栈顺序推进,目标元素若还未入栈,就把它前面的未入栈元素依次压入;然后必须等于栈顶才能弹出。附加“不能连续三次出栈”时,同时记录连续出栈次数,入栈后清零。S6-3 的 afedcb 在 a 出栈、其余全部入栈后,必须连续弹5次,违背条件。

后缀式。 操作数在前、运算符在后;可按表达式树做后序遍历。a*(b+c)-dabc+*d-。求值时遇操作数入栈,遇二元运算符先弹右操作数,再弹左操作数,压入计算结果。

变式。 后缀 8 3 2 * - 等于多少?答:8−3×2=2。 入栈1、2、3能否按3、1、2出栈?答:不能,3出后栈顶是2。

对应:S2-5;S6-3;S9-4、9、12。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

F25【必看】逆序对与最少相邻交换

逆序对是 i<ja[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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

F30【必看】欧拉通路、欧拉回路与连通性

欧拉通路经过每条边恰好一次;回路还要求起终点相同。它讨论的是边不重复,不是每个顶点只能经过一次(后者是哈密顿问题)。

对有边的无向图,在所有非零度顶点位于同一连通分量的条件下:全为偶数度当且仅当有欧拉回路;恰两个奇度顶点当且仅当有起终点不同的欧拉通路。许多考试定义直接把欧拉图限定为连通图,按题目约定处理孤立点。

2正则图。 每点度为2的简单无向图是若干互不相交环的并,未必连通。若还要求一条欧拉回路经过全图所有边,才必须是覆盖所有n点的单环,n≥3时不同带标号图数 (n−1)!/2。S6-9 的答案D正确,但“偶数度自动是欧拉图”的原解释缺连通条件。

变式。 两个互不相连的三角形,每点度2,有覆盖全部6条边的一条欧拉回路吗?答:没有,不连通。 一个4边环的边数是偶数,是否欧拉图?答:是,边数奇偶不是判定条件。

对应:S2-7;S6-9。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。

37个基础知识点

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。 ABCAC的LCS和最长公共子串长度分别多少?答:2和1。

对应:S1-9、14;S5-7;S9-6。

37个基础知识点

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。

37个基础知识点

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;其余为补充。

37个基础知识点

F37【临场方法】读题限定词与构造反例

选择题优先圈出“最坏/平均”“至多/至少”“有序/无序”“连通/非连通”“可重复/互异”“恰好/不超过”“包括根/不包括根”“边数/删法数”。这些词往往决定公式。

对于“一定成立”,找一个反例即可推翻;“可能成立”只需构造一个例子;“最大值”既要给出可达方案,又要排除更大值。复杂度题先写计数式,递归题先写递推式,栈题画栈,哈希题画地址表,图题标访问顺序和距离。

资料中有原图缺失时,不背未经验证的数字。 本批选择题缺图的是S2-15、S5-13、S8-15;能复习相应方法,但不能凭现有文字保证原答案正确。

变式。 “偶数个结点的树一定有两个重心”如何1秒反驳?答:4点星形树只有中心一个重心。 “度全偶就一定有覆盖全部边的欧拉回路”如何反驳?答:两个互不连通的三角形。

程序:S1与S2

程序专题:S1与S2的10段程序

本部分覆盖 S1—S2 中全部 10 组程序。来源编号只定位你提供的材料,不据此推定年份。带“修订”的代码是为消除材料中的缺漏、类型错误或越界所作的教学版本;不能把修订后的行为冒充原程序行为。做阅读题时依次写下:输入约束→变量含义→循环不变量→输出意义→手算最小样例→复杂度。

程序:S1与S2

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 后三个数全被标记,其他分支无法继续。

程序:S1与S2

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;这是允许另有“顶层也不碎”的传统楼层覆盖模型。

程序:S1与S2

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;两边仍须有序。

程序:S1与S2

PR04|分层状态最短路:至多一条边免费

来源:S1/T19。必会程度:高。 一个顶点应拆成“没用优惠”和“已用优惠”两个状态,d[u][0/1] 保存到该状态的最短距离。普通过边保持层数,费用加 w;免费过边由 0 层到 1 层,费用加 0。状态图边权仍非负,所以用 Dijkstra。不能只保存 d[u]:到同一点较便宜但已用优惠的路线,与较贵却保留优惠的路线,未来潜力不同,二者都要保留。

五空:A、B、B、C、C,内容分别为 0d[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 时不再能直接用空路径满足次数条件。

程序:S1与S2

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)<nprev_permutation(bits.begin(),bits.end())code[j][i]==1(signature>>i)&1code[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) 次。

程序:S1与S2

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”不会在同一位重叠。

程序:S1与S2

PR07|子序列计数 DP、二进制值与倒序更新

来源:S2/T17。必会程度:高。 solve2 的掩码枚举“选哪些下标”,保持原顺序拼接;它统计长度≤m 的所有子序列数值之和,允许前导零,相同字符串由不同下标组成仍分别计数。solve1dp[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: 子串与子序列能互换吗?答:不能;子串要求下标连续,本题任意选择下标。

程序:S1与S2

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: 为什么要把长度纳入相等判断?答:第一哈希里 101 数值相同;长度可区分前导零导致的歧义。第二哈希的字符偏移也能帮助区分,但仍不能省去对整体定义的理解。变式 3: 把合并顺序改为根+左+右,含义?答:先序标签串;倒序计算子树的处理顺序并不因此变成“后序字符串”。

程序:S1与S2

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-aa+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]、边界和、答案的数据范围。

程序:S1与S2

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: 普通最短路更新成功时,旧最短为什么要留?答:它依然是一条合法路线,并可能恰好是第二小的不同长度;覆盖前不保存就丢失候选。

程序:S3与S4

专项讲义:位运算、二分、DAG 与区间贡献

本章覆盖 S4 的3段阅读程序与 S3 的2段完善程序。P0 内容先读“本质”和“手算”;P1/P2 推导用于查漏。

程序:S3与S4

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)。第二个单选问的是第一条赋值后,不是最终输出。

程序:S3与S4

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。

程序:S3与S4

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。

改代码题要先检查内存与不变量。

  1. hi=mid 改为 hi=mid-1:套用了另一种二分模板,可能跳过答案。在单调阈值模型中输出可能正确,也可能比正确值小1,不保证始终正确。
  2. lo+(hi-lo)/2(lo+hi)>>1:在本题非负、和不溢出的范围内等价;推广到大整数边界不能直接替换。
  3. 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→二分”这一组合。

程序:S3与S4

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。

程序:S3与S4

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

程序专题精讲:S6—S9

这部分按“读出程序在做什么—写出不变量—检查边界—推导填空—迁移变式”的顺序学习。P0 表示考前必须掌握,P1 表示高频进阶,P2 表示最后有余力再看。来源码对应用户材料;S6 自带 2022 标题、S8 自带 2021 标题,S7、S9 未凭印象推定年份。

**材料质量说明:**S8 有两段程序被原材料主动删节,还有若干答案与代码矛盾。以下明确区分“所给代码的注释版”“修正版”和“教学重写”;没有把缺失代码伪装成完整原题。考前应记住算法和可验证的推导,不背损坏文本中的选项字母。

程序:S6至S9

O01|Sunday 字符串匹配:跳的是窗口后面的字符【P1】

来源:S6 阅读程序(1)。目标是在文本串 s 中找模式串 t 第一次出现的 0 基下标,找不到返回 -1。

知识点与不变量

设文本长度 n、模式长度 m。当前比较窗口是 s[i..i+m-1]。本次失败后看的是 s[i+m],即窗口右边紧挨着的字符 c。

所给程序主干注释版

#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。

易错与变式

  1. 变式:s="aaaaa"t="aaa",若要求所有可重叠出现位置?答案为 0、1、2;首次成功不能直接 return。只找互不重叠匹配时才在成功后跳 m。
  2. 变式:模式 abaca 的 shift 值?答案 a→1,b→4,c→2,其他字符→6。若预处理从右到左仍直接覆盖,会留下最左位置,可能跳过答案。
程序:S6至S9

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。

变式

  1. 将回填循环改为 j=0..n-1,但仍从每组末尾向前填,会怎样?不稳定。例 [21,11] 十进制第一趟得到 [11,21],第二趟可排序正确,但一般不能保证;更直接例 [12,11] 第一趟为 [11,12],第二趟同为十位1又反转,错误得到 [12,11]
  2. 32 位非负整数按 8 位一组排序,每趟 256 桶,做几趟?最多 4 趟,时间 O(4(n+256))。若改为有符号整数,须单独处理符号,不能把负数直接用原公式当下标。
程序:S6至S9

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 不能删除。

变式

  1. 把 -13 转为 -2 进制。余数依次 1、1、1、0、1、1,答案 110111;代回 -32+16+4-2+1=-13
  2. 已知 -3 进制数 1201,求十进制值。答案 1×(-27)+2×9+0×(-3)+1=-8。可用 Horner 法从左往右 v=v*(-3)+digit,O(位数)。
程序:S6至S9

O04|两个有序数组的第 k 小:边界是算法的一部分【P0】

来源:S7 完善程序(1)。第 k 小按重复次数计算,k 从 1 开始;数组下标从 0 开始。

填空意图与必须修正的边界

所给空的预期依次 C、B、C、C、A,即 m1+m2a1[m1]<=a2[m2]left1>right1y=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,允许某一前缀长度为零,这是前面越界错误的根源。

变式

  1. A=[1,2,2],B=[2,3,5],第 4 小是多少?答案 2;重复值必须分别占名次,不能去重。
  2. 两数组总长度为偶数,求中位数。答案是第 N/2 小和第 N/2+1 小的平均值;用 ((long long)x+y)/2.0,防加法溢出和整数除法截断。
程序:S6至S9

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 步。

变式

  1. a=4,b=6,c=3 能否完成?不能。可量水量必须是 gcd(4,6)=2 的倍数。一般 c<=max(a,b) 且 gcd(a,b)|c 时可行。
  2. 灌满代价为 2,倒水代价为 1,倒空代价为 0,还能直接 BFS?不能;用非负边权 Dijkstra。若所有边仅0或1,可以用 0-1 BFS。
程序:S6至S9

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:

#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) 必定不同”。

变式

  1. 两单位球心距2,交体积多少?0,外切面为一点,三维体积为0。
  2. 两单位球心距1,求体积并?答案 8π/3-5π/12=9π/4。交与并通过容斥联系,不要把球冠公式直接当作并的体积。
程序:S6至S9

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题依赖缺失对应行号,不把其“一次”的说法当作循环执行次数规则。

变式

  1. 全为负数 [-5,-2,-9]:非空子段答案 -2;若允许空段,答案0。初始化不能混用。
  2. 求最大连续子段积,能否只维护最大积?不能。负数会把最小积变最大,需同时维护以当前位置结尾的最大积和最小积。例 [-2,3,-4] 答案24。
程序:S6至S9

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==MaTWE/TWE=ManTWFu

SGVsbG93b3JsZA== 解出 Helloworld,w 小写。CSP2021csp 的源代码实际编码为 Q1NQMjAyMWNzcA,长度14;标准填充才是 Q1NQMjAyMWNzcA==,长度16。贴文选项与代码不一致。第32题缺输入与完整题干,不能确认答案;string::npos 是无符号类型的最大值,也不能笼统说“cout npos 输出 -1”。

变式

  1. 输入7字节,标准格式输出多少字符、几个等号?12字符、2个等号;无填充格式10字符。
  2. 若字符表仍64个符号,但把每组取成7位会怎样?下标可能达127,数组越界;Base64 的64来自 2^6,掩码63即二进制六个1。
程序:S6至S9

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。

变式

  1. 如果问“运算次数最少”而不是“4的个数最少”,如何改?对于只用二元运算的完整表达式,叶子数K对应运算K-1,因此答案减1;但若增加一元运算或允许免费常数,此等价关系不一定保留。
  2. 允许向零截断的整数除法,2最少用多少4?仍是3,但转移不必要求 %==0,只须结果>0;其他目标的最优值可能改变。题目写“整除”时必须看清是“仅整除才合法”还是语言中的整数除法。
程序:S6至S9

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:

  1. p->son[0]=stack[top--]:弹出的旧树放左侧。
  2. stack[top]->son[1]=p:p 是剩余右链的新右孩子。
  3. x->dep<y->dep:Euler RMQ 比较深度,取最小。
  4. A[i*b+j]->dep<A[i*b+j-1]->dep:下降时置位。
  5. v+=(S>>(i-1)&1)?-1:1:按差分恢复相对深度。
  6. (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) 混用了变量,预处理应依赖数组长度而非询问次数。

易错与变式

  1. Euler 深度 [0,1,2,1,0],以“降1置1”编码差分,低位对应最早差分,状态是多少?差分 +1,+1,-1,-1,位从低到高0、0、1、1,所以状态12。查询位置1..3(0基)深度1、2、1,最小深度1。
  2. 将 RMQ 从最大改成最小,建树弹栈条件如何改?由 <>,建最小堆笛卡尔树;LCA 的 Euler 查询仍取最小深度,这部分不能跟着改成最大深度。
程序:S6至S9

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,均按非负且不溢出的通常题意理解。

变式

  1. [6,6,6] 输出多少?-1;如果改条件为 i!=j,允许不同位置相等值配对,则输出6。
  2. 求最大按位与,能否用“两个最大数”直接回答?不能。例 [8,7,6],8&7=0,但7&6=6。位运算大小与普通数值大小不能简单互换。
程序:S6至S9

O12|随机快速选择:只递归答案所在的一边【P0】

来源:S9 阅读程序(2)。选择一个随机基准 p,划分比 p 小和不小于 p 的元素,根据 p 的名次只递归一侧。

变量含义与原程序边界缺陷

基准暂放 d[L];a 从左向右找不小于基准的数,b 从右向左找小于基准的数,然后交换。循环结束后 a 调整到“右组第一个位置”,比基准小的数位于 [L+1,a-1],共 a-L-1 个,故基准的相对名次为 a-L

**原材料缺失 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 的执行次数不等于整个程序运行时间;即使交换很少,指针扫描仍可能线性。

  1. 所有 n 个数相等,要快速找到任意第 k 小,如何避免二次退化?使用三路划分为 <p、=p、>p;k 落在等于组就直接返回,本例一趟 O(n)。
  2. 快排为何通常 O(n log n),快速选择却期望 O(n)?快排要递归左右两侧,选择只递归一侧。若题目要前 k 小有序排列,单次选择后还须排序该部分,总期望 O(n+k log k)。
程序:S6至S9

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。所以 abcddcba 不可达,输出-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 的阻碍证据;若有重复字符,交换相同字符看不出来,不能直接把字符状态硬判为唯一奇偶性。

变式

  1. abcd→bcda,m=0需要几步?1。改m=3需要几步?3。这也是“逆操作存在,但步数不相等”的最小直观例子。
  2. n=5,m=2,起点 abcde,终点 bacde 能否到达?不能。两个操作都是3循环,为偶排列;目标只交换a、b,是奇排列。
程序:S6至S9

O14|分数背包:单位价值排序与精确分数【P0】

来源:S9 完善程序(1)。物品可任意切分,取走体积占比 α 就获得同等占比价值,所以按价值密度 w/v 从高到低装。

贪心证明与填空推导

如果方案中取了低密度物品的一小部分,同时还有高密度物品可取,把前者的等体积替换为后者,不增加体积而增加或保持价值。不断交换后,最优方案可表现为若干个高密度完整物品,加至多一个部分物品。

原填空依次 D、B、D、D、B:

  1. w[j]*v[j+1]<w[j+1]*v[j]:当前密度更低就交换。分母为正,交叉相乘保持不等号;整数 w/v 会截断,不能用于比较。
  2. v[1]<=B:体积能否装下,不是价值能否装下。
  3. curV=v[1];curW=w[1];:第一件完整装入,后续从第二件开始。
  4. curW*v[i]+(B-curV)*w[i],v[i]curW+(剩余容量/v[i])*w[i] 通分后的分子、分母。
  5. 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。

变式

  1. 不允许切割,仍按密度贪心可以吗?不可以。容量50,物品(价值,体积)=(60,10),(100,20),(120,30);密度法选前两件得160,而后两件得220。这是0/1背包,通常用DP。
  2. 两件密度相同,改变先后会影响分数背包最优值吗?不会。在总取入体积相同时,价值都等于密度乘体积;但具体取哪些部分可以不同。
程序:S6至S9

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:

填空与逐个排除

预期 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. [1,2,3] 的最优值?w(1^2)+w(2^3)=w(3)+w(1)=5+2=7;只取1和3则w(2)=3,明显更小。
  2. 如果恰好选两个元素,完整序列的结论还能用吗?不能。例 [0,1,0],原题最大4;恰好选两个时最大2。若要求恰好选K个,应给DP增加选择数量这一维,且按阶段维护状态,不能在同一表里混用不同长度。
程序:S6至S9

本部分的考前自测清单

看到陌生程序,按下面顺序读,比从第一行逐字猜快:

  1. 先找输入范围、下标起点和输出变量;数组长度n能否容纳下标n?返回下标还是排名?
  2. 识别状态:i是“当前位置”、k是“剩余排名”、F是“代价”还是“个数”?至少用一句中文写出定义。
  3. 每个循环/递归写一个不变量。只会说“这里用了DP/二分”不足以填空。
  4. 检查四种边界:最小输入、全相等、全负数或0、刚好用完一边。
  5. 数复杂度时分别算状态数、每状态工作、递归树层数。一个函数叫Map不代表 O(log n),一段有二分不代表整个算法 O(log n)。
  6. 填空先看“后面怎样用它”:数组下标必须合法,数量必须与下标差1,路径恢复条件必须与DP转移同向。
  7. 带“必然、任意、总是”的判断,优先找最小反例;遇到损坏题干,明确不确定处,不背错结论。
24题自测

闭卷自测:24题检查能否迁移

建议明晚开始前限时20分钟完成前16题,后天早上完成17—24题。先做再看答案。题目是根据材料知识点重新编写,不是所谓押题;做错回到相应讲义。

24题自测

题目

  1. 5 & 35 | 35 ^ 3 分别是多少?
  2. 在题设16位无符号模型中,x=1,先执行 x ^= x<<6,再执行 x ^= x>>8,最终是多少?
  3. int a=100000,b=100000; long long c=a*b; 为什么不可靠?改一个表达式使乘法安全。
  4. 当k=7、n=-10时,C++中 n%k(n%k+k)%k 分别是多少?
  5. for(i=1;i<=n;i*=2) for(j=0;j<n;++j) 的复杂度?默认i不会因越界溢出。
  6. 有序数组两指针各只右移、不回退,虽然内外两层循环,累计复杂度为什么可为O(n)?
  7. 有4个相同红球、3个相同蓝球,任意蓝球不相邻,排列数是多少?
  8. 从10人中选3人组成不分工小组,与选金银铜各一人,分别多少种?
  9. 1—30中不能被2或3整除的整数有多少个?
  10. K6中有多少个无向简单四环?不同起点、反向走视为相同。
  11. 根为第1层,最多4层的二叉树最多多少个点?10个点的二叉树高度至少多少?
  12. 空小根堆插入8、3、7、1,删掉两次最小值,堆顶多少?
  13. 两个互不相连的三角形组成的图,每个顶点度为2,是否有一条走遍全部边的欧拉回路?
  14. 一条含5个顶点的有向链有几种拓扑序?5个点无边的DAG有几种?
  15. BFS可否直接解决边权分别为1、5、10的最短路径?Dijkstra能否一般性处理负边?
  16. 0/1背包一维dp更新中,容量从小到大遍历会有什么问题?
  17. 已排序[1,1,3,4],差不超过2的无序下标对有几个?第3小的差是多少?
  18. 求最小可行整数,区间[lo,hi]且hi已知可行,while(lo<hi),check(mid)为真时应该更新哪边?
  19. DAG边0→1、0→2、1→2,计入单点路径,字典序第3条路径是什么?从0出发多少条?
  20. [2,2]所有非空子数组最大值之和多少?贡献法为什么不能左右都找严格更大?
  21. 0和负数能否直接套“非负整数LSD基数排序”的原代码?分别解释。
  22. 两个瓶子容量3与5,从(0,0)到某一瓶恰有4,写出一条6步操作序列。
  23. 两点间只能使用一次“边免费”优惠,为什么 dist[u] 一维状态不够?
  24. 为n个对象编不同二进制码,每个码最多k个1。w次测试最多能区分多少对象?n=5、k=1时最少w是多少?
24题自测

答案与回查

答案与关键理由 回查知识
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错得多,补状态建模。分数只用于定位弱点,不代表真实考试成绩。

24题自测

三个可反复填写的复习模板

读一段代码: 输入范围______;输出意义______;数组/变量含义______;循环不变量______;一个最小样例______;时间/空间______;危险边界______。

分析一道填空: 空位要的类型______;前后语句的联系______;排除选项的理由______;回代后的边界例______。

记录一道错题: 来源题号______;当时想法______;具体错因______;正确规则及适用条件______;自己改一个数据后的答案______。下一次先遮住答案复述,再看能否说明为什么。

90道选择题索引

附录: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
90道选择题索引

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;最早截止优先不普遍最小化加权超时惩罚。
90道选择题索引

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组删边集合,但缺少完整边集,不能据此证明最小性、完备性。
90道选择题索引

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)快速幂。
90道选择题索引

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。
90道选择题索引

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。
90道选择题索引

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(香农) 相符。
90道选择题索引

本批选择题最应防止背错的结论

  1. 欧拉回路不能只看度全偶;还要检查有边部分的连通性。
  2. Dijkstra需要非负边权;MST不要求边权非负,二者也不是同一问题。
  3. 最早截止优先不能保证一般加权超时惩罚最小;S1调度题是通过构造0惩罚证明。
  4. 名为quick_power不保证O(log n);相同递归调用写两次仍要算两次。
  5. 线段树“完整覆盖块”“访问结点”“函数调用”不是同一计数。
  6. S6-7的“完全三叉树”有术语歧义;用97时必须知道是在满层解释下。
  7. S2-15、S5-13、S8-15缺图,现阶段不能把资料答案当成独立验证结果。

独立计算核验记录。 对S1背包枚举32个子集;对S5 LCS做二维DP;对S8等腰三角形枚举729个数位三元组;对S5数根按模15计数;对S8逆序对直接数15对位置;对S6三叉树用不同结点数的完全三叉树反查前序父号。这些核验用于审校资料,不要求考场写程序完成选择题。

纠错:S1至S4

程序审校与答案索引: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;由于程序缺失和骨架错误,不认证为可执行程序的正确填空

验证记录

纠错:S6至S9

程序审校:S6—S9

纠错:S6至S9

编译及运行验证