ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

从积木题看算法思维:游程编码与连续段统计在信奥竞赛中的应用

从积木题看算法思维:游程编码与连续段统计在信奥竞赛中的应用 1. 项目概述从一道赛题看算法思维的构建最近在带学生备赛翻看往年真题时常州市赛2023年的这道B4222积木题让我眼前一亮。它不像一些复杂的图论或动态规划题那样一上来就让人发怵而是用一个非常生活化的“搭积木”场景包裹了一个经典的算法问题。题目大意是给定一组不同高度的积木要求计算在只能看到“轮廓”的前提下最少需要多少块积木才能拼出这个轮廓。这听起来是不是有点像我们小时候玩的游戏但恰恰是这种从具象到抽象的转化最能考察一个选手的基本功对数据的理解、对问题本质的挖掘以及将现实模型转化为可计算步骤的能力。很多刚接触信奥信息学奥林匹克的同学一看到“市赛”、“算法”这些词可能就觉得高深莫测下意识地去想有没有什么“高级”的模板可以套用。其实不然。这道题的核心本质上是对一个整数序列进行一种特定的“压缩”或“简化”操作。它适合所有已经掌握C基础语法数组、循环、条件判断、正开始接触基础算法的同学来练习。通过解决它你不仅能巩固编程基础更能学习到一种至关重要的思维方法如何忽略无关细节抓住影响结果的关键变量。接下来我就结合这道B4222详细拆解一下从读题到ACAccepted通过的完整思考与实现过程其中会穿插很多我在教学和解题中总结的“避坑”经验。2. 核心需求解析与问题抽象2.1 题目场景还原与关键信息提取我们先抛开代码像侦探一样仔细审视题目描述这里我根据常见赛题风格进行还原和阐述。假设我们面前有一排立在地上的积木柱子每根柱子由若干块单位高度的积木方块叠成因此柱子的高度就是一个正整数。现在我们从侧面远远望去只能看到它们的“天际线”或者说“轮廓”。如果几根相邻的柱子高度相同那么在轮廓上它们就会连成一条水平的线段我们无法分辨中间到底有多少根柱子。题目要我们求的是能形成这个轮廓的、最少需要的积木柱子数量。举个例子假设原始高度序列是[2, 3, 3, 2]。它的轮廓是先是一个2然后升到3并保持水平一段再降回2。为了用最少的柱子实现这个轮廓我们只需要三根柱子高度分别为2 3 2。因为中间两个高度为3的柱子在轮廓上合并了。所以答案就是3。关键信息点提炼输入一个整数序列代表一排积木柱的初始高度。操作寻找轮廓。轮廓变化的点只发生在高度发生变化的位置。输出构成这个轮廓所需的最少积木柱数量。核心逻辑相邻且高度相同的柱子在轮廓视角下可以“合并”视为一根。注意这里最容易产生的误解是去考虑“积木方块”的总数。题目问的是“柱子”的数量而不是“方块”的数量。这是一个典型的计数对象转换务必在审题时圈出来。2.2 问题抽象与算法思路确立将上述自然语言描述抽象成算法问题是解题的关键一步。抽象过程 我们有一个数组heights[]。我们需要遍历这个数组但目的不是处理每一个元素而是找出所有“连续相同高度子序列”的个数。每一个这样的子序列在最终的最少柱子表示中都只贡献一根柱子。思路确立 因此算法变得非常直接如果数组为空显然不需要柱子答案为0。初始化计数器count 1。为什么是1因为至少第一根柱子总是需要的它开启了一个新的“连续段”。从第二根柱子开始依次和它前面的那根柱子比较高度。如果当前高度heights[i]和前一高度heights[i-1]不同说明轮廓在这里发生了变化我们需要一根新的柱子来体现这个变化所以count。如果相同说明当前柱子还在上一个“连续段”中轮廓没有变化我们不需要为它新增柱子计数。遍历结束后count的值就是所需的最少柱子数。这个思路的时间复杂度是 O(N)只需要一次线性扫描空间复杂度是 O(1)仅需几个变量对于信奥竞赛的限制条件来说绰绰有余。为什么这样想—— 思维层面的剖析这实际上运用了“游程编码”Run-Length Encoding, RLE的思想。我们不是存储每一个具体值而是存储“值的变化点”。在本题中“值”就是高度“变化点”就是高度发生改变的位置。计数器的每一次增加都对应轮廓上的一个转折点包括起点。这种“关注变化忽略重复”的思维在数据处理、信号处理乃至游戏开发中都非常常见。3. 代码实现与逐行精讲有了清晰的思路我们用C来实现。这里我会提供两个版本的代码一个是最直观的版本另一个是稍作简化的版本并解释其中的每一个细节。3.1 版本一清晰直观版#include iostream #include vector using namespace std; int main() { int n; // 积木柱的数量 cin n; // 边界情况处理如果没有积木柱直接输出0并结束 if (n 0) { cout 0 endl; return 0; } vectorint heights(n); // 使用动态数组vector存储高度避免固定数组可能的内存浪费 for (int i 0; i n; i) { cin heights[i]; // 读入所有高度 } int min_blocks 1; // 最少柱子数初始化为1因为第一根柱子总是独立的 // 从第二根柱子开始遍历下标1 for (int i 1; i n; i) { // 核心判断当前柱子高度是否与前一根不同 if (heights[i] ! heights[i - 1]) { min_blocks; // 如果不同则需要一根新柱子来体现轮廓变化 } // 如果相同则什么都不做继续循环 } cout min_blocks endl; // 输出结果 return 0; }逐行精讲与避坑指南#include vector虽然题目可能给出了n的范围使用普通数组int heights[100005]也可以但我更推荐使用vector。它是一种动态数组更现代、更安全避免栈溢出也更能体现C标准库的运用。这是从“C风格”向“C风格”过渡的好习惯。边界处理if (n 0)这是一个非常重要的编程习惯。即使题目可能保证n1主动处理边界情况能使你的程序更健壮逻辑更完整。在竞赛中这能避免因未预料到的边界数据导致的运行时错误RE。min_blocks初始化为1这是思路的直接体现。只要有一根柱子它就算一个“连续段”。很多同学在这里会初始化为0然后在循环里从第一根开始判断这样容易在空数组或单元素数组时出错。初始化为1并让循环从i1开始逻辑更清晰。循环条件for (int i 1; i n; i)注意是i n确保不会访问heights[n]越界。使用前置自增i是个人习惯对于内置类型它与i效率无异但养成好习惯有益无害。核心判断if (heights[i] ! heights[i - 1])这是算法的灵魂。比较的是当前元素和它的前一个元素。一定要想清楚下标新手常犯的错误是比较heights[i]和heights[i1]这会导致最后一次循环越界。输出与返回记得输出换行endl这符合评测机的通常预期。return 0表示程序正常结束。3.2 版本二紧凑输入版这个版本在输入的同时进行判断节省了一个存储所有高度的数组空间。当n很大时这能节省可观的内存但逻辑上稍微绕一点。#include iostream using namespace std; int main() { int n; cin n; if (n 0) { cout 0 endl; return 0; } int prev_height, current_height; cin prev_height; // 先读入第一根柱子的高度 int min_blocks 1; for (int i 1; i n; i) { cin current_height; // 读入当前柱子的高度 if (current_height ! prev_height) { min_blocks; } prev_height current_height; // 关键将“当前”变为下一轮的“之前” } cout min_blocks endl; return 0; }这个版本的技巧与易错点双变量滚动我们只维护两个变量prev_height和current_height分别代表“前一个高度”和“当前高度”。状态更新在每次判断后必须执行prev_height current_height;。这是整个逻辑正确运行的关键它保证了在下一轮循环中prev_height始终是current_height的前一个值。忘记这一步是常见错误会导致比较对象错乱。适用场景当问题只需要顺序处理数据且不需要回头访问之前的数据时这种“流式处理”方法非常高效。它不仅省空间有时还能省时间减少内存访问。这道题完美符合条件。实操心得对于初学者我建议先从版本一开始写逻辑清晰不易错。当你对问题理解非常透彻后可以尝试版本二这是一种重要的空间优化技巧。在竞赛中根据数据范围本题n一般不会太大选择即可。版本一更具普适性和可读性。4. 测试用例设计与调试思维写完代码不等于完事设计测试用例验证程序的正确性是必备技能。下面我设计几组有针对性的测试数据并说明它们覆盖了哪些边界和特殊情况。测试用例输入 (n 和 heights)预期输出测试目的与说明0(仅一个0)0空数组边界。测试程序是否能处理n0的情况。151单元素数组。只有一个柱子答案必然是1。51 1 1 1 11全部相同。所有柱子高度一样轮廓是一条水平线只需一根柱子。51 2 3 4 55严格递增。每个高度都不同每根柱子都无法合并答案等于n。55 4 3 2 15严格递减。同上测试递减序列。62 3 3 3 2 14一般情况。序列为2, (3,3,3), 2, 1。连续3个3合并所以是4根柱子。71 2 2 1 1 3 35有多个连续段。轮廓变化点为1, 2, 1, 3。注意最后两个3合并。如何测试本地测试你可以将上面的输入数据保存为in.txt在命令行运行你的程序并将输出重定向到文件再与预期输出对比。# 假设编译后的程序叫blocks.exe blocks.exe in.txt out.txt然后查看out.txt的内容。在线评测机OJ思维在OJ上提交后如果出错它会返回一个错误类型如WA, RE。这时你需要根据错误类型和上述测试用例来“脑补”可能出错的数据。WA (Wrong Answer)结果不对。重点检查算法逻辑尤其是循环的起始、结束条件和比较语句。用“全部相同”和“单元素”用例最容易发现初始值错误。RE (Runtime Error)运行时错误。最常见的是数组越界。检查你的数组大小是否足够如果用静态数组或者vector访问下标是否在[0, n-1]范围内。n0时的处理不当也可能导致RE。TLE (Time Limit Exceeded)超时。本题算法是O(N)几乎不可能超时。如果发生检查是否有死循环比如循环变量更新错误。调试技巧在代码关键位置如循环开始、判断分支插入临时输出语句打印变量值是理解程序运行过程、定位bug的最朴实有效的方法。5. 算法扩展与思维提升这道题虽然简单但它背后的思想可以延伸到更复杂的问题。理解这一点你的算法思维就能再上一个台阶。5.1 问题变体思考如果题目不是求最少柱子数而是求轮廓线本身的长度假设柱子宽度为1呢分析轮廓线长度由水平部分和垂直部分构成。水平部分就是每个“连续段”的长度柱子数垂直部分就是高度差。但仔细想想最少柱子数其实等于轮廓线中“转折点”的数量。而求轮廓线总长度则需要计算所有相邻不同高度之间的“台阶”高度差之和再加上起始和结束的宽度不这需要更严谨的定义。实际上一个更经典的“天际线”问题是给定一系列矩形积木柱求它们轮廓的顶点序列。这就引出了著名的“天际线问题”Skyline Problem通常需要用扫描线算法和优先队列来解决复杂度也更高。通过对比你能深刻体会到本题的简化之美——它只关心计数不关心具体几何形状。5.2 同类问题举一反三掌握“统计连续段”或“检测变化点”这个模式你可以解决很多类似问题字符串压缩例如将“aaabbcccc”压缩成“a3b2c4”。这同样是游程编码遍历字符串计数连续相同字符。计算分组数有一组学生按身高排序后需要分成若干组要求同组内身高差不超过某个值。这需要在遍历时不仅比较相邻项是否“相等”还要判断是否“满足某个条件”从而决定是否开启新的一组。信号平滑与边缘检测在数据处理中连续相同的值可以视为平稳信号而值的变化点可能就是需要关注的“边缘”或“事件”。思维模式总结遇到需要对序列进行“分段”或“合并相邻同类项”的问题时第一时间想到这种单次扫描、比较相邻元素的模板。它的核心框架是int count 1; // 或0取决于第一段如何定义 for (int i 1; i n; i) { if (data[i] ! data[i-1]) { // 这里的“!”可以替换成任何分段条件 count; // 可选在这里记录分段点信息 } }6. 竞赛实战中的注意事项与效率提升在信奥竞赛的实战环境中除了写出正确的算法还有一些细节能决定成败。6.1 输入输出效率对于本题数据量不会太大使用标准的cin/cout完全足够。但如果遇到输入数据量极大如n 10^5的题目就需要考虑输入输出效率。关闭流同步在main函数开头加入以下两行可以大幅提升cin/cout的速度使其接近scanf/printf。ios::sync_with_stdio(false); cin.tie(nullptr);注意一旦使用了这两行就不能再混用cin/cout和scanf/printf否则可能导致输入输出顺序错乱。使用scanf/printfC风格的输入输出在默认情况下比未优化的cin/cout快。对于纯整数或浮点数读取scanf也很方便。使用快读函数对于极端情况n 10^6可以手写一个读取整数的函数通过逐字符读取来获得最高速度。这是竞赛中的高级技巧。对于本题的建议直接使用cin/cout保持代码简洁清晰。在时间限制宽松的赛题中可读性比那一点点微乎其微的效率提升更重要。6.2 代码风格与可读性清晰的代码风格能让你在调试时更快地找到问题也方便他人或未来的你阅读。变量命名使用有意义的名称如min_blocks,heights而不是a,b,ans。适当注释在关键逻辑处如算法核心、边界处理写上简短注释。合理缩进与空格让代码结构一目了然。大多数现代编辑器如VSCode都有自动格式化功能ShiftAltF。6.3 心理与时间策略先保证正确再追求优化像本题先写出O(N)时间、O(N)空间版本一的正确代码。提交通过后如果时间充裕再考虑能否优化成O(1)空间版本二。切忌一开始就追求“完美”而写出复杂易错的代码。善用样例题目给出的样例输入输出是第一个测试工具。确保你的程序能通过样例。自己构造临界数据思考n0,n1,n最大值以及所有元素相同、全部递增、全部递减等情况。这能帮你发现大部分边界错误。这道B4222积木题就像一块很好的“基础积木”。它本身不复杂但搭建起了从问题理解、抽象建模、算法设计、代码实现到测试调试的完整流程。解决它的价值不在于算法本身有多高深而在于完整地实践了一次解题的标准化思考过程。在信奥学习的道路上把每一道这样的基础题吃透积累起来的思维模式和代码经验才是应对未来更复杂挑战的坚实基石。下次当你遇到一个看似新颖的问题时不妨先问问自己它是不是某个基本模式的“换装”能不能像处理这些积木一样找到那个决定性的“变化点”
返回列表