ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PTA B1010一元多项式求导:C语言在线处理与边界条件解析

PTA B1010一元多项式求导:C语言在线处理与边界条件解析

做在线题库刷题,最怕碰到哪种题?表面像数学题,实际考输入处理和边界条件。PTA B1010“一元多项式求导”就是非常典型的例子。题面一句话:设计函数求一元多项式的导数。第一次刷到的人,十个有八个觉得高数求导手到擒来,可写完代码一提交,不是格式错就是漏了“零多项式”这个特殊情况,只能拿部分分。

我当初也一样,第一次提交测试点就没全过。翻来覆去调试,最后发现问题根本不在求导公式上,而在两个容易忽略的小细节:常数项求导后为零,不能输出;如果所有项求导后都为零,必须输出“0 0”。就这两行逻辑,挡掉了不少人。这篇文章把题目拆开、把代码写透、把坑位讲完,新手可以直接照着写,刷过的人也可以看看自己当年漏了哪个点。

1. 题目拆解:B1010到底在考什么

1.1 读懂题面:系数和指数成对出现,输出也是成对

题目输入部分看起来简单:以指数递降的方式,输入多项式非零项的系数和指数,数字之间用空格隔开。这句话翻译过来就是,输入是一整串数字,每两个数字一组,前一个是系数,后一个是指数。比如“3 4 -5 2 6 1 -2 0”,表示的数学式子就是 3x^4 - 5x^2 + 6x - 2。

这里有个容易忽略的信息:指数是递降的。题目已经帮你把顺序排好了,你读取的时候不需要自己再排序,输出的时候顺序也自然是正确的。如果忽略了这句话,有些人会额外写排序逻辑,纯属多此一举。

输出部分也有讲究:要求“输出求导后非零项的系数和指数”。注意“非零项”三个字,它是整道题最大的陷阱。求导之后系数为零的那些项,不应该出现在输出里。另外,如果整个多项式求导后没有任何非零项,比如输入“2 0”代表常数2,求导为0,那就要输出“0 0”。这些细节在题面里都写得很清楚,但做题环境下,很多人根本没逐字看完就开始写代码了,后面自然容易踩坑。

1.2 求导规则如何翻译成程序逻辑

数学上,一元多项式的求导规则很简单:对每一项 ax^n,求导结果是 anx^(n-1)。放到程序里就是一组映射关系:系数和指数分别是 (coef, exp),求导后变成 (coefexp, exp-1)。

映射关系虽然简单,但要分情况讨论:

  • 如果 exp 大于 0,正常输出 coef*exp 和 exp-1;
  • 如果 exp 等于 0,代表常数项,求导结果为 0,这一项不输出;
  • 如果所有项都被跳过,也就是整个多项式求导后为零多项式,此时必须输出“0 0”。

这三种情况看起来简单,但顺序很重要:输出“0 0”的判断必须放在整个输入循环结束之后,而不是在循环内。一旦在循环里看到某项被跳过了就急着输出“0 0”,那后面的项都没法处理了。我第一次写的时候就是把零多项式判断写在循环里,结果测样例直接崩了。

1.3 表面是数学题,本质是工程题

这道题放在题库的B级位置,难度不大,但它很能检验一个人处理“不定长输入”和“特殊边界”的能力。很多初学者写代码时习惯先把输入个数读进来,再循环固定次数处理,这种思维在面对这道题时行不通,因为题目不会告诉你总共有多少项,只能通过读文件结束标志来判断。

这和真实业务开发里的“数据流处理”很像:你不知道数据什么时候结束,但必须完整地消费完每一组数据,并且对空结果集做兜底输出。换句话说,B1010的高数知识含量其实很低,工程细节含量很高。能把这道题一次写对,说明你对输入输出边界、状态标记、特殊情况处理都有基本的敏感度。

2. 核心思路:三种常见解法的设计取舍

2.1 流式处理:读一组、算一组、输出一组

