Codewars Sum of a Sequence Hard Core Version C 解题分析
题目: Sum of a Sequence Hard Core Version
语言: C
调研日期: 2026-10-13
结论: 这是一个等差数列的闭式求和问题。通过方向判定和等差数列公式可以在 O(1) 时间、O(1) 空间内完成;难点不在循环,而在边界定义和整数运算安全性。
1. 题意与约束
给定 begin、end、step,求序列:
begin, begin + step, begin + 2 * step, ...
中所有不越过 end 的项之和。
| 规则 | 含义 |
|---|---|
begin == end |
立刻返回 begin,不关心 step 的方向 |
| 向反方向前进 | 返回 0 |
step != 0 |
题目已保证 |
end 不可恰好到达 |
只累加最后一个未越界项 |
| 序列很大 | 不允许逐项循环 |
例如,begin = 1, end = 5, step = 3 时,序列是 1, 4,答案是 5;不能把 5 错当成最后一项。
flowchart TD
A[输入 begin end step] --> B{begin == end}
B -- 是 --> R1[返回 begin]
B -- 否 --> C{step 的方向远离 end}
C -- 是 --> R2[返回 0]
C -- 否 --> D[求项数 n]
D --> E[求最后一项 last]
E --> F[使用 n * (begin + last) / 2]
F --> R3[返回和]
2. 问题核心
2.1 从枚举转换为数学对象
这是等差数列:
a0 = begin
ak = begin + k * step
令 q 是不越过终点的最大下标:
q = floor((end - begin) / step)
n = q + 1
last = begin + q * step
这里除法只在已经确认方向正确之后使用。若 step 为负,end - begin 也为负,因此两者相除仍得到非负项数。
begin begin + step ... last end
a0 ------------ a1 ------------------- aq ---------+
|<---------------- n items ---------------------->|
S = a0 + a1 + ... + aq = n * (a0 + aq) / 2
2.2 为什么闭式公式正确
把数列正向与反向相加:
S = a0 + a1 + ... + aq
S = aq + a(q-1)+ ... + a0
--------------------------------
2S = (a0 + aq) + (a0 + aq) + ... + (a0 + aq)
共有 n 对,因此:
S = n * (a0 + aq) / 2
这也是本题对"大序列"限制的真正考点:不是优化循环常数,而是消除循环。
3. 三类算法的比较
| 方法 | 时间 | 空间 | 是否能通过大数据 | 主要风险 |
|---|---|---|---|---|
| 逐项循环累加 | O(n) |
O(1) |
否 | 超时,累加过程也可能溢出 |
| 递归累加 | O(n) |
O(n) 调用栈 |
否 | 栈深度限制,仍是线性时间 |
| 等差数列闭式 | O(1) |
O(1) |
是 | 项数、乘法中间值与返回值溢出 |
3.1 逐项循环
long long sum = 0;
for (long long x = begin; /* 未越界 */; x += step) {
sum += x;
}
优点是直观,适合普通版本或小规模输入。缺点是复杂度与项数线性相关;题目明确说明会测试极大序列,因此这种方案即使逻辑正确也会超时。
此外,x += step 可能在判断越界前发生有符号溢出。C 的有符号整数溢出是未定义行为,不是一个可依赖的"自动回绕"机制。
3.2 递归
递归表达式可以写成 S(a, end, step) = a + S(a + step, end, step)。它在数学上自然,但没有改变复杂度,且 C 不保证尾递归优化。Discuss 中有用户专门询问递归是否会因深度限制而失败,题目作者的回应也指出只有编译器将递归优化成迭代时才可能避免该问题。
此方法的价值是练习递归定义,不适合本题的性能约束。
3.3 闭式求和
先判断是否可行,再计算项数和最后一项,最后使用等差数列求和公式。它是唯一同时满足题意和大数据约束的方案。
优点:
-
运行时间不随项数增长。
-
不依赖数组、递归或额外库。
-
可把所有边界规则集中在少量明确分支中。
代价:
-
必须避免把
end直接当作最后一项。 -
必须处理负
step。 -
必须对中间值使用更宽的整数类型或明确约束输入范围。
4. 方法三:闭式公式
以下版本适用于 GCC 或 Clang 常见的 __int128 扩展。它假定 Codewars 测试所需的最终答案能放入 long long;在生产代码中,若结果可能超出返回类型,应改为错误返回、饱和返回或大整数接口,而不能静默强转。
long long sequence_sum(long long begin, long long end, long long step)
{
if (begin == end) {
return begin;
}
if ((begin < end && step < 0) || (begin > end && step > 0)) {
return 0;
}
__int128 first = begin;
__int128 limit = end;
__int128 delta = step;
__int128 terms = (limit - first) / delta + 1;
__int128 last = first + (terms - 1) * delta;
__int128 pair_sum = first + last;
/*
* n 为偶数时先除 n;n 为奇数时,首尾和必为偶数。
* 这避免在可约分的一侧先做不必要的大乘法。
*/
__int128 total = (terms & 1)
? terms * (pair_sum / 2)
: (terms / 2) * pair_sum;
return (long long)total;
}
4.1 这比常见写法更安全的原因
-
先处理相等端点
begin == end是特殊规则,必须放在方向判断之前。否则begin = end = 2, step = -9可能被错误拒绝。 -
使用
last,而非强行使用end当步长不能整除距离时,终点不属于序列。例如
1, 5, 3的最后一项是4。 -
避免浮点数
大整数转为
double后会丢失低位,整数和可能变成错误的邻近值。Discuss 中最高赞的性能相关讨论之一就是大测试下的精度问题,后续回复明确建议不要使用浮点数。 -
扩大中间运算类型
即便最后答案能装入
long long,n * (first + last)也可能先溢出。__int128将end - begin、(n - 1) * step和求和乘法放到更宽的域中计算。 -
约分后再乘法
n * pair_sum / 2虽然数学正确,但可能在除以2前就溢出。根据n的奇偶性先对可整除的一边除以2。
4.2 可移植性取舍
__int128 不是 ISO C 标准类型,但在 Codewars 常见的 GCC/Clang 工具链中通常可用。若目标编译器不支持它,有三个选择:
| 方案 | 依赖 | 适用场景 |
|---|---|---|
明确保证输入和中间值均在 int64_t 范围内 |
无 | 受控题目或协议 |
| 用预检查防止乘法溢出 | 无 | 生产库,需要定义超界错误语义 |
| 使用多精度库 | 外部依赖 | 数值结果必须完全精确且可能很大 |
不要改用 double 来"绕开"整数溢出。那是把确定的整数运算错误替换为静默的精度错误。
5. Discuss 调研结果
5.1 可核实范围
截至 2026-10-13,Codewars 的公开 C 详情页显示该题有 65 次 C 完成;匿名访问 Solutions 页时页面明确提示未获得查看解答的权限。Discuss 页可以公开读取,共显示 152 条讨论,但其内容主要是题目翻译、测试用例和数值类型问题,不是可按"最热门解法"排序的源码库。
因此,不能诚实地把 Discuss 里的三条帖子称为"最热门的三份解答代码"。下面列出的是 Discuss 中实际反复出现、并且与算法选择直接相关的三类思路或工程结论。
| Discuss 中的主题 | 可见票数线索 | 对本题的意义 | 结论 |
|---|---|---|---|
| 闭式数学公式 | 有评论直接指出"不需要循环,存在简单数学公式" | 指向等差数列求和 | 正确的主算法 |
| 整数精度和溢出 | 大数精度讨论获 5 票;另有 long 接近边界时结果翻转的报告 |
闭式公式仍需安全整数运算 | 使用整数和更宽中间类型 |
| 递归 | 递归深度问题获 2 票,并得到作者回应 | 递归没有消除项数依赖 | 不适用于本题 |
此外,票数最高的两类讨论分别是:
-
单元素序列
begin == end应返回该元素,而不是0。 -
某些语言的测试或描述曾对负方向示例存在争议。
这解释了为什么健壮实现必须把"端点相等"放在最前面,并以正式题意而非某个历史测试争议作为行为依据。
6. 你的两种 Solution 评审
6.1 代码来源与总览
代码来源:用户于 2026-10-13 在本对话中粘贴的两份 C 函数。两份函数的接口都与 kata 要求兼容:
typedef long long int lli;
lli sequence_sum(lli begin, lli end, lli step);
| 版本 | 数学逻辑 | 时间 / 空间 | 能否满足 Hard-Core 大数据 | 结论 |
|---|---|---|---|---|
方法一:while 累加 |
小范围、未溢出时正确 | O(n) / O(1) |
否 | 还存在严重的 int 截断和溢出问题 |
| 方法二:递归累加 | 安全算术范围内正确 | O(n) / O(n) 栈 |
否 | 无显式终止分支,且存在栈溢出与 begin + step 溢出 |
| 方法三:闭式公式 | 正确 | O(1) / O(1) |
是 | 需用宽中间类型或显式溢出策略 |
flowchart LR
A[输入 begin, end, step] --> B{方法一}
B --> B1[方向判断]
B1 --> B2[while 每项累加]
B2 --> B3[项数 n 次循环]
A --> C{方法二}
C --> C1[方向判断]
C1 --> C2[begin 加递归调用]
C2 --> C3[n 层调用栈]
A --> D{推荐}
D --> D1[方向判断]
D1 --> D2[等差数列闭式]
D2 --> D3[常数次整数运算]
6.2 方法一:循环累加
原代码:
typedef long long int lli;
lli sequence_sum(lli begin_number, lli end_number, lli step) {
int sum = 0;
int flag = 0;
if (step != 0) {
if (begin_number == end_number) return end_number;
else if (end_number > begin_number && step > 0) {
flag = begin_number;
while (flag <= end_number) {
sum += flag;
flag += step;
}
return sum;
}
else if (end_number < begin_number && step < 0) {
flag = begin_number;
while (flag >= end_number) {
sum += flag;
flag += step;
}
return sum;
}
}
return 0;
}
做对的部分
-
begin_number == end_number在方向判断之前返回,符合题目特殊规则。 -
正、负方向的空序列判定正确。
-
条件使用
<=/>=,不会把不可达的end错加入序列。例如1, 5, 3会累加1 + 4,结果为5。
问题与改进
| 严重性 | 位置 | 问题 | 最小反例或影响 | 分类 | 改进 |
|---|---|---|---|---|---|
| 高 | int sum |
返回类型是 lli,但累加器只有 int;sum += flag 的有符号溢出是未定义行为 |
sequence_sum(1, 65536, 1) 的正确值为 2147516416,仍可放进 long long,却已超过 32 位 int |
ISO C 安全性 | 至少改为 lli sum;本题最终应改闭式公式 |
| 高 | int flag |
将 lli begin_number 赋给 int 可能无法表示;结果是实现定义的,后续比较和循环失真 |
begin_number = 2147483648, end_number = 2147483649, step = 1 |
ISO C 可移植性 / 正确性 | flag 必须至少为 lli |
| 高 | while |
每一项都计算,题目明确的大序列会使运行时间随项数线性增长 | 例如步长为 1、区间含数十亿项时 |
算法复杂度 | 使用第 2 节的等差数列闭式 |
| 中 | flag += step |
即使 flag 改为 lli,在检查循环条件前加法仍可能有符号溢出 |
终点接近 LLONG_MAX 或 LLONG_MIN 时 |
ISO C 安全性 | 闭式版本采用更宽的中间类型;或在生产接口中预检查 |
| 低 | if (step != 0) |
题目已保证步长非零,因此不是必须的分支;保留它无害,但 step == 0 的 API 语义应由调用约定定义 |
本 kata 不会传入 0 |
题意边界 | 可以保留为防御性处理 |
因此,方法一的方向逻辑值得保留,但不能只把 int 改成 lli 就认为完成:那只修复了窄化问题,仍然是无法通过 Hard-Core 大输入的 O(n) 解法。
6.3 方法二:递归累加
原代码:
typedef long long int lli;
lli sequence_sum(lli begin, lli end, lli step) {
if (step == 0) return 0;
if ((step > 0 && begin > end) || (step < 0 && begin < end))
return 0;
return begin + sequence_sum(begin + step, end, step);
}
正确性判断
在 begin + step 和全部累加都不溢出的前提下,方法二对题目的普通输入是正确的。它把"已经越过终点"编码为下一层调用的方向不一致:例如 sequence_sum(1, 5, 3) 会计算 1 + 4 + sequence_sum(7, 5, 3),最后一层返回 0,总和为 5。
但这种终止方式不够直观:当 begin == end 时,它不会直接返回,而是额外递归一层后才因越界返回 0。这恰好能得到正确值,却引入了不必要的算术和风险。
| 严重性 | 位置 | 问题 | 最小反例或影响 | 分类 | 改进 |
|---|---|---|---|---|---|
| 高 | return begin + sequence_sum(...) |
递归层数等于项数,既不是 O(1),也不是尾递归:加法必须等待递归结果返回 |
超长序列导致栈耗尽或超时 | 算法复杂度 | 使用闭式公式 |
| 高 | begin + step |
相等端点仍会先计算下一项,可能在本应直接返回时发生有符号溢出 | sequence_sum(LLONG_MAX, LLONG_MAX, 1) 应返回 LLONG_MAX,但会计算 LLONG_MAX + 1 |
ISO C 未定义行为 | 首先写 if (begin == end) return begin;;最终改闭式 |
| 中 | 返回加法 | 每一层的 begin + 递归和 都可能溢出,即使问题约定的最终值通常可表示,也不能依赖溢出回绕 |
大幅度或大量项的序列 | ISO C 安全性 | 宽中间类型与显式范围策略 |
| 低 | step == 0 |
对本 kata 是不可达防御分支;注释中的"通常返回 0"不应被当作题目规则 | 题目保证 step != 0 |
题意边界 | 注释写明"题外防御策略",或删除 |
若只为教学目的保留递归形式,至少应将等端点作为显式基例:
lli sequence_sum_recursive(lli begin, lli end, lli step)
{
if (begin == end) {
return begin;
}
if ((step > 0 && begin > end) || (step < 0 && begin < end)) {
return 0;
}
return begin + sequence_sum_recursive(begin + step, end, step);
}
这版更容易证明正确,但仍不适合提交本题,因为它没有解决线性时间、线性栈和整数溢出问题。
6.4 方法三:闭式公式(提交版本)
第 4 节的实现现在作为第三种方法纳入比较。它不再按项遍历,也不创建递归调用,而是直接计算:
项数 terms = (end - begin) / step + 1
最后一项 last = begin + (terms - 1) * step
结果 total = terms * (begin + last) / 2
它与方法一、方法二的关键差异如下:
| 维度 | 方法一:循环 | 方法二:递归 | 方法三:闭式公式 |
|---|---|---|---|
| 是否访问每一项 | 是 | 是 | 否 |
| 时间复杂度 | O(n) |
O(n) |
O(1) |
| 额外空间 | O(1) |
O(n) 调用栈 |
O(1) |
| 大数主要风险 | 累加器和循环更新溢出 | 递归参数与返回值溢出 | 差值、乘法中间值溢出 |
| 本题适用性 | 不适用 | 不适用 | 适用 |
方法三通过 __int128 承接差值、末项和乘法的中间结果,并在乘法前根据奇偶性先除以 2。因此它同时解决了前两种方法的复杂度问题和大部分中间溢出问题。它的唯一显著取舍是 __int128 为 GCC/Clang 扩展,而不是 ISO C 标准类型。
6.5 三种方法的结论
方法一比方法二更接近普通工程中的迭代写法,但目前 int / lli 混用会先破坏正确性;方法二的整数类型一致,却用递归把同一个线性问题转成了栈空间问题。两者都没有满足 Hard-Core 的核心约束。
提交本题应采用方法三,即第 4 节的闭式版本。它不依赖递归或外部库;代价是 __int128 属于 GCC/Clang 扩展。若只考虑 Codewars 常见 C 工具链,这是合理的依赖;若要编写跨编译器生产库,则应改为标准 int64_t 加溢出检查或明确的大整数接口。
7. 建议测试集
begin |
end |
step |
期望 | 覆盖点 |
|---|---|---|---|---|
| 2 | 2 | -9 | 2 | 相等端点优先 |
| 2 | 6 | 2 | 12 | 正向整除 |
| 1 | 5 | 3 | 5 | 终点不可达 |
| 1 | 5 | -3 | 0 | 正向区间却向负方向 |
| -1 | -5 | 1 | 0 | 负向区间却向正方向 |
| -1 | -5 | -3 | -5 | 合法负步长 |
| -2 | 4 | 658 | -2 | 步长大于距离,只有首项 |
| 5 | -1 | -2 | 8 | 负方向多项求和 |
| 1 | 65536 | 1 | 2147516416 | 方法一的 32 位 sum 溢出 |
| 2147483648 | 2147483649 | 1 | 4294967297 | 方法一的 flag 窄化 |
LLONG_MAX |
LLONG_MAX |
1 | LLONG_MAX |
方法二不应在相等端点继续计算 begin + step |
还可加入性质测试:
若方向合法,result(begin, end, step)
应等于 begin + result(begin + step, end, step)
前提是 begin + step 没有越过 end。
该性质适合小范围随机输入的交叉验证:用慢速循环作为测试预言机,用闭式公式作为被测实现;不要在正式大数据路径中保留慢速循环。
8. 横向知识扩展
8.1 有限差分
等差数列的一阶差分恒为常数:
a(k + 1) - a(k) = step
"一阶差分恒定"意味着数列是一次函数,因此前缀和是二次函数。这就是为什么可以得到闭式解。类似地:
-
常数序列的求和是一次函数。
-
等差数列的求和是二次函数。
-
二次多项式序列的求和是三次函数。
遇到"重复按固定规则推进并求总量"时,应先判断能否把迭代对象转化为已知数列或前缀和公式。
8.2 算法复杂度与数值范围是两条独立约束
O(1) 不等于绝对安全。闭式解把性能问题解决后,会暴露数值范围问题;反过来,使用 64 位整数也不能让 O(n) 循环通过极大规模测试。
正确方案 = 时间复杂度正确 + 数学边界正确 + 数值表示正确
8.3 C 的整数语义
无符号整数按模运算回绕;有符号整数一旦溢出则触发未定义行为。优化器可能基于"有符号溢出不会发生"的假设重排代码。因此:
-
不要用有符号回绕检测溢出。
-
不要让循环控制变量先溢出再比较。
-
对外部输入先做范围证明,或使用更宽类型/显式检查。
9. 来源
-
Codewars Kata API: 题干、支持语言、题目等级和公开统计。
-
Codewars C Details: C 完成次数和题目详情。
-
Codewars C Discuss: 152 条公开讨论、边界与精度主题的投票信息。
-
Codewars C Solutions: 匿名访问被权限页拦截,未将其当作解答源码来源。