1. 先说说我为什么会给一道求和题单开一页tracker
那天晚上照例打开牛客的每日一题,弹出来一道名字很朴素的题——“many sum”。我当时的心理活动和大多数刷题人会做的事一模一样:求和题嘛,套个循环加起来不就完了?结果提交上去,超时。再一看数据范围,好家伙,n和m的数量级都不小,每次询问都老老实实地把区间扫一遍,复杂度直接几百万起步,这题要是能过才见鬼了。
那段时间我刚好在坚持做牛客的每日一题,顺手把所有刷过的题都记在一个tracker表格里。起初tracker上只是记录日期、题号、过没过,后来发现光记这些根本没有复盘价值,因为隔两周回去看自己写过的AC代码,经常想不起来当时为什么这么做。我开始在tracker里补充知识点、做题耗时、提交失败次数、当时卡住的原因,再后来甚至给每道题拍了“总结照片”,就是那种一句话核心思路加一段伪代码的小模板。
“many sum”就是我第一次认认真真在tracker里单独开了一页的题。不是因为这道题有多难,而是它太典型了,典型到值得把它从“每日一题打卡记录”里单独拎出来,当作一类题的代表好好拆一遍。它的题目逻辑其实非常干净,但涉及的基础知识点、输入输出处理、边界条件和优化思路,几乎覆盖了算法入门阶段最常见的所有坑。如果你也是刚开始刷牛客、打算用每日一题培养手感但又不知道怎么整理学习成果的人,这篇就当是我把那一页tracker内容翻出来,对着你从头讲一遍。
我用的追踪方式不复杂,甚至可以说不高科技,就是一张Markdown表格加一个简单的统计脚本。但正是这张“很土”的表,让我后来发现了一个反直觉的事实:做了几百道题之后,真正阻碍我的不是难题不会解,而是同一类基础题的细节反复犯错。比如区间求和题里的long long、下标偏移、多组输入,全都是老朋友了,可它们每次都能换个姿势让我重新WA一次。所以我才坚持把每道题的知识点标签、失败原因、一句话结论都记下来,让tracker不只回答“我刷了多少题”,还能回答“我又在哪些地方栽过”。
很多刷题的人喜欢追求题量,觉得一天刷五道才叫努力。我的经验刚好相反——一天一道题,配合认真记录和定期复盘,长期效果比盲目堆量好得多。就像给设备刷系统固件一样,每天一个小增量更新,攒一段时间就是个稳定版本;如果每天灌一堆乱糟糟的补丁,迟早要出问题。牛客每日一题这个入口的好处在于题目质量比较稳定,不会今天冒出一道水题明天来一道劝退题,适合当作长期迭代的素材。而tracker就是你的版本管理工具,帮你看清每次“提交版本”里到底改了什么、为什么改。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. “many sum”到底在考什么:题面拆解与核心思路
2.1 题目到底说了什么
先按我记忆中的题面结构说个大概,它核心就是给你一个长度为n的数组,再给你m次询问,每次询问一对区间左右端点l和r,要求输出子数组从第l个元素到第r个元素的和。题目名字叫“many sum”其实已经很直白了——求和,而且求很多次。这里的“many”就是m很大,大到不允许你每次询问都重新加一遍。
这类题在牛客和各种OJ上都有变体,有的会把数组初始值换成别的公式生成,有的会要求结果对某个数取模,但内核没变:静态数组、多次区间求和。如果你在牛客每日一题里碰到它,大概率也是这个套路,只是数据范围可能换一换。
第一次做的时候我犯了个经典错误:看到题就写俩嵌套循环,外层处理m次询问,内层从l遍历到r累加。本地测试样例过了,心里美滋滋,一提交直接TL。原因很简单——如果n是10^5的数量级,m也是10^5的数量级,最坏情况每次询问都横跨大半个数组,总操作量就到了10^10级别,往好了说跑十几秒,往坏了说永远跑不完。
2.2 前缀和为什么能一次搞定
这道题真正的考点是前缀和。理解起来非常直白:先用一次遍历预处理出一个前缀和数组pre,pre[i]表示原数组从第1个元素加到第i个元素的总和。那么从l到r的子数组和就等于pre[r]减去pre[l - 1]。为什么是l - 1而不是l?因为前缀和是包含当前下标的,pre[r]里已经包含了第l到第r个元素,我要去掉的是第1到第l - 1个元素,pre[l - 1]刚好就是这个范围的总和。
有了这个预处理,每次询问就变成了两次读取加一次减法,O(1)时间出结果。整体复杂度从O(n * m)降到了O(n + m)。这就是这道题从超时到AC的最后一步。
我第一次看别人题解时说“前缀和”三个字,觉得这玩意儿好像没什么了不起,就是算个累计值。但很多时候就是这样,高手和新手的差别不在于知道多少高级数据结构,而在于看到一个题目特征能不能立刻联想到对应的解法。这道题给了你完美暗示——数组是静态的(没有修改操作),询问是大量的、重复的。只要抓住“静态”和“多次询问”这两个关键词,前缀和的小灯就该亮起来。
2.3 一个能直接抄的参考实现
拿C++写的话,核心代码大概长这样:
cpp复制#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
while (cin >> n >> m) {
vector<long long> pre(n + 1, 0);
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
pre[i] = pre[i - 1] + x;
}
while (m--) {
int l, r;
cin >> l >> r;
cout << pre[r] - pre[l - 1] << '\n';
}
}
return 0;
}
这里有两个细节值得展开说。第一,pre数组声明成了vector
用Python刷牛客的话,写法也差不多:
python复制import sys
def main():
data = sys.stdin.buffer.read().split()
idx = 0
out = []
while idx < len(data):
n = int(data[idx]); idx += 1
m = int(data[idx]); idx += 1
pre = [0] * (n + 1)
for i in range(1, n + 1):
pre[i] = pre[i - 1] + int(data[idx])
idx += 1
for _ in range(m):
l = int(data[idx]); r = int(data[idx + 1])
idx += 2
out.append(str(pre[r] - pre[l - 1]))
sys.stdout.write("\n".join(out) + "\n")
if __name__ == "__main__":
main()
Python版我特意用了sys.stdin.buffer.read()一次性读入全部数据再切分开,而不是一行一行input(),因为牛客的多组输入用例数据量大时,逐行input()的开销会让你本来AC的代码硬生生变成TLE。用C++的话,关掉cin和stdio的同步也是同样的道理。这类输入输出优化,属于“看着不重要、关键时刻要命”的细节。
3. 用tracker管理每日一题:我用的打卡模板和复盘节奏
3.1 一张表记住什么才有价值
最初建tracker的时候,我的表头只有日期、题目、状态三列。两周后复盘发现一个问题:看到某个题名,我能想起“这题我过了”,但完全想不起“这题核心是差分”或者“这题我因为数组开太小WA了三次”。通过状态记录得到的复盘信息量约等于零。后来我把表头扩展成这样:
| 日期 | 题目标签 | 难度 | 核心知识点 | 首次通过耗时 | 提交次数 | 失败原因/复盘结论 |
|---|---|---|---|---|---|---|
| 2025-01-15 | 牛客每日一题 | 简单 | 前缀和 | 38分钟 | 4次 | int溢出,改用long long;下标从1开始 |
| 2025-01-16 | 牛客每日一题 | 中等 | 差分数组 | 55分钟 | 6次 | 没考虑区间覆盖边界,差分恢复时忘了加n+1位置 |
表格里最有价值的是“失败原因/复盘结论”这一列。它记录的不是代码报了什么错,而是我为什么写错了、哪一步思路出了问题。比如那道prefix sum的题,我的失败原因写的是“int溢出,改用long long;下标从1开始”。等到一个月后再翻到这个表,扫一眼这一列,就能瞬间回想出这道题的全部关键细节。
你可能会说:这也太费时间了,每道题都要写这么多字。其实不用纠结格式,记给自己看的东西,粗糙一点完全没问题。只有一句话也够用。关键是强迫自己回答一个问题:这道题让我多花时间的那个点是什么?如果你的tracker永远回答不了这个问题,它就只是一张打卡日历,不是学习记录。
3.2 每周复盘怎么做才不流于形式
我给自己定的节奏是每周日晚上花二十分钟做一次周复盘,不是统计“这周做了几题”,而是做三件事。
第一件事是数一数本周每题提交了几次,把提交次数超过三次的题标出来。连续三周被标出来的题目类型,就是我真实的薄弱点。比如我有一段时间反复在“双指针”类型的题上提交四五次,后来我就知道,每天刷到双指针题的时候应该格外谨慎,不能自以为会了就跳过大模拟。
第二件事是给每周的知识点做个简单分布。我不用什么花哨的数据分析工具,就是看tracker表里“核心知识点”列,哪些标签出现得最多,哪些标签是上周刚出现过这周又出现的。如果某个知识点一周出现三次,那这个知识点一定处于“以为自己会了,其实没会”的状态,需要主动找同类题巩固。
第三件事是挑一道“本周最值得回看”的题,可能是最卡的,也可能是意外秒掉的,把它的核心思路重新写一遍——用笔写,不复制粘贴。这个习惯帮我内化了大量题解,比多看几十道视频课都有效。
3.3 要不要写脚本自动统计
刷到一定数量之后,手动记录开始变得烦人,我写了一个极简脚本,直接读取牛客个人提交记录页面,按日期、状态、题名筛出当天通过的题目,自动往tracker里追加一行。核心逻辑不复杂,用requests抓页面文本,再用正则按题目链接和提交时间筛选提交记录。
但后来我主动停掉了全自动统计。原因是自动统计只能告诉我“今天做完了什么”,记录不了“今天为什么卡住”。这两者之间差的是做题过程的思考痕迹,恰恰是tracker最有价值的部分。所以我最后的选择是:自动脚本负责生成骨架(日期、题名、通过状态),人工负责填充肉(核心知识点、失败原因、复盘结论)。这样既不累,又能保留思考痕迹。
如果你不想碰脚本,用Markdown表格或者飞书/Notion多维度表格都行。工具不重要,重要的是你要在里面留下“这道题让我学到什么”的答案,不是只留“我AC了”的结果。
4. “many sum”的同类变式:四种求和场景的解法笔记
4.1 静态区间求和直接前缀和
“many sum”属于最基础的静态区间求和——数组建好之后永远不会修改。这种场景直接上前缀和就行,不必引入更重的数据结构。除了上文的pre数组,还有一种理解方式:差分是前缀和的逆运算,所以如果题目只要求在初始数组基础上做若干次区间加,最后再查询一次最终值,那只要开一个差分数组就能把O(n * m)的区间更新降到O(n + m)。这两个知识点是同一根藤上的,我通常在记录“many sum”时就会顺带把差分也写进备注,因为它们经常在牛客每日一题里交替出现。
4.2 二维矩阵的区间和要用容斥
如果题目从一维数组升级成二维矩阵,同样是静态数据、多次询问子矩阵和,那就需要二维前缀和。核心公式是一条容斥关系:S[i][j]表示从(1,1)到(i,j)的矩形区域和,递推式是S[i][j] = S[i-1][j] + S[i][j-1] - S[i-1][j-1] + a[i][j]。查询以(x1,y1)为左上角、(x2,y2)为右下角的子矩阵和时,用S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1-1][y1-1]。
很多人第一次写二维前缀和时容易算错,主要是漏了中间那个S[i-1][j-1]因为多减了一次又加回来。我第一次写这道题WA了两次,第一次是减错下标,第二次是忘了把左下角换成x1-1。建议你写二维版本之前,先在纸上画一个4x4的小矩阵,把每个格子算一遍再写代码,能省很多调试时间。
4.3 动态修改的单点更新用树状数组
如果题目在静态查询的基础上加了一个新动作:每次询问前会修改某个元素的值,前缀和就没法直接用了,因为每次修改都要重新构建前缀和数组,复杂度又回到O(n)。这时候要切换到树状数组或线段树,单次更新和单次查询都是O(log n)。
树状数组的核心只有两个函数,一个是单点更新(往上走,更新所有覆盖当前点的区间),一个是前缀查询(往下走,累加所有覆盖前缀的区间)。代码量非常少,背完就能用。
cpp复制int bit[N];
void add(int idx, int delta) {
for (; idx <= n; idx += idx & -idx) bit[idx] += delta;
}
long long query(int idx) {
long long s = 0;
for (; idx > 0; idx -= idx & -idx) s += bit[idx];
return s;
}
树状数组能做的操作,线段树基本都能做,但树状数组代码更短、常数更小、更容易写对。如果你看到题目里说“单点更新 + 区间求和”,优先想树状数组。只有当题目要求“区间更新 + 区间求和”时,树状数组才需要加一个差分思想升级成双BIT,否则就用懒标记线段树。
4.4 离线查询的进阶思路
再往外延伸一步,“many sum”这类题的宇宙里还有莫队这种离线算法,专门处理不是单纯求和、而是需要维护某种区间信息的查询(比如区间众数、区间不同元素个数)。这个属于进阶范畴,我刚刷牛客三个月时看到莫队题解一头雾水,后来对着一个典型题硬啃了一个周末,才逐渐明白什么是“离线排序 + 移动指针”。
但我不建议新手在没掌握前缀和、树状数组之前就去碰莫队。就像你不会在系统还没刷稳的时候就刷第三方OS一样,基础数据结构的熟练度上不去,直接入门进阶算法很容易产生“我是不是不适合刷题”的错觉。先把“many sum”这一层的套路吃透,再去探索下一层,节奏才对。
5. 这道题让我踩过的三个坑:溢出、越界和输入输出的格式陷阱
5.1 int溢出的完整排查链路
第一次交“many sum”时,我用的就是int数组,AC代码之外的逻辑看起来完全没问题,本地样例也全部通过,但牛客就是报WA。我当时的第一反应是检查区间下标有没有搞错,翻了半天一点毛病没找到。直到我突然自己想通:数据范围给得很大,我对着n和m的上限粗略算了一下,累计和明显超过int能表示的最大值,然后我把pre数组从int换成long long,一交就过了。
这个错误藏得比较深的地方在于:题目给的数值单个未必爆int,样例也未必会触发溢出,但真正的测试数据会把n个数全部拉满。这种“感觉对了但答案不对”的最常见原因就是数据精度不够。后来我给自己定了一条规则:凡是涉及累计和、连乘积的,一律无脑开long long。这条规则帮我避开了大量类似问题。
5.2 前缀和数组越界的隐蔽陷阱
第二个坑是我在第二次做类似题时才踩的。当时我把前缀和数组开成了和原数组一样大小,也就是n的大小,然后直接写pre[n]。这在读入时没有问题,但查询l = 1时就会出现pre[l - 1]访问pre[0]的情况。如果数组下标从0开始建,pre[0]里存的是第一个元素的值而不是0,之后所有的区间前缀和都会被整体偏移。
排查过程也很典型:本地样例全过,牛客上随机WA,最后我尝试把数组从下标1开始存储,把pre[0]显式初始化为0,代码立刻就通了。这个事情的教训是,前缀和数组一定比原数组长度大1,并且pre[0]必须是0。这算是“数组越界不容易报错、只会在结果上给你捣乱”的经典案例。
5.3 牛客OJ输入输出格式的隐藏要求
第三个坑和用户输入有关。牛客和力扣最不一样的地方,就是牛客很多题目用标准输入输出评测,不会给你封装好的函数,所有数据都要自己从stdin里读。题目可能包含多组测试用例,而且不会提前告知一共有几组。处理方式就是while (cin >> n >> m)这种循环读到EOF为止。
我有一次把单组数据的代码交上去,本地测试时因为手动输入了一组数据,看起来一切正常,但牛客后台给的是多组数据,程序只处理了第一组就结束了,输出自然对不上。后来我写OJ题时养成了习惯:只要题目没说“只有一组测试数据”,一律用while + EOF的形式读入,最后再把输出收集起来统一输出。Python用户注意不要用print逐条打,最好把输出拼成列表后再sys.stdout.write一次性输出,否则频繁刷新的开销也容易TLE。
另外一个很容易被忽略的小细节是换行符。有些平台要求每组输出之间换行,最后一行也要有一个换行符。在C++里用'\n'输出问题不大,但在Python里如果不做处理,最后一行没有换行在某些严格校验的老OJ上也会判PE甚至WA。稳妥起见,输出的最后统一补一个换行。
6. 从“many sum”到一类题:我的题解总结模板长什么样
6.1 为什么要在AC之后再做一次“事后总结”
很多人的题解只写到AC就结束了,问题是“AC”这个状态只能证明代码在这一份评测数据面前是对的,不能证明思维方式已经可迁移到同类题目。我在“many sum”之后专门做过一个练习:连续一周,每天找一道前缀和的变式题,用同一套总结模板写出题解。一周后我基本能做到看到“静态数组 + 区间和”就直接反应出前缀和,不再需要思考。
这套总结模板的核心是逼我把隐形知识显性化。哪怕只写给自己看,也要把“为什么这么做”写出来。写不出来说明还没完全懂,这时候回看题解才有针对性。写了之后,下一次再做到同类题,翻出模板一看,比自己闷头回忆快得多。
6.2 我的模板结构
每道值得总结的题,我会按下面几个部分写:
- 一句话核心:不写题目背景,只写“静态数组多次区间求和,前缀和预处理后O(1)查询”。
- 适用信号:列举什么特征出现时该想到这个解法。比如看到“无修改操作 + 多次区间查询”就是前缀和。
- 复杂度与边界:时间空间复杂度、数据范围、是否需要long long、下标偏移的处理。
- 易错点记录:我自己WA的原因,比如“pre[0]必须为0”“数组长度开成n + 1”。
- 变式联想:如果题目加单点更新,怎么办;如果改成二维,怎么办;如果多次输入多组数据,怎么处理。
写完之后我会把这篇小总结归到tracker的“知识卡片”目录下,标题就用“前缀和 - 多组区间查询的通用解法”这类命名,而不是具体的题号。因为复习的时候按知识点找比按题号找高效得多。
6.3 复用这个模板的举例
拿“many sum”来说,我的卡片是这么填的:
一句话核心:静态数组、多组区间查询,直接前缀和;连续次累加必须用long long。
适用信号:题目描述里出现“q次询问”“区间和”“无修改”,三个词至少中两个。
复杂度:预处理O(n),单次查询O(1),总体O(n + q),空间O(n)。
易错点:pre数组长度n + 1,pre[0] = 0;多组输入用while(cin >> n >> q);所有计数变量能开long long别用int。
变式联想:若带单点修改,改成树状数组;若题目变成“二维子矩阵和”,用二维前缀和和容斥公式;若变成“区间先加某个值、最后统一输出”,用差分。
这样一张卡片写完之后,我不但刷明白了“many sum”,连带把前缀和和差分这一整块知识都串起来了。
6.4 卡片多了以后怎么复习
卡片积累到二三十张之后,我会用“盲测法”复习:随机抽出一张卡片,遮住“一句话核心”和“易错点记录”,只看“适用信号”,试着在30秒内说出解法方向。说不出就翻回去重看一遍。这个方法坚持一段时间,比重新刷一遍题更省时间,效果也更好。
这就是我后来理解的刷题状态:不是把每道题都做得完美,而是让每道做过的题都留下一个可检索、可复用的知识模块。就像给系统刷入了一个个功能明确的模块包,用到的时候直接加载就行,而不是每次从零开始编译。说到底,玩客云能刷成飞牛OS固件包,是因为固件本身是一个完整、稳定的系统镜像;刷题也一样,你搭好的知识卡片和tracker,就是属于你自己的“算法系统镜像”。