这道题最优雅的写法是流式处理。核心原因在于,求导运算是一个只依赖当前项的操作:给定当前项的系数和指数,就能立刻算出结果并输出,完全不需要瞻前顾后。这是一种很典型的“在线处理”思路,能边读边算,就不必把数据全部存下来。

用C语言的scanf实现非常自然:每次成功读入两个整数就代表一个项,处理完直接输出。因为题目保证指数是递降的,输出顺序天然符合要求。这种方案的空间复杂度是O(1),不管输入有多长,内存占用始终不变。在测评系统里,空间限制虽然不算严格,但养成这种不浪费内存的习惯,对以后写大程序有帮助。

很多同学可能会问:如果输出逻辑依赖后面的项怎么办?那就不能流式了,得先存。但本题不需要,所以别把简单问题复杂化。

2.2 数组暂存:先读入再统一输出

第二种写法是用数组把所有输入存下来,处理完再统一输出。这种方案的好处是逻辑更直观,适合还没习惯在线处理思维的初学者。你可以先定义一个足够大的数组,比如系数数组和指数数组各开一个较大空间,然后循环读入,直到文件末尾。

读完以后,再用一个for循环遍历数组,逐个判断指数是否为0,决定是否输出。这种做法的空间复杂度是O(n),当题目输入量很小时完全没问题,但如果把场景扩展到超长的输入,就有点浪费了。

从写代码的角度看,数组版本比流式版本多几行,但思路更接近大多数人入门时的习惯:先把数据准备好,再统一料理。如果你觉得流式处理的“边读边输出”有点绕,先用数组版本写对,再去优化成流式,是很合理的进阶路径。

2.3 结构体与链表方案:数据结构的经典延伸

除了数组,数据结构课上还会教用链表表示多项式。每个结点存一个系数、一个指数,还有一个指向下一个结点的指针。这种方案之所以经典,是因为在多项式加法和乘法这类更复杂的题目里,链表能方便地插入和删除结点。

回到B1010,如果题目要求实现的是“求导并返回新的多项式链表”,那链表方案就是题眼。但B1010本身只是输出求导结果,根本不需要构造新链表。所以我不推荐在这道题上强行写链表,但建议大家课后把链表版本也练一遍:遍历旧链表,对每个结点生成新结点,把指数为0的结点跳过,最后把新链表打印出来。

这一节的引申价值在于:同一个数学问题,不同场景下要求不同。搞清楚“题目到底要我返回值还是直接打印”,是做算法题最容易被忽略的审题能力。

3. 实操过程:C语言标准解法与参数细节

3.1 可以直接抄的C语言解法

下面这版是我最终提交满分的代码,非常短,但每一行都有作用:

#include <stdio.h> int main() { int coef, expo; int first = 1; while (scanf("%d %d", &coef, &expo) == 2) { if (expo > 0) { if (!first) { printf(" "); } printf("%d %d", coef * expo, expo - 1); first = 0; } } if (first) { printf("0 0"); } return 0; }

逐行解释一下:

  • while (scanf(...) == 2):循环读取每一组系数和指数,直到成功读入的整数不足两个。这个条件比!= EOF更严谨,因为哪怕只读到一个整数,也不能构成完整的一组项。
  • if (expo > 0):只有指数大于0的项才需要求导输出。指数为0的常数项,求导后系数为0,不输出。
  • if (!first) printf(" "):在当前项前面补一个空格,但第一项前不补。first=1表示还没有输出过任何项。
  • first = 0:输出一项后,把标记改成0,后续项前面就要补空格。
  • if (first) printf("0 0"):整个循环结束后如果first仍然是1,说明一项都没输出,结果是零多项式,按规则输出“0 0”。

这个写法特别注意:first既控制空格,又充当零多项式的判断标识,一举两得。C语言里整数0为假、非0为真,所以代码里的if (first)和if (!first)语义是反着的,写的时候别搞混。

