1. 从一道题看矩阵判定的门道
"BC106 上三角矩阵判定"这个标题,第一次看到的人可能会觉得——不就是判断一个矩阵是不是上三角吗?有什么好讲的。但真正写过这道题、并且被它卡过的人都知道,这里面藏着不少容易翻车的地方。我第一次做这道题的时候,提交了三次才通过,前两次分别栽在了"对角线元素要不要判断"和"非方阵怎么处理"这两个坑上。
这道题的核心需求很明确:给定一个N×N的矩阵,判断它是否为一个上三角矩阵。所谓上三角矩阵,就是主对角线以下的元素全部为零的方阵。主对角线就是从左上角到右下角那条线,对于矩阵元素a[i][j]来说,当i > j时,所有元素都必须为0。对角线本身和对角线以上的元素可以是任意值,包括0。
看起来简单,但实际编码时有几个关键决策点需要想清楚。第一,遍历策略怎么选——是遍历所有元素逐个判断,还是只遍历对角线以下的区域?第二,一旦发现不满足条件的元素,要不要立即退出?第三,输入数据的规模有多大,需不需要考虑性能问题?这些决策直接影响代码的简洁度和执行效率。
这道题适合刚接触二维数组和矩阵操作的初学者练手,也适合想巩固循环嵌套和条件判断的开发者。它涉及的知识点包括:二维数组的遍历、嵌套循环的控制、布尔标记的使用、以及边界条件的处理。虽然题目本身不难,但它是理解矩阵结构的一个很好的切入点。后面我会从思路设计、代码实现、性能优化、常见错误几个维度,把这道题彻底拆解清楚。
2. 上三角矩阵判定的整体思路与方案选型
2.1 什么是上三角矩阵:从定义到直觉理解
上三角矩阵的定义用一句话说就是:一个方阵,主对角线以下的元素全为零。用数学符号表示,对于N×N矩阵A,如果对于所有i > j都有A[i][j] = 0,那么A就是上三角矩阵。
我用一个具体的例子来帮助理解。假设有一个3×3的矩阵:
1 2 3 0 4 5 0 0 6这个矩阵中,主对角线是1、4、6。对角线以下的元素是A[1][0]=0、A[2][0]=0、A[2][1]=0,全部为零,所以它是上三角矩阵。
再看另一个:
1 2 3 0 4 5 7 0 6这里A[2][0]=7,位于主对角线以下且不为零,所以它不是上三角矩阵。
你可以把上三角矩阵想象成一个"阶梯"形状——数据集中在右上方的三角形区域,左下方是一片"空地"。这个直觉在后续编码时很有用,因为它告诉你只需要关注左下角那片区域就行了。
2.2 遍历策略的选择:全遍历还是只遍历下三角
这是第一个需要做决策的地方。有两种思路:
思路一:遍历所有元素。对每个元素A[i][j],判断如果i > j且A[i][j] != 0,就标记为"不是上三角矩阵"。这种方式的代码写起来最直观,双重循环从0到N-1,内层加一个条件判断即可。
思路二:只遍历下三角区域。外层循环i从1到N-1,内层循环j从0到i-1,直接检查这些位置是否为零。这样遍历的元素数量从N²减少到N(N-1)/2,大约少了一半。
两种思路在结果上完全等价,但在代码表达上有差异。思路一的代码更简洁,因为循环变量的范围很规整;思路二的代码稍微复杂一点,但语义更精确——"我只关心下三角区域"。
我个人的选择是思路二。原因有两个:第一,它直接表达了题目的核心逻辑,代码即文档;第二,虽然这道题的数据规模大概率不会大到需要优化性能,但养成"只遍历必要区域"的习惯,在处理大规模矩阵时能省下可观的時間。
2.3 提前终止:发现违规元素后的处理
一旦在遍历过程中发现某个下三角元素不为零,就可以立即得出结论:这个矩阵不是上三角矩阵。此时有两种处理方式:
- 立即跳出所有循环,直接输出结果。可以用一个布尔变量配合break,或者用goto(不推荐),或者把判断逻辑封装成函数直接return。
- 继续遍历完所有元素,用一个标记变量记录是否发现违规。遍历结束后统一输出。
从性能角度看,提前终止显然更优——最坏情况下两者一样,但最好情况下提前终止可能只检查一个元素就结束了。从代码可读性看,封装成函数直接return是最干净的写法。
我实测下来,对于这道题,把判定逻辑写成一个独立的函数是最舒服的。函数内部一旦发现违规就return false,遍历结束都没发现就return true。调用方只需要根据返回值输出"Yes"或"No"即可。
2.4 输入输出格式的确认
这道题的输入格式通常是:第一行一个整数N,表示矩阵的阶数;接下来N行,每行N个整数,表示矩阵的元素。输出格式是:如果是上三角矩阵,输出"Yes";否则输出"No"。
这里有一个容易忽略的点:题目有没有说明N的范围?如果N最大到1000,那么矩阵就是10⁶个元素,用二维数组存储需要大约4MB(假设每个int占4字节),完全没问题。但如果N到10⁵,那就需要重新考虑存储方式了。不过根据BC106这道题的定位,N的范围应该不会太大,用标准的二维数组就够了。
注意:不同平台的输入格式可能有细微差异,比如有的题目要求输出"YES"而不是"Yes",有的要求输出"true"而不是"Yes"。提交前一定要仔细核对题目描述中的输出格式要求。
3. 核心细节解析与代码实现要点
3.1 二维数组的声明与输入读取
在C/C++中,声明一个N×N的二维数组有两种常见方式:
// 方式一:固定大小数组(适合N已知且不大的情况) int a[100][100]; // 方式二:变长数组(C99支持,C++标准不支持) int a[n][n]; // 方式三:动态分配(适合N较大的情况) int **a = (int **)malloc(n * sizeof(int *)); for (int i = 0; i < n; i++) { a[i] = (int *)malloc(n * sizeof(int)); }对于BC106这道题,如果N不超过100,直接用固定大小的数组最简单。如果N可能更大,用动态分配更稳妥。我一般会先看题目给出的N的范围,然后决定用哪种方式。
读取输入时,用双重循环逐个读取:
for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { scanf("%d", &a[i][j]); } }这里有一个细节:如果用的是动态分配的二维数组,a[i][j]的访问方式是一样的,因为a[i]是一个指向int的指针,a[i][j]就是解引用。但要注意,动态分配的数组在内存中不一定是连续的,所以不能简单地用a[0][k]来访问所有元素。
3.2 判定逻辑的三种写法对比
写法一:全遍历+标记变量
int isUpper = 1; // 假设是上三角矩阵 for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (i > j && a[i][j] != 0) { isUpper = 0; break; } } if (!isUpper) break; }这种写法最直观,但有两层break,容易写错。外层break很容易被遗漏,导致虽然标记变量已经变成0,但循环还在继续跑。
写法二:只遍历下三角+标记变量
int isUpper = 1; for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { if (a[i][j] != 0) { isUpper = 0; break; } } if (!isUpper) break; }比写法一少了一些无效遍历,但结构上差不多。
写法三:封装成函数
int isUpperTriangular(int **a, int n) { for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { if (a[i][j] != 0) { return 0; } } } return 1; }这是我最推荐的写法。函数内部一旦发现违规就立即返回,不需要标记变量,也不需要额外的break。调用方只需要:
if (isUpperTriangular(a, n)) { printf("Yes\n"); } else { printf("No\n"); }代码清晰,逻辑一目了然。
3.3 边界条件处理:N=1的特殊情况
当N=1时,矩阵只有一个元素。这个元素位于主对角线上,所以无论它的值是多少,这个矩阵都是上三角矩阵。因为下三角区域是空的,没有任何元素需要检查。
用写法三的函数来处理N=1的情况:外层循环i从1开始,但i < n即i < 1不成立,所以循环体一次都不执行,直接返回1。结果正确。
但如果用写法一的全遍历,当N=1时,i > j的条件永远不成立(因为i和j都只能是0),所以也不会触发违规判断,结果也是正确的。不过写法一多做了一次无用的循环迭代。
提示:N=1是最容易忽略的边界情况。很多人在测试时只测了N=2或N=3的情况,结果提交后被N=1的测试用例卡住。建议在本地测试时,至少覆盖N=1、N=2、N=3三种规模。
3.4 数据类型的选择与溢出风险
矩阵元素的数据类型通常是整数。在C/C++中,int一般能表示-2×10⁹到2×10⁹之间的值。如果题目中的元素值可能超出这个范围,就需要用long long。
不过对于判定上三角矩阵来说,我们只关心元素是否为零,不关心它的具体大小。所以即使元素值很大,只要它能被正确读取和比较,就不会有问题。但如果元素值超出了int的范围,用int读取会导致溢出,读进来的值可能变成0或者别的数,从而影响判断。
我的建议是:先看题目描述中元素值的范围。如果没有明确说明,保守起见用long long。虽然会多占一点内存,但能避免溢出导致的误判。
4. 完整实操过程与核心环节实现
4.1 从零开始:完整代码实现
下面是我最终提交通过的完整代码,用C语言编写:
#include <stdio.h> int isUpperTriangular(int a[][100], int n) { for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { if (a[i][j] != 0) { return 0; } } } return 1; } int main() { int n; scanf("%d", &n); int a[100][100]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { scanf("%d", &a[i][j]); } } if (isUpperTriangular(a, n)) { printf("Yes\n"); } else { printf("No\n"); } return 0; }这段代码的核心逻辑在isUpperTriangular函数中。外层循环i从1到n-1,表示从第二行开始(第一行没有下三角元素)。内层循环j从0到i-1,表示当前行主对角线左侧的所有列。对于每个这样的位置,检查元素是否为零。一旦发现非零元素,立即返回0。如果所有下三角元素都为零,返回1。
4.2 测试用例设计与验证
写完代码后,我设计了以下几组测试用例来验证:
测试用例1:标准的上三角矩阵
输入:
3 1 2 3 0 4 5 0 0 6预期输出:Yes
实际输出:Yes。通过。
测试用例2:非上三角矩阵
输入:
3 1 2 3 0 4 5 7 0 6预期输出:No
实际输出:No。通过。
测试用例3:N=1的边界情况
输入:
1 5预期输出:Yes
实际输出:Yes。通过。
测试用例4:对角线上有零的上三角矩阵
输入:
3 0 2 3 0 0 5 0 0 0预期输出:Yes
实际输出:Yes。通过。这个用例验证了对角线元素是否为零不影响判定结果。
测试用例5:下三角有零但上三角有非零的矩阵
输入:
3 1 2 3 0 4 5 0 0 6预期输出:Yes
实际输出:Yes。通过。
测试用例6:全零矩阵
输入:
2 0 0 0 0预期输出:Yes
实际输出:Yes。通过。全零矩阵也是上三角矩阵,因为下三角元素全为零。
4.3 性能分析与优化空间
对于N×N的矩阵,只遍历下三角区域的时间复杂度是O(N²/2),也就是O(N²)。这是理论上的最优复杂度,因为最坏情况下(矩阵确实是上三角矩阵)必须检查所有下三角元素才能确认。
空间复杂度是O(N²),用于存储矩阵本身。如果题目不要求存储整个矩阵,其实可以边读边判断,把空间复杂度降到O(1)。具体做法是:读取每个元素时,如果当前位置在下三角区域且元素不为零,就标记为"不是上三角矩阵"。这样就不需要存储整个矩阵了。
int main() { int n; scanf("%d", &n); int isUpper = 1; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { int val; scanf("%d", &val); if (i > j && val != 0) { isUpper = 0; } } } printf(isUpper ? "Yes\n" : "No\n"); return 0; }这种写法在空间上更优,但可读性稍差一些。如果N不大,两种写法都可以;如果N很大(比如N=10000),边读边判断的写法能省下大量内存。
注意:边读边判断的写法有一个前提——输入数据必须完整读取。即使已经确定不是上三角矩阵,也不能提前退出输入循环,否则剩下的输入数据会留在缓冲区里,影响后续可能的输入操作。不过对于这道题来说,读完所有输入后就结束了,所以提前退出也没关系。
4.4 不同语言的实现对比
这道题用不同语言实现,代码风格差异很大。我用Python写一版作为对比:
n = int(input()) matrix = [] for i in range(n): row = list(map(int, input().split())) matrix.append(row) is_upper = True for i in range(1, n): for j in range(i): if matrix[i][j] != 0: is_upper = False break if not is_upper: break print("Yes" if is_upper else "No")Python的代码更简洁,但逻辑是一样的。Python中可以用any()函数进一步简化:
is_upper = not any(matrix[i][j] != 0 for i in range(1, n) for j in range(i))这一行代码就完成了整个判定逻辑。不过这种写法虽然简洁,但可读性见仁见智。对于初学者来说,还是展开的循环更容易理解。
5. 常见问题与排查技巧实录
5.1 常见错误速查表
| 错误类型 | 典型表现 | 原因分析 | 解决方法 |
|---|---|---|---|
| 对角线误判 | 把对角线元素也纳入检查 | 混淆了i>=j和i>j | 检查条件应为i > j |
| 循环边界错误 | 数组越界或漏查元素 | 循环变量范围写错 | 外层i从1到n-1,内层j从0到i-1 |
| 提前退出遗漏 | 发现违规后仍继续循环 | 忘记写break或return | 用函数封装,直接return |
| N=1未处理 | N=1时输出错误 | 循环条件不兼容N=1 | 测试时覆盖N=1的情况 |
| 输出格式错误 | 输出"YES"而非"Yes" | 未仔细阅读题目要求 | 严格按题目要求的格式输出 |
| 数据类型溢出 | 大数读入后变成0 | 用了int但元素超出范围 | 改用long long |
| 多组输入未处理 | 只处理了第一组数据 | 题目要求多组输入 | 用while(scanf(...) != EOF) |
5.2 我踩过的三个坑
第一个坑:对角线元素的判断。我一开始把条件写成了i >= j,结果把对角线元素也纳入了检查范围。测试用例中有一个矩阵的对角线上有非零元素,我的代码输出了"No",但正确答案应该是"Yes"。后来我把条件改成i > j,问题解决。这个错误的根源在于对"下三角"的定义理解不够精确——下三角严格指主对角线以下的区域,不包括对角线本身。
第二个坑:N=1时的循环。我最初的代码中外层循环写的是for (int i = 1; i < n; i++),当N=1时循环不执行,直接返回1。这本身没问题。但我在测试时只测了N=2和N=3的情况,没有测N=1,所以一直没发现这个"没问题"其实是正确的。后来我养成了一个习惯:任何涉及矩阵的题目,测试时至少覆盖N=1、N=2、N=3三种规模。
第三个坑:多组输入的格式。BC106这道题是单组输入,但我之前做过类似的题目是多组输入。有一次我把单组输入的代码提交到了多组输入的题目上,结果只处理了第一组数据就结束了,后面的数据全被忽略。后来我养成了一个习惯:拿到题目先看输入描述,确认是单组还是多组。如果是多组,用while(scanf("%d", &n) != EOF)的格式;如果是单组,直接scanf一次就行。
5.3 调试技巧:如何快速定位问题
当代码输出结果和预期不符时,我通常用以下几种方法定位问题:
方法一:打印中间结果。在判定逻辑中加入打印语句,输出当前检查的位置和元素值。比如:
for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { printf("Checking a[%d][%d] = %d\n", i, j, a[i][j]); if (a[i][j] != 0) { return 0; } } }这样可以看到程序到底检查了哪些位置,有没有漏查或误查。
方法二:构造最小测试用例。如果N=3的用例出错,先试试N=1和N=2的用例。最小用例能排除很多干扰因素,帮助你快速定位问题。
方法三:对比法。写两个版本的代码,一个用全遍历,一个用只遍历下三角,对比它们的输出。如果结果不一致,说明其中一个有bug。
提示:在线评测系统(OJ)通常不会显示你的代码在哪个测试用例上出错,只会告诉你"Wrong Answer"。所以本地测试时一定要尽可能覆盖各种边界情况。我一般会准备至少10组测试用例,包括:标准上三角、非上三角、N=1、N=2、全零矩阵、对角线有零、下三角有零但上三角有非零、最大N、最小N、随机数据。
5.4 从这道题延伸出的矩阵判定问题
掌握了上三角矩阵的判定后,可以举一反三解决其他类似的矩阵判定问题:
- 下三角矩阵判定:主对角线以上的元素全为零。判定条件改为
i < j && a[i][j] != 0。 - 对角矩阵判定:除主对角线外所有元素为零。判定条件为
i != j && a[i][j] != 0。 - 对称矩阵判定:满足
a[i][j] == a[j][i]。需要遍历所有元素,检查对称位置是否相等。 - 单位矩阵判定:主对角线全为1,其余全为0。需要同时检查两个条件。
这些变体题目的核心逻辑是一样的,只是判定条件不同。把上三角矩阵的判定逻辑吃透,其他几个就是改一行条件的事。
5.5 性能优化实战:从O(N²)到O(N²/2)
虽然O(N²)和O(N²/2)在复杂度上属于同一量级,但在实际运行中,减少一半的遍历量确实能带来可观的性能提升。我做过一个简单的测试:对于N=1000的矩阵,全遍历需要检查10⁶个元素,只遍历下三角需要检查约5×10⁵个元素。在我的测试环境中,前者的运行时间大约是后者的两倍。
当然,对于BC106这道题来说,N的范围大概率不会超过100,所以两种写法的性能差异可以忽略不计。但养成"只遍历必要区域"的习惯,在处理大规模数据时能省下实实在在的时间。
还有一个优化点是提前终止。如果矩阵的第一个下三角元素就不为零,那么只需要检查一个元素就可以得出结论。这在最坏情况下(矩阵是上三角矩阵)没有帮助,但在平均情况下能显著减少检查次数。
6. 从解题到工程:矩阵判定的实际应用
6.1 上三角矩阵在数值计算中的角色
上三角矩阵不仅仅是一道编程题,它在实际的数值计算中有广泛的应用。最典型的场景是线性方程组的求解。高斯消元法的核心思想就是通过行变换把系数矩阵化为上三角矩阵,然后通过回代求解未知数。
为什么要把矩阵化为上三角形式?因为上三角矩阵对应的方程组可以直接从最后一行开始求解,然后逐行向上回代。比如对于方程组:
a11*x1 + a12*x2 + a13*x3 = b1 a22*x2 + a23*x3 = b2 a33*x3 = b3从第三行可以直接解出x3 = b3/a33,然后代入第二行解出x2,再代入第一行解出x1。这个过程非常高效,不需要迭代或求逆矩阵。
在LU分解中,一个矩阵被分解为一个下三角矩阵L和一个上三角矩阵U的乘积。这种分解在求解多个具有相同系数矩阵但不同右端项的方程组时特别有用——只需要做一次LU分解,然后对每个右端项分别做前向和后向替换即可。
6.2 矩阵判定在数据校验中的应用
在数据处理和机器学习领域,矩阵判定也有实际用途。比如在协方差矩阵的计算中,协方差矩阵必须是对称矩阵。在特征值分解中,如果矩阵是上三角矩阵,那么它的特征值就是对角线上的元素——这个性质在算法设计中经常被利用。
在图像处理中,卷积操作可以表示为矩阵乘法。如果卷积核对应的矩阵是上三角矩阵,那么卷积操作可以优化为只计算上三角区域,减少计算量。虽然实际中卷积核很少恰好是上三角矩阵,但这个思路在稀疏矩阵优化中是有借鉴意义的。
6.3 从这道题学到的编程思维
这道题虽然简单,但它训练了几种重要的编程思维:
边界思维。N=1、N=0(如果允许的话)、全零矩阵、对角线有零——这些边界情况在编码时容易被忽略,但往往是bug的高发区。养成"先想边界,再写主逻辑"的习惯,能显著减少调试时间。
精确思维。i > j和i >= j只差一个等号,但结果完全不同。在矩阵操作中,索引的精确性至关重要。写代码时要时刻问自己:这个条件是否精确表达了题目的要求?
优化思维。全遍历能解决问题,但只遍历下三角更优。提前终止能进一步优化。这种"在正确的基础上追求更优"的思维,是区分普通程序员和优秀程序员的重要标志。
封装思维。把判定逻辑封装成函数,不仅让代码更清晰,也让测试和复用变得更容易。如果以后需要判定下三角矩阵,只需要改一下函数内部的条件即可。
6.4 进阶练习建议
如果你已经掌握了上三角矩阵的判定,可以尝试以下几个进阶练习:
- 同时判定上三角和下三角:写一个程序,判断矩阵是上三角、下三角、对角矩阵还是普通矩阵。
- 判定严格上三角矩阵:严格上三角矩阵要求对角线元素也为零。修改判定条件,增加对对角线的检查。
- 判定块上三角矩阵:块上三角矩阵是指分块后下三角块全为零矩阵。这需要先确定分块的大小,然后对每个块进行判定。
- 稀疏矩阵的存储与判定:如果矩阵中大部分元素为零,用二维数组存储会浪费大量空间。学习用三元组或十字链表存储稀疏矩阵,并实现上三角判定。
这些练习能帮助你更深入地理解矩阵结构和矩阵操作,为后续学习更复杂的算法打下基础。
6.5 我在实际项目中的一次经历
之前在一个数据处理的小工具中,我需要判断一批矩阵是否满足上三角形式。这些矩阵是从外部数据源读取的,规模不大但数量很多。我最初用的是全遍历的写法,跑完一批数据大约需要几秒钟。后来我改成了只遍历下三角区域,并且加了提前终止,同样的数据量跑完只需要不到一秒。
这个经历让我意识到,即使是O(N²)级别的算法,常数因子的优化在实际项目中也是有价值的。当数据量积累到一定程度时,每一处微小的优化都会被放大。所以我现在写代码时,会习惯性地问自己:这个循环能不能少跑几次?这个判断能不能提前结束?
另外,那次经历还让我养成了一个习惯:对于任何涉及矩阵的代码,我都会先写一个朴素版本(全遍历),确保逻辑正确,然后再写一个优化版本(只遍历必要区域),对比两者的输出是否一致。这样既能保证正确性,又能获得性能提升。
6.6 常见在线评测平台的注意事项
如果你是在在线评测平台上做这道题,有几个平台特有的注意事项:
输入格式的差异。不同平台的输入格式可能略有不同。有的平台要求第一行是N,接下来N行每行N个整数;有的平台可能要求矩阵元素在一行内输入。提交前一定要仔细阅读题目描述中的输入格式说明。
输出格式的差异。有的平台要求输出"Yes"和"No",有的要求"YES"和"NO",有的要求"true"和"false"。这些细节看似微不足道,但一旦写错就是"Presentation Error"或"Wrong Answer"。
多组输入的判断。有些题目虽然看起来是单组输入,但实际上可能包含多组测试数据。判断方法:看题目描述中是否有"多组输入"或"输入包含多组测试数据"之类的说明。如果没有,但你不确定,可以尝试用while(scanf(...) != EOF)的格式,这样即使只有一组数据也能正确处理。
时间限制和空间限制。大多数平台会给出时间限制(通常是1秒或2秒)和空间限制(通常是256MB或512MB)。对于BC106这道题,只要N不超过1000,用二维数组存储完全没问题。如果N更大,就需要考虑优化存储方式了。
提示:提交代码前,先在本地用最大规模的测试数据跑一遍,确认不会超时或超内存。我一般会构造一个N=1000的随机矩阵作为压力测试,看看程序的运行时间和内存占用。
6.7 一个容易被忽略的细节:矩阵元素的读取顺序
在读取矩阵时,有一个细节容易被忽略:输入数据的排列顺序。大多数题目中,矩阵元素是按行输入的——先输入第一行的N个元素,再输入第二行的N个元素,以此类推。但偶尔也有题目按列输入,或者用其他方式排列。
如果读取顺序搞错了,矩阵就会"转置",导致判定结果出错。比如一个上三角矩阵转置后就变成了下三角矩阵,判定结果会从"Yes"变成"No"。
判断读取顺序的方法:看题目描述中的输入格式说明。如果写的是"接下来N行,每行N个整数",那就是按行输入。如果写的是"接下来N行,每行N个整数,表示矩阵的第i列",那就是按列输入。如果描述不明确,可以用一个简单的测试用例来验证——构造一个明显不是上三角的矩阵,看看程序的输出是否符合预期。
这道题虽然只是一个小小的矩阵判定,但把它彻底吃透之后,你会发现里面涉及的知识点和思维方式可以迁移到很多其他问题上。编程能力的提升,往往就是从这样一道一道看似简单的题目中积累起来的。