ICPC区域赛题解精析:贪心、图论与状态压缩DP实战

发布时间:2026/8/3 1:34:37

ICPC区域赛题解精析:贪心、图论与状态压缩DP实战 1. 项目概述从一场区域赛的题解说起最近在整理过去的训练笔记翻到了2019-2020年ICPC西北俄罗斯区域赛的几道题目。这场比赛的题目质量相当不错既有考验思维深度的构造题也有对经典算法进行巧妙变形的题目非常适合用来进行专题训练和查漏补缺。我挑选了其中几道我个人觉得很有代表性或者当时在赛场上卡了我们很久的题目准备在这里做一个详细的复盘和解析。写题解的目的一方面是梳理自己的思路把当时那些“灵光一现”或者“百思不得其解”的瞬间固化下来另一方面也是希望能给正在备赛ICPC、Codeforces的同学们提供一个不同的解题视角。毕竟看官方题解或者顶尖队伍的代码是一回事理解一个普通参赛者在有限时间内如何一步步拆解问题、尝试思路、最终或未能找到正解的过程或许是另一种宝贵的学习经验。接下来的内容我会假设你具备基础的算法知识如动态规划、图论、数据结构但我会尽量把思考的“脚手架”搭出来而不仅仅是呈现一个完美的最终答案。2. 核心解题思路与策略选择2.1 区域赛题目的典型特征与应对策略西北俄罗斯赛区的题目向来以“思维难度”和“实现精度”著称。它不像一些赛区那样热衷于出“模板题”或者“大力出奇迹”的数据结构题而是更喜欢在问题模型上做一些精巧的变换让你觉得“似曾相识”却又“无从下手”。面对这类题目死记硬背模板是行不通的关键在于快速识别问题本质并将其归约到已知的算法模型上。我的通用解题策略通常分为四步问题抽象 - 模型识别 - 算法选择 - 边界处理。首先彻底理解题意用数学语言或自己熟悉的术语重新描述问题过滤掉无关的故事背景。其次寻找这个抽象后的问题与哪些经典模型如网络流、匹配、最短路、DP状态机等有相似之处。然后根据数据范围这是最重要的提示选择或设计算法一个1e5的数据范围和一个1e3的数据范围导向的算法复杂度天差地别。最后也是很多新手容易翻车的地方就是仔细考虑各种边界情况比如空集、极值、整数溢出等。2.2 本场赛事题目选析为何是这几道我选择了三道题目进行重点讲解它们分别代表了三种不同的挑战类型思维构造型题目可能看起来规则很简单但需要你发现其内在的数学规律或构造出特定的解。这类题往往代码很短但思维链很长。算法变形型核心是某个经典算法如DP、贪心但题目的约束条件做了改动需要你对算法的理解足够深入才能进行正确的适配。实现细节型思路可能直接明了但对数据结构的运用和代码实现的效率、准确性要求极高稍有不慎就会超时或出错。在比赛环境中时间分配至关重要。通常我们会让队伍里思维最敏捷的队员主攻第一类题对经典算法掌握最扎实的队员主攻第二类题而代码能力最强的队员则负责第三类题和复杂的模拟。当然这种分工是动态的随时根据解题进度调整。3. 题目A详解基于贪心的区间调度问题变体3.1 问题重述与初步分析我们来看第一道题假设其为A题。题目大意是有n个任务每个任务有一个开始时间si和结束时间ei以及一个价值vi。你有一台机器同一时间只能执行一个任务。但特殊规则是如果你选择执行某个任务那么在时间[si, ei]内你不能执行任何其他任务即使其他任务只占用了这个区间的一部分。你的目标是选择一组任务使得总价值最大。这立刻让我们联想到经典的“区间调度问题”和“区间带权调度问题”。经典的无权版本是贪心地选择结束时间最早的任务。带权版本则通常使用动态规划按结束时间排序后dp[i]表示考虑前i个区间且必选第i个区间时的最大价值转移时需要二分查找最后一个结束时间小于si的区间j然后dp[i] max(dp[i-1], dp[j] vi)。然而本题的特殊规则“选择一个任务就独占整个区间”改变了游戏规则。在经典模型中如果两个区间是[1,3]和[2,4]它们只是不能同时被选中但你可以二选一。在本规则下如果你选了[1,3]那么[2,4]因为其时间范围[2,4]与[1,3]有交集所以根本不能被考虑即使它只重叠了一部分。这意味着冲突的定义从“时间点重叠”变成了“区间存在交集”。3.2 关键转化从区间冲突到区间包含这个改变引导我们进行一个关键的转化思考。我们把所有区间画在数轴上。假设我们选择了一个区间I。那么任何与I有交集的区间都不能被选。这意味着所有能被选的区间必须完全位于I的左侧或者完全位于I的右侧且不能紧挨着因为紧挨着也算相交如[1,2]和[2,3]在本题规则下是相交的因为区间[2,2]是共有的。这听起来很复杂。但我们可以换个角度如果我们把所有区间按照左端点进行排序。当我们决定是否选择第i个区间时我们需要考虑所有左端点小于等于ei的区间因为它们可能与i相交但可以完全忽略左端点大于ei的区间它们一定在i的右边且不相交。然而这还不够。因为一个左端点小于si但右端点大于si的区间j也会与i冲突。所以冲突的条件是max(si, sj) min(ei, ej)即区间有交集。注意这里有一个常见的思维陷阱。有同学可能会想用“区间合并”的思路把有交集的区间合并成一个大区间然后问题转化为在若干不相交的大区间里选价值最大的一个。这是错误的因为我们的目标是最大化价值和而不是覆盖范围。合并区间会丢失单个区间的价值信息。3.3 动态规划状态设计与转移优化正确的思路是动态规划但状态定义需要精心设计。我们定义dp[r]为所有右端点小于等于r的区间中能获得的最大价值。我们对所有区间按右端点升序排序。考虑处理到区间i: (si, ei, vi)。如果我们不选它那么dp[ei]至少等于dp[ei-1]假设时间离散化后是整数。如果我们选它那么所有与它相交的区间都不能选。由于我们按右端点排序当我们选择i时我们必须确保之前选择的最后一个区间的右端点严格小于si。因为如果上一个区间的右端点r si那么区间(r, ?)和当前区间(si, ei)在si这个点就相交了因为r是闭区间端点。因此转移方程为dp[ei] max(dp[ei-1], dp[si - 1] vi)这里dp[si - 1]代表了所有右端点严格小于si的区间构成的最优解保证了与当前区间i绝对不相交。为什么按右端点排序因为这样当我们计算dp[ei]时所有右端点小于等于ei的区间都已经被考虑过了dp[si-1]是一个已经计算好的、稳定的最优子结构。如果按左端点排序转移时会非常麻烦因为可能涉及未来才考虑的区间。3.4 离散化与实现细节时间点范围可能很大需要离散化。离散化时我们不仅需要所有的si和ei为了计算si-1我们还需要将每个si-1也加入离散化数组如果si是离散化后的索引那么si-1就是前一个索引对应的值。或者更简单的方法离散化后我们不再用时间值作为dp数组的下标而是用离散化后的索引idx。那么dp[idx]表示处理到离散化点idx代表某个时间值val[idx]时的最大价值。转移时我们需要找到最后一个离散化点pos使得val[pos] si然后dp[ei_idx] max(dp[ei_idx - 1], dp[pos] vi)。寻找pos的过程可以用二分查找lower_bound快速完成。实操心得排序时如果右端点相同通常按左端点升序或任意顺序均可但有时为了处理某些边界按左端点降序可能更优避免同一右端点的区间相互干扰。在这题里按右端点升序、右端点相同时按左端点升序即可。二分查找si-1对应的离散化索引时要使用lower_bound寻找第一个大于等于si的位置然后将其减一就得到了最后一个小于si的位置。务必检查减一后索引是否有效0。dp数组可以只开一维在遍历区间时滚动更新。最终答案就是dp[最大索引值]。这道题的代码实现起来并不长但思维转折点在于理解“选择即独占”导致的严格不相交条件并将此条件转化为dp[si-1]这样一个简洁的转移。这是将题目约束成功编码到状态转移方程中的典型例子。4. 题目B详解图论中的奇偶性构造问题4.1 问题场景与模型建立第二道题B题是一个图论构造题。题目给了一个n个点m条边的无向图可能不连通。你需要给每条边定向使之成为一个有向图。目标是对于每一个节点v定义其差值diff(v) |outdeg(v) - indeg(v)|即出度与入度之差的绝对值。现在要求所有节点的差值之和尽可能小。初看之下这像是一个网络流或欧拉回路问题。因为在一个有向图中所有节点的出度之和等于入度之和。但这里我们追求的是每个节点自身度数的平衡而不是全局平衡。让我们重新表述问题我们有一堆边无向我们要决定每条边的方向。每决定一条边(u, v)的方向比如定为u-v那么u的出度加1v的入度加1。这相当于给u的“度数差”贡献了1出度增加给v的度数差贡献了-1入度增加相当于出度减入度的值减少了1。注意这里的“度数差”我们暂时定义为d(v) outdeg(v) - indeg(v)那么最终要求的diff(v) |d(v)|。所以给每条边定向就是给每个端点分配一个1和一个-1。我们的目标是让所有节点的|d(v)|之和最小。4.2 奇偶性分析与关键引理这是一个经典的“图定向以最小化度数差”问题。其核心在于每个节点的初始度数无向图中的度的奇偶性。考虑一个节点v它在无向图中的度数为deg(v)。当我们给所有与之相连的边定向后对于v来说每一条与之相连的边要么贡献1作为起点要么贡献-1作为终点。假设有x条边以v为起点那么以v为终点的边数就是deg(v) - x。那么v的度数差d(v) x - (deg(v) - x) 2x - deg(v)。观察这个公式d(v) 2x - deg(v)。因为2x是偶数所以d(v)的奇偶性完全由deg(v)的奇偶性决定deg(v)为奇数则d(v)必为奇数deg(v)为偶数则d(v)必为偶数。而我们要求最小化sum(|d(v)|)。|d(v)|的最小可能值是多少呢如果deg(v)是偶数那么d(v)也是偶数我们可以通过选择合适的x让d(v)0只需令x deg(v)/2即可。如果deg(v)是奇数那么d(v)是奇数其绝对值至少为1。我们能否达到这个下界呢即让所有偶度节点的d(v)0所有奇度节点的|d(v)|1。4.3 构造算法与可行性证明答案是肯定的并且存在一个优美且简单的构造方法。这个下界sum(|d(v)|) (奇度节点的数量)是可以达到的。算法步骤在原始无向图中找出所有度数为奇数的节点。我们知道在任意无向图中奇度节点的个数一定是偶数。将这些奇度节点两两配对在每对节点之间添加一条虚拟边。现在得到一个新图GG中所有节点的度数都变成了偶数因为奇度节点加了一条边变偶数偶度节点不变还是偶数。在G上寻找一条欧拉回路因为所有节点度数为偶如果图连通则存在欧拉回路。如果原图不连通则在每个连通分量上分别找欧拉回路。沿着欧拉回路走将回路上的每条边包括我们添加的虚拟边定向为前进方向。这样对于G中的每个节点进入它的边数等于离开它的边数即d(v) 0。现在移除我们添加的那些虚拟边。移除一条虚拟边(u,v)意味着什么这条边在欧拉回路中有一个方向比如u-v。移除它相当于在u的出度中减1在v的入度中减1。根据d(v)的定义(出度-入度)这会导致d(u)减少1d(v)增加1。由于在G中所有d(v)0移除虚拟边后对于一对配对的奇度节点u和v假设虚拟边方向是u-v那么移除后d(u) d(u) - 1 -1所以|d(u)| 1d(v) d(v) 1 1所以|d(v)| 1正好达到了下界对于原图中的偶度节点它们没有参与虚拟边配对因此移除虚拟边不影响它们d(v)保持为0。实现细节添加虚拟边只是为了证明存在性和引导构造。在实际代码中我们不需要显式地添加边再找欧拉回路。更实用的方法是对原图进行DFS或Hierholzer算法找欧拉通路。在遍历过程中当我们第一次离开一个节点时即回溯时才确定刚刚走过的边的方向。我们可以这样保证对于偶度节点进出平衡对于奇度节点我们将其设为DFS的起点或终点这样它就会恰好多一条出边或少一条入边使得|d(v)|1。一个简单的实现统计每个连通分量中奇度节点的数量。如果数量为0则该分量可以形成一个欧拉回路定向后所有点差值为0。如果数量为2则该分量可以形成一个欧拉通路从其中一个奇度节点开始到另一个结束定向后这两个奇度节点差值绝对值为1其余点为0。如果数量大于2实际上在无向图中只能是偶数则可以通过添加虚拟边在算法中体现为优先遍历策略将其分解为多个欧拉通路。注意事项这个构造算法证明了最小和就是奇度节点的数量。题目可能要求输出这个最小值或者输出一种具体的定向方案。如果是后者实现欧拉路/回路定向时需要仔细处理边的存储和标记避免重复访问。4.4 思维延伸与总结这道题的精妙之处在于它通过奇偶性分析将看似复杂的优化问题转化为了一个图论经典问题欧拉回路的存在性构造问题。它考察的是选手是否具备将“最优化目标”与“图的结构性质”联系起来的能力。关键的一步是发现d(v) 2x - deg(v)以及奇偶性引理这直接给出了问题的理论下界并指引了构造方向。在比赛中如果能快速洞察到这个奇偶性质就能节省大量盲目尝试的时间。这也提醒我们遇到图论中的度数问题多考虑奇偶性往往会有意想不到的收获。5. 题目C详解动态规划中的状态压缩与优化5.1 复杂约束下的DP状态定义第三道题C题是一个动态规划题目数据范围暗示我们需要状态压缩。题目描述大致是给定一个长度为n的序列an 20以及一个整数k。我们可以进行若干次操作每次操作选择序列中相邻的两个数将它们合并为它们的和得到一个新的序列。问最少经过多少次这样的操作可以使得序列中最多只有k种不同的数字k很小比如5。n20强烈提示状态压缩DP。我们需要用一个状态来表示当前序列的样子。但序列是动态变化的直接存储序列不现实。注意到操作是合并相邻项这非常类似于区间DP或石子合并问题。但目标不是最小化代价而是让数字种类不超过k。一个关键观察是合并操作不会改变序列的总和。设总和为S。那么最终序列一定是将原序列划分成若干个连续的段每个段被合并成了一个数这个数就是该段所有数字的和。我们的目标是选择一种划分方式使得合并操作数最少即段数最少不对合并次数 n - 最终段数。因为初始有n个数最终有m个数每次合并减少一个数所以需要n-m次操作并且最终这些段和即最终序列的数字的种类数不超过k。所以问题转化为将原序列划分成最少的连续段使得这些段和的种类数不超过k。我们希望段数m尽可能大因为操作数n-m越小越好但同时要满足种类数约束。5.2 状态设计与转移方程我们可以用DP来解决这个划分问题。设dp[i][mask]表示考虑前i个元素1-indexed当前已经形成的“段和集合”用位掩码mask表示的情况下最多的段数或等价地最少的操作次数。但“段和”可能有很多种我们无法直接将其放入mask。这里需要第二个观察我们只关心段和的种类而不关心具体的段和值是多少也不关心每种值出现了几次。而且由于最终种类数k很小5我们可以尝试枚举所有可能的“段和类型集合”。但段和的值可能很大怎么办我们换一种状态定义。设dp[i][c][mask]这似乎更复杂了。一个更聪明的做法是状态中不直接存储mask而是存储“已经产生了多少种不同的段和”以及“最后一段的和是多少”。定义dp[i][j][last_sum]last_sum的范围太大。我们需要再次利用数据范围n20和a[i]的大小假设a[i]也不大或者总和可控。实际上我们可以枚举所有可能的段和。因为n20不同的连续子段和最多有n*(n1)/2210个这个数量是可以接受的。所以我们可以预处理出所有可能出现的段和值去重后得到一个数组vals[]。设m vals[]的长度210。重新定义状态dp[i][j][mask]表示考虑前i个元素已经划分成了j段且使用的段和种类集合为maskmask是一个bitset或者因为种类数k5我们可以将vals映射到0~4的索引但mask需要能表示所有vals的出现情况这不行因为vals可能有上百种。看来mask的思路遇到瓶颈。我们需要压缩状态。既然最终只需要种类数k我们或许可以不必知道具体是哪些种类只需要知道种类数。但这样在状态转移时当我们新增一段我们需要知道这段的和是否已经在之前的种类中出现过这要求我们知道历史种类信息。5.3 巧妙的双维度DP与预处理一个经典的技巧是外层循环枚举最终允许的数字种类。即我们先假设我们知道最终允许哪几种数字段和然后检查是否能通过划分实现。但枚举所有可能的k种数字组合即使k5从最多210个候选值中选5个组合数太大。另一种思路是DP over subsets。定义dp[mask]为用mask表示的这些元素原序列下标能否被划分成若干段使得每段的和都在一个“合法的集合”S中。然后我们枚举这个合法集合S即最终允许的段和种类检查dp[full_mask]是否为真。我们想要找到最小的|S|即种类数使得存在这样的划分并且在此前提下划分的段数最多操作数最少。但这样复杂度是O(2^n * 2^c)c是候选段和数量不可行。我们需要更精妙的状态设计。让我们回到最初目标是n - 段数最小即段数最大。定义f[i]为考虑前i个元素在满足种类数约束下能划分出的最大段数。转移时f[i] max{f[j] 1}其中j i且区间(j1, i)的和sum(j1,i)是一个“合法”的数字并且新增这个数字后总的数字种类数没有超过k。为了记录种类数我们需要在状态中携带当前已经使用了哪些数字的信息。由于k5我们可以用一个mask来记录但mask不是对应vals的索引而是对应当前已使用的数字本身。但数字可能很多。怎么办注意在转移过程中当我们考虑以i结尾的最后一段时这段的和x sum(j1,i)是确定的。我们只需要知道在状态f[j]对应的历史划分中数字x是否已经出现过。如果出现过那么新增这一段不会增加种类数否则种类数加1。因此我们可以将状态定义为dp[i][mask]其中i表示前i个元素mask是一个长度为k的“数组”的压缩表示它记录了当前划分中已经使用的最多k个不同的段和值是什么。但这样mask会非常巨大。5.4 最终解法Meet-in-the-Middle 或 迭代加深搜索鉴于n20这其实是一个典型的折半搜索Meet-in-the-Middle可以解决的问题。我们可以枚举前一半序列的所有划分方案以及后一半序列的所有划分方案然后组合起来。具体地将序列分成左右两半各约10个元素。对于左半部分我们枚举所有可能的划分方式。对于每一种划分我们得到1) 划分的段数cntL2) 该划分产生的所有段和的集合setL一个无序集合。同样对于右半部分枚举所有划分得到cntR和setR。现在对于左半部分的一个结果(cntL, setL)和右半部分的一个结果(cntR, setR)它们能拼接成一个完整划分的条件是setL和setR的并集的元素个数不超过k。如果能拼接那么总段数就是cntL cntR。我们需要找到在满足种类数并集大小k的前提下最大的cntLcntR。如何高效枚举和匹配n10时划分方案数是贝尔数B(10) ≈ 115975对于每一半来说枚举是可行的。我们可以用位掩码表示划分一个长度为len的序列有len-1个间隙选择哪些间隙切开就决定了一种划分。枚举所有2^(len-1)种切法即可对于len10只有512种非常少。等等2^(9)512这比贝尔数小很多因为贝尔数考虑了不同的分组方式而这里“连续段”的划分唯一地由切割点决定。是的对于划分成连续段的问题确定哪些位置是“段尾”即可。所以枚举量是2^(len-1)完全可行。算法步骤预处理原序列前缀和pre[]方便计算任意区间和。将序列分成左右两半mid n/2。枚举左半部分对于从0到2^(mid-1)的每个掩码maskL这个掩码的二进制位表示1到mid-1这些位置是否是段尾。我们可以解析出所有的段遍历位置遇到段尾或末尾就计算一段的和。得到左半部分的段数cntL和段和集合setL用C的bitset或整数掩码表示不行值可能很大。我们需要存储(cntL, setL)。但setL如何存储用于快速匹配由于k5setL的大小最多为5。我们可以将setL中的数字排序后放入一个定长数组并用一个哈希值如将数字排序后转化为字符串再哈希或者直接用vectorint作为map的key来代表它。枚举右半部分类似地枚举右半部分从mid到n-1的所有划分maskR。注意右半部分的索引偏移。得到cntR和setR。组合匹配对于左半部分的每一个结果(cntL, setL)我们需要找到所有右半部分的结果(cntR, setR)使得setL和setR的并集大小s k。然后更新答案ans min(ans, n - (cntLcntR))因为操作数 n - 总段数。直接两两匹配是平方复杂度可能超时。我们可以进行优化对于每个左半部分的setL我们只关心能和它组合的右半部分结果。我们可以遍历右半部分的所有结果但这样还是O(左结果数 * 右结果数)最坏约(2^9)*(2^9)262144完全可以接受。匹配时需要计算两个集合的并集大小。可以将集合中的数字排序后归并或者放入unordered_set再计算。实现细节与优化枚举划分时可以通过mask快速计算段和记录当前段的起点遍历bit位遇到1则结算当前段。存储结果时可以使用mapvectorint, int其中key是排序后的段和集合vector value是在该集合下能达到的最大段数因为对于同一种集合我们只保留段数最大的那个这样组合时更优。注意左半部分和右半部分分别用两个这样的map。组合时遍历左map的每一个条目(setL, maxCntL)遍历右map的每一个条目(setR, maxCntR)计算并集大小。如果k则用maxCntLmaxCntR更新最大总段数。这道题将状态压缩、枚举、折半搜索和集合运算结合了起来。n20是一个强烈的提示指引我们向2^(n/2)的折半搜索思考。它要求选手不仅熟悉DP还要能根据数据范围灵活选择搜索策略并且能熟练处理集合类的状态和合并。6. 常见问题与调试技巧实录在解决这类竞赛题目时尤其是现场赛环境一些常见的陷阱和调试技巧能帮你节省大量时间。6.1 边界条件与初始化错误这是最常见的错误来源之一。数组下标是0-indexed还是1-indexed前缀和数组pre[i]通常表示前i个元素的和a[1]...a[i]那么区间[l, r]的和就是pre[r] - pre[l-1]。务必确保l-1不越界当l0时。我个人的习惯是统一使用0-indexedpre[i]表示a[0]到a[i-1]的和这样区间[l, r)的和是pre[r] - pre[l]思维负担更小。DP初始化dp[0]通常代表空集的状态需要根据题意仔细设置。例如在求最大值时通常初始化为-INF而dp[0]0。在计数类DP中dp[0]1。务必考虑清楚状态定义的起点。循环范围双层循环时内层循环的起始点是否依赖于外层更新顺序是否正确例如在背包问题中如果使用一维数组物品循环在外容量循环在内且逆序这是必须牢记的。排查技巧编写代码后先用小数据n1,2,3和极端数据全0全1最大值最小值测试。自己手动模拟DP表格看与程序输出是否一致。6.2 整数溢出与精度问题整数溢出这是C选手的噩梦。即使题目保证结果在int范围内中间计算过程也可能溢出。例如两个1e9的数相加可能还在int范围内2e9但相乘就溢出了。long long是你的好朋友。在不确定时对中间变量使用long long。特别是涉及前缀和、累加、乘积、组合数计算时。浮点数精度尽量避免使用浮点数比较。如果必须使用使用eps如1e-9进行容错比较。不要直接用。对于涉及除法的题目考虑能否转化为整数运算如比较a/b和c/d可以转化为比较a*d和b*c注意符号。实操心得在代码开头养成习惯typedef long long ll;。对于涉及大量累加的场景即使单个数字很小也使用long long。在乘法前可以加上判断if (a LLONG_MAX / b) { // 溢出处理 }。6.3 算法选择与复杂度误判错误估计复杂度这是导致TLE超时的主要原因。例如n1000时O(n^3)的算法1e9运算在2秒时限内通常很危险。n1e5时O(n^2)绝对不行。务必根据数据范围选择算法。一个经验法则现代CPU在竞赛环境中1秒大约能完成3e8到5e8次简单运算如整数加减、比较。将你的算法运算量与此对比。隐藏的复杂度例如在循环内部调用std::lower_bound是O(log n)整体是O(n log n)可以接受。但在循环内部调用std::vector::erase是O(n)的如果外层也是O(n)整体就变成O(n^2)了。要清楚所用STL操作的时间复杂度。排查技巧在提交前心里默算一遍最坏情况下的操作次数。如果使用map或set记住其操作是O(log n)的。如果使用unordered_map平均是O(1)但最坏情况是O(n)。在时间卡得很紧时考虑用数组和排序代替map。6.4 多组数据输入与初始化很多竞赛题目包含多组测试数据。常见的错误是忘记在读入每组数据前清空全局的vector、map、set或数组。对于静态数组如果只用到前n个位置但下一组数据n变小了可能残留上一组数据后面的值造成错误。安全的做法是每次用memset或循环清空所需范围或者直接在读取n后使用vectorint a(n)。标准模板int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin T; while (T--) { // 在这里声明变量或清空全局容器 int n; cin n; vectorint a(n); // ... 解决单组数据 } return 0; }6.5 调试输出与对拍当程序结果错误但又找不到原因时小数据调试构造小的随机数据用你的程序和另一个暴力但正确的程序通常用于小数据同时运行比较结果。这个过程称为“对拍”。这是找出逻辑错误最有效的方法之一。输出中间变量在怀疑的代码段输出关键变量的值观察其变化是否符合预期。尤其是在DP、递归、循环中。使用断言在代码中插入assert语句检查你认为不变的条件是否被违反。例如在二分查找中assert(l r)在数组访问前assert(idx 0 idx n)。个人习惯我会写一个简单的Python脚本随机生成小数据分别用我的C程序和一个纯暴力的Python程序运行并自动比较输出。一旦发现不一致就保存这组数据然后用调试器或输出日志来定位问题。花半小时写一个对拍脚本可能在接下来的比赛中为你节省数小时的调试时间。

相关新闻