3.2 Python版本有哪些不一样的坑

用Python写这道题的同学也很多,思路一样,但输入输出方式要注意:

import sys data = sys.stdin.read().strip().split() first = True for i in range(0, len(data), 2): coef = int(data[i]) expo = int(data[i + 1]) if expo > 0: if not first: print(' ', end='') print(coef * expo, expo - 1, end='') first = False if first: print('0 0')

sys.stdin.read()一次性把整个输入读成字符串,再用.split()按空白字符切分,可以稳妥处理输入分散在多行的场景。这里不建议用input()一行行读,因为测试数据的换行位置不可控,用input()容易漏项。

Python版有两个细节和C不同:一是print(end='')控制不换行,二是在项与项之间手动加空格。很多新手直接写print(coef * expo, expo - 1),默认会用空格隔开两个数并且在末尾换行,再叠加循环,格式就全乱了。我的做法是先自己拼好整行再一起输出,逻辑上更好检查。

如果担心输出性能,可以先把所有要输出的内容收集到一个列表里,最后用' '.join(...)一次性输出。这样既避免多次print调用,也方便统一处理末尾空格。不过本题数据量不大,直接print也不会超时。

3.3 边界条件手推演示

拿几个典型输入,亲手过一遍流程比看十遍源码都管用。

第一个是题目样例:输入“3 4 -5 2 6 1 -2 0”。

  • 第1项:(3, 4),指数非0,输出 12 3;
  • 第2项:(-5, 2),指数非0,输出 -10 1,前面补一个空格;
  • 第3项:(6, 1),指数非0,输出 6 0;
  • 第4项:(-2, 0),指数为0,跳过。

最终输出:12 3 -10 1 6 0。

第二个是输入“2 0”,代表常数2。这一项求导后系数为0,被跳过,循环结束后first还是1,于是输出0 0。这正好符合“零多项式”的要求。

第三个是输入“5 2 3 0”,代表 5x^2 + 3。

  • 第1项:(5, 2),输出 10 1;
  • 第2项:(3, 0),跳过。

最终输出:10 1,行尾没有多余空格。

可以做个简单对照表:

输入多项式含义求导后输出核心考察点
3 4 -5 2 6 1 -2 03x^4 - 5x^2 + 6x - 212 3 -10 1 6 0基本求导
2 0常数20 0零多项式
0 0零多项式0 0零多项式
5 2 3 05x^2 + 310 1过滤常数项

这几个例子覆盖了大部分测试点,手推完基本就稳了。

4. 常见问题与排查技巧实录

4.1 零多项式输出问题

这是B1010翻车率最高的地方。典型错误代码是这样的:

if (expo == 0) { printf("0 0"); }

看起来像是“遇到指数为0的项就输出0 0”,但完全错了。指数为0只是说明当前项是常数,求导后该项丢弃,不代表整个多项式一定为零。举个例子,“5 2 3 0”里也有指数0的项,但前面5x^2求导后是10x,整个结果不是零多项式。

正确做法是等整个输入处理完,用一个标记变量判断是否输出过任何项。如果从来没有输出过,才能断定结果是零多项式。我把这个标记起名叫first或者flag,含义见仁见智,但逻辑必须是“全局判断”,不能在单项上做判断。

4.2 行末空格导致格式错误

测评系统对格式比本地IDE严格得多。多一个空格,轻则判Presentation Error,重则直接判Wrong Answer。这道题输出数量的不确定性非常高,你不知道多少个项,所以很容易在循环里随手写出“每个结果后跟一个空格”的代码,结果末尾多出一个空格。

我的建议是用“前置空格法”而不是“后置空格法”:每一次要输出新的项之前,先判断一下前面有没有输出过。有,就打一个空格;没有,就什么也不打。这个思想不只在B1010有用,几乎所有“不定数量输出”的题目都能用。

4.3 输入终止判断出错

