多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

C语言数列分段题详解:相邻比较与count初始化的坑

C语言数列分段题详解:相邻比较与count初始化的坑 数列分段这道题是CCF CSP认证题库里非常经典的签到题也是很多学C语言的人第一次接触“连续相等分段计数”这个题型的起点。我第一次在评测系统里提交这道题时觉得逻辑并不难真正让我意外的是——就这么一个简单题目居然也能栽在count初始化和数组边界上。它最适合三类人刚学完数组的C语言初学者、准备CCF CSP认证的学生、以及想快速找回刷题手感的老手。看懂这道题你收获的不只是一个答案而是一种在刷题里特别值钱的思路如何把“分段”这个直观概念压缩成一行相邻比较的判断。1. 题目理解与核心思路拆解1.1 原题到底在问什么——题意、输入输出与样例这道题来自CCF CSP认证的历次真题题目编号是201509-1名称就叫“数列分段”。原题描述非常短给定一个整数数列数列中连续相等的最长整数序列算作一段问这个整数数列共有多少段。输入格式很固定第一行是一个正整数n表示数列中整数的个数第二行是n个整数表示待分段的数列。输出只有一个整数就是段数。题目给了标准样例当n9数列是“8 8 8 0 12 12 8 0”时正确答案是5。这个样例其实已经把“分段”的规则讲透了开头的三个8连在一起它们完全相等所以第一段是8 8 8接下来是一个单独的0和第二段前面的8不同和后面的12也不同所以0自己单独成段两个12连续相等组成第三段后面的8单独一个和前面的12不同所以是第四段最后一个0单独成段所以第五段。合起来就是8 8 8 / 0 / 12 12 / 8 / 0总共5段。看到这里很多人第一反应是统计一下数列里有几种不同的数字不就行了比如这个样例里有0、8、12三种数字那是不是段数就是3这是最容易踩的误区。因为分段有一个关键词——“连续”。如果同一个数字没有连续出现而是被其他数字隔开那它就必须拆成多个段。这个区分是理解整道题的关键也是后续所有代码思路的基础。1.2 核心思路为什么只需要比较相邻元素就够了理解了题意后真正该思考的问题是怎么用程序判断“新起一段”的时机先举一个简单的例子。假设一队人按身高排队你要数“身高连续相同的队伍”有几组。你站在队伍侧面从头往后看什么时候能确认一组结束了只有一种情况当前这个人和下一个人身高不同。只要相邻两个人的身高不一样就说明前一组到这里结束了新的一组从下一个人开始。如果相邻两个人身高一样他们当然属于同一组不需要额外处理。这个例子背后的逻辑就是相邻比较段与段之间的分界线只能出现在相邻两个元素不相等的位置上。所以段数本质上等于“1 相邻元素发生变化的次数”。那个“1”是第一段的起点每遇到一次相邻不同就说明后面又开了一个新段。不需要维护复杂的状态不需要用到嵌套循环甚至不需要提前排序只需要一趟遍历就能完成。这一点让这道题的时间复杂度是O(n)空间复杂度是O(n)——n是数列长度题目限制n不超过1000所以无论如何都不会超时。极端情况可以帮助验证思路如果n1数列只有一个数那它就只算一段答案是1如果整个数列所有数字全相同比如“5 5 5 5 5”全程没有相邻不同答案也是1如果数列是“1 2 1 2 1”每个数和前一个都不同那答案就是5等于每个元素都单独成段。这三种极端情况都能用“相邻比较”的思路得到正确答案说明思路是自洽的。这也是写任何算法代码之前最值得养成的习惯脑子里先把极端数据过一遍。2. 从零准备一个能跑通的环境2.1 本地环境VS Code配GCC、Ubuntu装gcc怎么选写C语言代码第一步是准备一个能编译、能报错、能调试的环境。很多初学者在环境上浪费大量时间其实选一个自己熟悉的就行不需要追求复杂。如果你用的是Windows我建议用VS Code加MinGW-w64的方案。去官网下载MinGW-w64安装包装好后把bin目录的路径加到系统环境变量Path里。然后在VS Code里装C/C扩展新建一个后缀是.c的文件直接写代码按F5或者用终端里的gcc命令编译运行。看起来步骤多实际上装过一次之后非常顺手报错信息也比老旧的Dev-C清楚得多。如果你是Ubuntu或者其他Linux发行版事情更简单。打开终端执行sudo apt update sudo apt install gcc build-essential装完以后验证一下gcc --version能看到版本号就说明环境正常了。编译代码时我强烈建议加上两个参数gcc -Wall -stdc99 shulie_fenduan.c -o shulie_fenduan-Wall的意思是让编译器把警告全部显示出来很多潜在错误在警告阶段就能发现-stdc99表示使用C99标准后面代码里会用到的变长数组在C99里是合法的。如果你当天实在不方便装本地环境也可以先用在线编译网站应急比如各类在线IDE。但我个人不太推荐长期依赖在线工具因为本地开发环境能让你学会看编译器警告也能练出用gdb调试的本事这两个能力在后续刷题中都非常重要。2.2 提交到CCF CSP平台时最容易忽略的三件事在自己电脑上跑通和提交到OJ上通过中间还隔着几道坎。我见过太多新生在本地运行得好好的一交上去就是编译错误或者答案错误问题基本都出在下面几件事上。第一提交语言选错。CCF CSP系统里提交代码时语言栏一般有C、C、Java等选项。题目是用C语言写的就老老实实选C语言选成C也不至于完全不行但有些细节会因为编译器差异出现奇怪的问题。建议提交前看一眼语言窗口千万别顺手选了Java然后贴C代码。第二代码里混入了文件操作。有些人习惯在本地写freopen来读测试文件比如我为了批量测试数据也经常写freopen(input.txt, r, stdin)。但提交到OJ之前这些代码必须全部删掉。OJ平台自带输入重定向不需要你在代码里指定文件。头上带着freopen的代码在评测机上大概率会出问题轻则运行时错误重则直接找不到文件。第三输出格式和评测机要求不一致。题目说输出一个整数那就只输出那个整数不要自作主张加额外的提示文字。比如printf(结果是%d\n, count)这种带中文提示的输出在本地看着舒服评测机只看它期望的那个整数多出来的字符会被判成答案错误。记住OJ严格比对输出内容哪怕多一个空格都可能出错。3. 手写可满分通过的实现与逐行拆解3.1 基于C99的一份标准满分代码先说结论下面这份代码是我在实际刷题时常用的写法能直接拿满分#include stdio.h int main() { int n; scanf(%d, n); int a[n]; int i; for (i 0; i n; i) { scanf(%d, a[i]); } int count 1; for (i 1; i n; i) { if (a[i] ! a[i - 1]) { count; } } printf(%d\n, count); return 0; }整个代码不到二十行核心部分只有两处一个是把数列存进数组的for循环一个是比较相邻元素的for循环。如果你看到这里还不太明白为什么count的初值是1下面我来逐行拆解。3.2 逐行拆解为什么count要从1开始第一段不用解释scanf读入n然后用一个循环把第二行的n个整数挨个存进数组a。注意这里用的是int a[n]这是C99标准的变长数组允许用变量声明数组大小。因为题目保证n不会超过1000所以这个数组不会占用太多栈空间完全安全。如果你不想依赖C99特性也可以直接写成int a[1005]效果一样。真正的关键在下面int count 1; for (i 1; i n; i) { if (a[i] ! a[i - 1]) { count; } }为什么count初始值要是1不是0因为只要n大于等于1整个数列就至少存在一个段。这个“至少一段”在刚开始遍历之前就已经成立了所以计数器的底数必须是1。接着从下标1开始比较当前元素和它前面的元素。如果相等说明它们属于同一段什么都不用做如果不相等说明这里发生了段的变化就把count加1。也可以换个写法从下标0开始比较a[i]和a[i1]思路完全一样但要注意循环只能走到n-2否则最后一个元素会越界访问。两种写法我都试过相比之下从1开始比较i和i-1循环条件写起来更直观不容易出现边界错误所以我更推荐这一版。举个例子来验证。数列是“8 8 8 0 12 12 8 0”n等于9初始化count1代表第一段已经存在i1a[1]8a[0]8相等不处理i2a[2]8a[1]8相等不处理i3a[3]0a[2]8不等count变2i4a[4]12a[3]0不等count变3i5a[5]12a[4]12相等不处理i6a[6]8a[5]12不等count变4i7a[7]0a[6]8不等count变5。最终count等于5和题目样例完全一致。3.3 亲手验一遍样例和极端用例光看逻辑还不够实际跑一遍更放心。我建议每道题写完至少准备三组测试数据题目给的样例、最小输入、极端输入。第一组就是标准样例。输入9 8 8 8 0 12 12 8 0手动推导可以得到如下表格遍历位置当前值和前一个比较count变化初始化--1i188相等1i288相等1i308不等2i4120不等3i51212相等3i6812不等4i708不等5第二组最小输入1 42n等于1时数组只有一个元素循环体for (i1; i1; i)一次都不会执行count保持初始值1直接输出1。答案是1正确因为单独一个数就是一段。第三组极端输入5 1 1 1 1 1所有数字都相同遍历时每次比较都相等count始终是1输出1。这也符合直觉连续相等的五个数当然只能算一段。我再测一个更极端的交替序列5 1 2 1 2 1相邻元素每次都不相等count依次从1变到5输出5。每一个元素都单独成段答案也是5。这四组数据测完基本可以确定程序没有逻辑问题了。4. 常见错误、坑点与调试实录4.1 新手最容易遇到的三个编译或运行错误数列分段这道题虽然简单但我在给新生答疑时发现翻车点非常集中几乎全在下面三个地方。第一个count初始化成0。这是最容易犯的错误。如果你把int count 1错写成int count 0输出会少1。比如样例明明应该输出5你会得到4。原因很简单第一段没有被计入。遇到这种“答案总差1”的情况第一反应就应该去检查计数器的初始值和边界条件。第二个数组越界。有些同学喜欢从i0开始比较a[i]和a[i1]但循环条件写成了i n导致最后一次循环时访问了a[n]而a[n]根本不存在。数组下标从0开始合法的下标范围是0到n-1。如果要从0开始比较循环条件必须写成i n - 1才会只访问到a[n-1]和a[n-2]。第三个scanf里漏了取地址符。写成scanf(%d, n)而不是scanf(%d, n)代码直接段错误或者编译警告。编译器一般会给出“format argument is not a pointer”之类的提示这说明该写的地方别偷懒。我把这几个错误的具体表现整理成一张速查表方便你对照检查错误类型典型代码现象计数器初值错int count 0;输出比正确答案少1循环越界for (i0; in; i)比较a[i]和a[i1]可能段错误或输出不确定漏写地址符scanf(%d, n);编译警告或运行时崩溃4.2 快速定位问题的两种调试方法如果你运行结果不对别急躁先用最笨但最有效的方法printf大法。在关键的循环里加一行临时输出看看count是怎么变化的。比如你可以临时改成for (i 1; i n; i) { if (a[i] ! a[i - 1]) { printf(i%d, a[%d]%d, a[%d]%d, new segment\n, i, i, a[i], i - 1, a[i - 1]); count; } }跑一遍标准样例观察每次count变化是否和预期一致。如果第一次变化发生在i3而你觉得应该在i2就变化那说明你对数组下标或分界条件的理解有偏差。调试信息记得在提交前删掉否则影响输出格式。如果你的环境支持gdb也可以用命令行调试最基本的步骤。编译时加上-g参数gcc -g shulie_fenduan.c -o shulie_fenduan然后运行gdb ./shulie_fenduan在main里打断点break main run next print i print count逐步执行时打印变量能看到每一行代码执行后状态的变化。对这道题来说printf大法通常就够了gdb更适合复杂程序。但提前把gdb的基础操作练熟后面做CSP第二题、第三题时会很受益。4.3 为什么不要写复杂的分组扫描版本有些同学初学时会下意识写一个内层while循环把连续相同的所有元素一次性跳过去看起来更贴近“分段”的字面意思。大致会长这样int count 0; int i 0; while (i n) { int j i; while (j n a[j] a[i]) { j; } count; i j; }这个写法确实也能AC逻辑也没错。但对比以后你会发现它比“相邻比较”版本多了内层循环和指针移动代码行数更多变量更复杂出错的概率也更高。如果内层循环的边界条件写错比如j n就又会出现数组越界。我建议你理解这种用法因为这个“跳过连续相同区间”的思路在字符串压缩等场景里很有用但在“数列分段”这道题里相邻比较已经是最简洁、最不容易错的解法。写代码不追求炫技追求的是结果正确、边界清楚、别人能看懂这三个目标都满足的版本才是好版本。5. 从数列分段延伸开去的学习价值5.1 相邻比较这个套路还能用到哪“比较当前元素和前一个元素”这个思路并不只在这一道题里有用。我后来的刷题经历里至少有三个场景直接沿用了这套逻辑。第一是游程编码也就是RLE压缩。比如字符串“AAAABBBCC”游程编码会转成“4A3B2C”的形式。转换过程的核心就是先把相邻字符是否相同判断出来找到连续相等区间的终点然后记录字符和数量。这和数列分段的分段逻辑几乎是同一个模子。第二是统计最长连续相同子串的长度。比如数组“1 1 1 0 0 1 1”你要找出最长的连续相同段。这时仍然需要遍历数组比较a[i]和a[i-1]只是把“count”换成了“记录当前长度并更新最大值”。第三是一些双指针题目的入门。很多时候双指针的内层循环就是通过对相同值分组来跳过重复元素从而降低时间复杂度。如果你把数列分段这道题的思路吃透再去看那些“去除重复元素”“字母压缩”的题目会有一种“原来如此”的通透感。这些扩展都不算难但能明显感觉到基础题里藏着的思路会在其他题目里反复出现。这也是为什么我一直建议初学者不要急着跳题先把手上的每一道简单题吃透。5.2 过了这题之后C语言接着学什么数列分段算是数组基础题里比较典型的一道。如果这道题你能在不看答案的情况下独立完成下一个阶段就可以系统地往前推进了。我列一个比较实用的路线供参考先把翁恺老师的C语言视频里数组、循环、函数这三章看完配合视频里的练习题做去PTA或者学校OJ上把“基础编程题目集”做完里面包含字符串逆序、九九乘法表、日期计算等经典题目刷CCF CSP前两题近三年的真题第一题练手第二题练模拟能力再往后就是指针、结构体、文件操作和动态内存这些是后续C语言课程设计的硬性要求。这里特别提醒一句刷题的过程中不要只看通过率更重要的是总结每道题的“坑”。比如数列分段的坑是count初始值字符串逆序的坑是换行符和数组结尾的\0日期计算的坑是闰年判断和大小月。把这些坑集中记下来一段时间以后你会发现自己的代码一次通过率明显提高。5.3 我这几年带新手刷题的一点体会这道题我给不少人讲过有些是刚开始学C语言的大一新生有些是准备CCF CSP认证的考生。每次讲到最后我都会强调同一件事简单题真正考察的不是你有没有学过某个高级算法而是你写代码时有没有边界意识。什么叫边界意识就是看到n等于1时知道答案是1而不是0就是把循环条件写出来先想一遍最后几次访问会不会越界就是写计数器时先问自己这个变量在第一次判断之前应该是什么状态。这些能力不是在长篇大论的教程里学会的恰恰就是在“数列分段”这种简单题目里一次次磨出来的。如果你今天第一次接触这道题我希望你不仅把代码改对更要把代码背下来之后试着默写一遍然后故意改错一个地方看看会出现什么现象。这个过程走一遍你对数组遍历和相邻比较的印象会比做十道类似题目还要深。
返回列表