PTA-101b-数据结构(连续子序列最大和、输出 1 ~ n)
算法1-7~9 连续子序列最大和
分数 25
作者 陈越
单位 浙江大学
给定 n 个整数组成的序列 { a1,a2,⋯,a**n },“连续子序列”被定义为 { a**i,a**i+1,⋯,a**j },其中 1≤i≤j≤n。“连续子序列最大和”则被定义为所有连续子序列元素的和中最大者。例如给定序列 { -2, 11, -4, 13, -5, -2 },其连续子序列 { 11, -4, 13 } 有最大的和 20。请编写程序,计算给定整数序列的连续子序列最大和。
本题旨在测试各种不同的算法在各种数据情况下的表现。各组测试数据特点如下:
- 数据 0~6:测试基本正确性;
- 数据 7:103 个随机整数;
- 数据 8:104 个随机整数;
- 数据 9:105 个随机整数。
输入格式:
输入第一行给出正整数 n (≤105);第二行给出 n 个整数,绝对值均不超过 100,其间以空格分隔。
输出格式:
在第一行中输出连续子序列最大和,第二行输出该子序列首尾的数组下标(从 0 开始),以 1 个空格分隔。若解不唯一,则输出最小的数组下标(如样例所示)。
注意:如果序列中所有整数皆为零或负数,则取空子列的结果是最大的,为 0;此时空子序列数组首尾的下标均为 -1。
输入样例:
10 |
输出样例:
11 |
解析
#include <iostream> |
注意
要彻底理解 “连续子序列最大和” 的 C 语言实现,我们需要从问题本质、算法选择、代码结构、核心逻辑、边界处理五个维度逐步拆解,结合具体案例让每一步都清晰可追溯。
一、问题本质与算法选择
1. 问题核心需求
给定 n 个整数序列,需找到:
- 所有连续子序列的和中的最大值(称为 “最大和”);
- 该子序列的首尾下标(从 0 开始),若有多个解,选下标最小的;
- 特殊规则:若所有元素非正(全负或全零),最大和为0(对应 “空子列”),下标为
-1 -1。
2. 算法选择:Kadane 算法
为什么不用暴力法(枚举所有子序列,O (n²) 复杂度)?因为题目中 n 最大为 10⁵,暴力法会执行约 10¹⁰次操作,严重超时。而Kadane 算法通过 “动态维护当前子序列和”,只需遍历 1 次数组(O (n) 复杂度),能高效处理大规模数据。
二、完整代码结构拆解
代码整体分为 5 个模块:头文件引入→输入处理→变量初始化→核心遍历计算→结果输出,我们逐个拆解。
c运行
|
三、关键变量作用详解
每个变量的设计都对应算法逻辑,理解它们是掌握代码的关键:
| 变量名 | 初始值 | 作用 |
|---|---|---|
max_sum |
0 | 存储全局最大和,初始为 0(对应 “空子列” 的默认最大和)。 |
current_sum |
0 | 存储当前正在累加的子序列和,每次遍历都更新,不合适时重置。 |
max_start |
-1 | 存储全局最大子序列的起始下标,初始为 - 1(空子列的默认下标)。 |
max_end |
-1 | 存储全局最大子序列的结束下标,初始为 - 1(空子列的默认下标)。 |
current_start |
0 | 存储当前子序列的起始下标,当前子序列被放弃时,更新为下一个元素下标。 |
四、核心遍历逻辑逐行解析
遍历循环(for (int i = 0; i < n; i++))是算法的核心,我们结合测试样例(输入:10 -10 2 2 3 4 -5 -23 4 7 -21)逐步拆解:
测试样例数组:a[0]=-10, a[1]=2, a[2]=2, a[3]=3, a[4]=4, a[5]=-5, a[6]=-23, a[7]=4, a[8]=7, a[9]=-21
1. 基础操作:current_sum += a[i]
将当前元素a[i]加入 “当前子序列” 的和中,相当于 “继续扩展当前子序列”。
2. 关键判断 1:if (current_sum > max_sum)
当 “当前子序列和” 超过 “全局最大和” 时,说明找到了更优的子序列,需更新全局信息。
- 样例过程:
i=1(a[1]=2):current_sum=2 > 0,更新max_sum=2,max_start=1,max_end=1;i=2(a[2]=2):current_sum=4 > 2,更新max_sum=4,max_end=2;i=3(a[3]=3):current_sum=7 > 4,更新max_sum=7,max_end=3;i=4(a[4]=4):current_sum=11 > 7,更新max_sum=11,max_end=4;- 后续
i=5~9的current_sum均未超过 11,故max_sum最终为 11,下标为1~4。
3. 关键判断 2:else if (current_sum < 0)
当 “当前子序列和” 为负数时,说明这个子序列已经 “无价值”—— 继续往后加元素,只会让和更小(比如当前和为 - 10,加下一个元素 2,和为 - 8,不如直接从 2 开始新子序列)。
- 样例过程:
i=0(a[0]=-10):current_sum=-10 < 0,重置current_sum=0,current_start=1(下一个子序列从 i=1 开始);i=6(a[6]=-23):current_sum=11-5-23=-17 < 0,重置current_sum=0,current_start=7;i=9(a[9]=-21):current_sum=4+7-21=-10 < 0,重置current_sum=0,current_start=10(超出数组范围,无影响)。
4. 隐含逻辑:current_sum == max_sum
当 “当前子序列和” 等于 “全局最大和” 时,不更新全局下标 —— 因为题目要求 “解不唯一时输出最小下标”,首次出现的子序列已经记录了最小下标,后续相同和的子序列无需处理。
五、边界情况处理
代码需覆盖所有特殊场景,确保结果符合题目要求:
场景 1:全负序列(如输入3 -5 -3 -1)
- 遍历过程中,
current_sum始终为负(-5→重置→-3→重置→-1→重置),max_sum保持初始值 0; - 输出:
0和-1 -1(空子列结果)。
场景 2:单元素正(如输入1 8)
i=0时,current_sum=8 > 0,更新max_sum=8,max_start=0,max_end=0;- 输出:
8和0 0。
场景 3:单元素负(如输入1 -6)
current_sum=-6 < 0,重置后max_sum仍为 0;- 输出:
0和-1 -1。
场景 4:混合序列(有正有负,但最大和为正)
- 如样例,最终输出正的最大和及对应下标。
六、效率与安全性说明
- 时间效率:O (n),仅遍历 1 次数组,10⁵个元素可在毫秒级完成计算;
- 空间效率:O (n),用动态内存
malloc存储数组(避免静态数组栈溢出,栈空间通常仅几 MB,10⁵个 int 约 400KB,堆空间足够); - 内存安全:用
free(a)释放动态内存,避免内存泄漏。
七、测试样例完整流程回顾
输入样例的遍历过程可总结为下表,清晰看到变量变化:
| i | a[i] | current_sum | max_sum | max_start | max_end | current_start |
|---|---|---|---|---|---|---|
| 0 | -10 | -10→0 | 0 | -1 | -1 | 1 |
| 1 | 2 | 2 | 2 | 1 | 1 | 1 |
| 2 | 2 | 4 | 4 | 1 | 2 | 1 |
| 3 | 3 | 7 | 7 | 1 | 3 | 1 |
| 4 | 4 | 11 | 11 | 1 | 4 | 1 |
| 5 | -5 | 6 | 11 | 1 | 4 | 1 |
| 6 | -23 | -17→0 | 11 | 1 | 4 | 7 |
| 7 | 4 | 4 | 11 | 1 | 4 | 7 |
| 8 | 7 | 11 | 11 | 1 | 4 | 7 |
| 9 | -21 | -10→0 | 11 | 1 | 4 | 10 |
最终输出:
11 |
通过以上拆解,不仅能理解代码 “怎么写”,更能明白 “为什么这么写”,后续遇到类似问题(如子序列最大和的变种)也能灵活调整。
算法1-10~11 输出 1 ~ n
分数 10
作者 陈越
单位 浙江大学
给定正整数 n,输出 1 ~ n,每个数字占一行。
本题旨在测试不同的算法在各种数据情况下的表现。各组测试数据特点如下:
- 数据 0:测试基本正确性;
- 数据 1:n=1;
- 数据 2:n=1000;
- 数据 3:n=10000;
- 数据 4:n=100000;
- 数据 5:n=1000000。
输入格式:
输入在一行中给出正整数 n (≤106)。
输出格式:
输出 1 ~ n,每个数字占一行。
输入样例:
3 |
输出样例:
1 |
解析
#include <stdio.h> |
注意
为什么加这 1 行就不超时了?(简单解释)
原来的 printf 是 “每次打印都直接告诉系统输出”,1e6 次打印就要麻烦系统 1e6 次,很慢;加了 setvbuf(stdout, NULL, _IOFBF, 16384) 后:
- 系统会先在内存里开一个 “16KB 的小仓库”(缓冲区);
- 每次
printf的内容(比如 “1\n”“2\n”)先存到这个 “仓库” 里; - 等 “仓库” 装满了,再一次性告诉系统 “把仓库里的内容全输出”;
- 这样 1e6 次打印,最后只需要和系统交互几十次(16KB 能装很多行数字),速度就快多了。
这行代码怎么记?(不用死记,理解意思)
stdout:指 “标准输出”(就是我们平时打印的内容);_IOFBF:表示 “全缓冲”(先存满缓冲区再输出);16384:缓冲区大小(16KB,这个数字不用改,直接用就行)。
测试效果
之前简单版跑 n=1e6 可能超时,加了这行后:
- 循环逻辑没变,还是大二能看懂的
for循环; - 运行时间会从几秒降到几十毫秒,完全能通过 OJ 的时间限制。
万一还是超时?(再加 1 行优化输入)
如果 n 特别大,scanf("%d", &n) 也可能有点慢,再补 1 行优化输入缓冲,代码还是很简单:
c运行
|
总结
这个优化方案的优点:
- 简单:只加 1-2 行代码,原有逻辑(循环、printf)完全不变,大二能轻松理解;
- 高效:解决 1e6 规模的超时问题,原理好解释(“先存再批量输出”);
- 通用:以后写其他循环输出的题目,加这行都能提速。
不用害怕 “优化”,这个级别的优化本质就是 “让系统少跑几趟腿”,记住 setvbuf(stdout, NULL, _IOFBF, 16384) 这行 “提速魔法” 就行~