初学者绕不过去的第二个坑是输入读不完或者读多了。有人喜欢用for循环固定次数,觉得测试数据总是那么多项。可B1010的输入项数根本没有保证,用固定次数会留下很大的隐患。

还有人想看清楚一行结束没有,用getchar逐字符判断换行,结果把题目写复杂了。标准做法就是利用scanf的返回值:如果成功读入两个整数,返回值为2;如果读到输入末尾,返回EOF。循环条件写成scanf(...) != EOF或者scanf(...) == 2都行。我推荐后者,语义更精准:明确是“每成功读入两个数,就处理一组”,而不是依赖EOF宏。

Python里对应的就是用sys.stdin.read(),一次性喂给split,完全不用关心空白字符是什么种类。这是Python在这种题目上的天然优势。

4.4 数据范围的隐藏炸弹

B1010的题目数据范围比较温和,int完全够用。但如果把场景抽离出来,当系数很大、指数也很大的时候,coef * expo这个乘法结果就可能超出int范围。工程上的习惯是一律用long long保存系数和指数,反正内存不差那几字节,换来的是更安心的数值范围。

我把常见报错整理成一个速查表,方便你自查:

现象可能原因排查方向
样例能过,提交部分正确漏了零多项式全局判断检查循环结束后有没有if (!flag)
输出末尾多空格后置空格法产生行尾空格改用前置空格法
输出全部空白scanf循环根本没进入检查输入读取方式,别用固定次数
输入有换行就漏项用getchar或按行读改用scanf或sys.stdin.read

5. 一题一收获:这道题背后的通用套路

5.1 “在线处理”思维值得刻意练习

做B1010最大的感受就是:不是所有算法题都需要把数据存下来才能处理。很多程序员在真实业务里也常常先囤数据再计算,这本身没错,但有时候会造成不必要的内存开销和代码复杂度。在线处理要求你在数据流到达的瞬间完成计算,只要问题本身不依赖后续数据,就可以这么做。

顺着这种思路,很多基础题都会变得很简单,比如“统计一串数字中的最大值”“计算连续和”。它们都不需要记住全部输入,只要一边读一边更新状态。练习在线处理模式,对理解流式数据、提升代码简洁度都有帮助。

5.2 一个标记变量解决输出格式问题

first或者flag这个小变量,我在很多题里都在用。它的核心价值是记录“某种状态是否发生过”,既可以控制空格,也可以判断是否有输出,甚至可以替代布尔数组。比如判断一个字符串是否有连续重复字符,也可以用类似标记。

有的同学会觉得多定义一个变量很麻烦,宁可在循环里偷懒多输出一个空格。但测评系统不惯着这种偷懒。记住一句话:状态标记是编程基本功,不要怕多写一行first = 0。

5.3 从B1010延伸出去:多项式运算的进阶路线

把一元多项式求导做明白,只是多项式题目的第一小步。后续你可能会遇到多项式加法、多项式乘法,以及用链表表示和操作多项式。这些题目会比B1010复杂很多,但底层仍然是“每项拆开、逐项处理”的思路。

建议刷完B1010后,可以顺手练几道同类型的题,比如多项式相加,或者在线性表章节里出现的“两个有序链表合并”。练的办法很简单:先不看题解,独立写出一个能应对正常输入和边界条件的版本,再和别人的做法对比,看看谁的思路更简洁。这种刻意练习,比盲目刷几十道重复题更有效。

说实话,B1010在整套题库里算不上难题,代码量也小,但它教会我的东西很实在:永远别在没全读完题的情况下写代码,永远记得给特殊情况留一个出口。我后来刷题养成的习惯就是,先把输入输出格式圈出来,再动手写,很多隐藏的坑其实就藏在格式说明里。这道题我写完后还会再过几遍边界,比如“指数是0怎么办”“输入是0 0怎么办”,提交前自己先怼一怼,能省下不少罚时。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进