
华为OD机考C卷里的“最佳升级时间窗”我应该算是被它“坑”过、又被它“救”过的人。上个月集中刷2026新系统卷的时候这道题在好几个考友群里反复出现Java版、Python版、JS版、Go版、C版、C版都有人贴说明它确实是近期出现频率很高的真题。很多人第一眼看到“升级时间窗”这五个字就觉得要上动态规划结果把自己绕进去了。其实它考的是非常经典的滑动窗口、双指针顶多加个前缀和二分。这篇文章我不只给答案会把为什么这么想、代码怎么写、现场机考要注意什么全部讲透给准备C卷双机位的朋友一份能直接抄作业的笔记。1. 题目全貌与考点拆解1.1 真题印象与命题动机先说我实际见到的题目版本。不同批次的C卷题面描述会有一点差异但核心意思基本一致有n天的服务资源占用数据想找一个连续的时间窗做系统升级整个窗口内的资源消耗总和不能超过给定的上限K问满足条件的最长升级时间窗是多长起始下标是多少如果存在多个等长窗口一般要求输出起始下标最小的那个。有的考友反馈他们遇到的版本是“窗口内每个点的风险值都不能超过K”有的版本是“升级期间累积成本不能超过预算”还有的版本要求输出窗口的起止编号而不是长度。这些都属于同一类题处理方式几乎没有区别区别只在于对sum的维护方式。我下面的展开会以“区间和不超过K”为主因为这是最经典、也最能覆盖大部分变体的模型。为什么命题组会喜欢出这道题因为它能同时考察三个基本功连续子区间的枚举能力、单调性分析能力、以及对输入输出边界是否敏感。它不像动态规划那样需要很强的状态设计也不像图论那样需要背模板但写不对的人非常多主要栽在类型溢出、下标输出、窗口收缩逻辑这三件事上。1.2 考点图谱与难度定位这道题挂在C卷里绝大多数情况属于中等偏下难度略高于纯入门题但低于需要状态压缩或线段树的题目。核心考点如下连续子区间的最优枚举暴力两层循环显然不行必须用滑动窗口双指针的单调性证明为什么左右指针都只会往一个方向移动前缀和与二分的替代方案理解它和滑动窗口之间的关系输入输出模式华为OD机试偏向ACM模式代码要自己处理读入和打印边界条件处理包括窗口为空、窗口覆盖整个数组、存在单点恰好等于K等情况如果你的目标是机考高分这类题属于“必须拿全分”的题没有任何借口丢分。它不涉及复杂的空间状态也没有需要记忆的高难度板子只要思路清晰10分钟以内从读题到写出AC代码是正常水平。1.3 题型难度与备考定位我自己在刷题时会把这类题放在“双指针与滑动窗口”专题的第一梯队因为它非常干净。只要你能独立写出这道题的双指针写法就说明你已经掌握了滑动窗口最核心的收缩逻辑后续再遇到“最长无重复子串”“最短覆盖子串”这些滑窗题目理解成本会低很多。备考定位上也值得说一句华为OD机考的时间有限C卷题目不少你不可能每道题都用最精巧的解法所以需要用最小代价拿到稳定分数。滑动窗口模板就是这种“投入产出比”极高的工具代码量短、思路固定、容易验证。2. 核心算法思路从暴力到最优解2.1 暴力枚举为什么不行假设数组长度n最大能到10^5甚至某些版本的数据范围是10^6暴力的思路是枚举所有起点i和终点j再累加区间和判断是否超过K。这样做的复杂度是O(n^2)当n为10^5时需要执行约10^10次加法在OJ上基本就是超时哪怕你用C语言也一样救不回来。还有一个细节是区间和如果每次都用一层循环重新累计总复杂度会到O(n^3)那种代码在示例数据上可能看着对遇到稍微大一点的测试点直接崩。所以优化的第一步就是消除重复计算让每个元素最多被加入一次、移除一次。有人会想到先用前缀和把区间和计算降到O(1)然后枚举起点终点总复杂度还是O(n^2)依然不够。核心问题不是“求区间和慢”而是“枚举区间太多”。我们需要利用数据本身的单调性把有效枚举次数压缩到O(n)。2.2 滑动窗口的收缩逻辑滑动窗口能成立的前提是数组元素非负。非负意味着区间越长、总和越大区间缩短只会让总和减少或不变。基于这个单调性我们可以维护一个当前窗口[left, right]并让right不断向右扩展。每加入一个新元素a[right]后如果sum超过K就需要从左边收缩把a[left]移出去直到sum重新小于等于K。这个收缩过程用while循环而不是if判断因为加进来的那个元素可能非常大一次性移掉一个左边界还不够必须连续移出多个元素才能恢复合法状态。收缩结束后以right为右端点的所有合法窗口里最长的一定是[left, right]本身。因为left已经是被迫移动到最右边才停下的而区间和又满足“越长越大”所以任何比它短的窗口都不会更优。只要每次更新maxLen一趟遍历下来就能拿到全局最优。我用一个实际例子跑一遍你感受会更直观。数组为[3, 1, 2, 4, 1]K为5。right走到2时sum为6超了收缩一次后left变成1sum变成3此时窗口[1, 2]长度为2。right走到3时sum是7连续收缩两次left变成3sum为4窗口只剩[3, 3]长度为1。right走到4时sum为5窗口[3, 4]长度为2。整个过程中最长长度就是2最早满足的是[0,1]起始下标0。这里有一个很隐蔽的边界需要提醒如果某个a[right]本身就大于K那么任何包含它的窗口都不合法。代码里不用特殊处理因为加入它之后sum必然大于Kwhile循环会一直把left推到right1窗口变空后续从right1开始重新累积。这个行为是自动正确的但你要能看懂为什么不会越界。2.3 前缀和与二分作为第二解法除了滑动窗口这道题还有一种经典的“前缀和二分”写法。先预处理前缀和数组prepre[i]表示前i个元素的和。对于右端点right我们要找最靠左的左端点left使得pre[right1] - pre[left] K也就是pre[left] pre[right1] - K。因为数组元素非负pre是单调递增的所以可以在pre[0..right]这个范围内二分查找第一个不小于阈值的下标。对每个右端点做一次二分总复杂度是O(n log n)虽然比滑动窗口慢一点但同样能AC绝大多数情况而且可以加深你对区间和问题的理解。我建议你两种写法都写一遍。滑动窗口是最终考试的首选前缀和二分则能帮你理解“为什么二分要求单调性”一旦将来碰到负数数组、或者需要统计满足条件的区间数量这种思维方式能更快帮你迁移到线段树、平衡树这些更重的工具上。2.4 最优解证明与复杂度分析这个解法的复杂度非常漂亮每个右端点被加入sum一次每个左端点最多被移出一次所以总操作次数是O(n)空间只需要常数个变量和原始数组O(1)额外空间。最优性证明的关键在于“左端点单调不减”。当right向右移动时之前被移出的左端点如果重新放回来只会让窗口变长、sum变大而sum大就意味着更可能超过K所以它不可能在后续成为更优窗口的左端点。这就是为什么我们不需要让left回退也能保证不漏解。如果你在面试时需要给面试官讲思路建议从“暴力为什么慢”开始然后说“因为非负使得区间和具有单调性所以双指针可以线性扫描”最后补一句“每个元素至多进出窗口一次所以O(n)”。这套话术比直接背模板要可信得多。3. 多语言实现与细节对比3.1 Java实现与ACM模式模板华为OD机考很多时候是ACM模式也就是你要自己写完整的类处理从标准输入读取数据、最后用System.out输出。Java版的核心代码如下import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc new Scanner(System.in); int n sc.nextInt(); long limit sc.nextLong(); long[] a new long[n]; for (int i 0; i n; i) { a[i] sc.nextLong(); } int left 0; long sum 0; int maxLen 0; int bestLeft 0; for (int right 0; right n; right) { sum a[right]; while (sum limit) { sum - a[left]; left; } int cur right - left 1; if (cur maxLen) { maxLen cur; bestLeft left; } } System.out.println(maxLen bestLeft); } }这里我特意用了long而不是int因为int最大只能表示约21亿如果n是10^5、每个元素是10^5总和就达到10^10直接溢出。很多考友在本地IDE跑小数据没问题提交到OJ就WA排查半天最后发现是类型问题。记住涉及累加和的操作一律用long起步。如果输入规模非常大Scanner可能比BufferedReader慢不少。我在实测中n到10^6时Scanner的耗时大概是BufferedReader的3到4倍。建议提前准备好BufferedReader版本考试时直接替换import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException { BufferedReader br new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st new StringTokenizer(br.readLine()); int n Integer.parseInt(st.nextToken()); long limit Long.parseLong(st.nextToken()); st new StringTokenizer(br.readLine()); long[] a new long[n]; for (int i 0; i n; i) { a[i] Long.parseLong(st.nextToken()); } // 后续逻辑相同 } }3.2 Python实现简洁但要注意读入速度Python版的代码量最少但性能最容易翻车。不要在for循环里一行一行input()数据量大时会非常慢。统一用sys.stdin.buffer.read一次性读入再解析import sys def main(): data list(map(int, sys.stdin.buffer.read().split())) if not data: return n, limit data[0], data[1] a data[2:2 n] left 0 s 0 max_len 0 best_left 0 for right in range(n): s a[right] while s limit: s - a[left] left 1 cur right - left 1 if cur max_len: max_len cur best_left left print(max_len, best_left) if __name__ __main__: main()Python里while循环虽然简单但在极限数据下频繁进入while收缩累加也会拖慢速度。如果遇到Python版本超时可以再换成前缀和二分利用Python的内置bisect模块速度会更快一些。现场考试时优先用你最有把握的语言不要为了炫技选一个不熟的语言。3.3 JS、Go、C、C的实现与注意点JS在机考环境一般用Node.js。读取输入用fs.readFileSync(/dev/stdin, utf8)注意牛客网和华为OD系统对标准输入的处理略有差异但基本都支持直接读全部输入再解析。代码如下const fs require(fs); const input fs.readFileSync(/dev/stdin, utf8).trim().split(/\s/).map(Number); const n input[0]; const limit input[1]; const a input.slice(2, 2 n); let left 0; let sum 0; let maxLen 0; let bestLeft 0; for (let right 0; right n; right) { sum a[right]; while (sum limit) { sum - a[left]; left; } const cur right - left 1; if (cur maxLen) { maxLen cur; bestLeft left; } } console.log(maxLen, bestLeft);JS的Number是双精度浮点超过2^53会丢精度但机考数据一般不会大到那个程度。如果实在不放心可以用BigInt不过BigInt的运算性能会差一些而且代码更啰嗦非必要不推荐。Go版本需要自己处理字符串转换Scanner默认buffer有限数据行非常长时可能报错最好手动调大Bufferpackage main import ( bufio fmt os strconv strings ) func main() { scanner : bufio.NewScanner(os.Stdin) scanner.Buffer(make([]byte, 1024*1024), 1024*1024) scanner.Scan() line1 : strings.Fields(scanner.Text()) n, _ : strconv.Atoi(line1[0]) limit, _ : strconv.Atoi(line1[1]) scanner.Scan() fields : strings.Fields(scanner.Text()) a : make([]int64, n) for i : 0; i n; i { v, _ : strconv.ParseInt(fields[i], 10, 64) a[i] v } left : 0 var sum int64 maxLen, bestLeft : 0, 0 for right : 0; right n; right { sum a[right] for sum int64(limit) { sum - a[left] left } cur : right - left 1 if cur maxLen { maxLen cur bestLeft left } } fmt.Println(maxLen, bestLeft) }C版本比较常规注意把iostream的同步关闭否则大数据输入可能变慢#include bits/stdc.h using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; long long limit; cin n limit; vectorlong long a(n); for (int i 0; i n; i) { cin a[i]; } int left 0, maxLen 0, bestLeft 0; long long sum 0; for (int right 0; right n; right) { sum a[right]; while (sum limit) { sum - a[left]; left; } int cur right - left 1; if (cur maxLen) { maxLen cur; bestLeft left; } } cout maxLen bestLeft endl; return 0; }C语言需要注意数组大小我的习惯是直接静态声明显得大一点比如long long a[1000005]如果动态分配则要记得free#include stdio.h int main() { int n; long long limit; if (scanf(%d %lld, n, limit) ! 2) return 0; long long a[1000005]; for (int i 0; i n; i) { scanf(%lld, a[i]); } int left 0, maxLen 0, bestLeft 0; long long sum 0; for (int right 0; right n; right) { sum a[right]; while (sum limit) { sum - a[left]; left; } int cur right - left 1; if (cur maxLen) { maxLen cur; bestLeft left; } } printf(%d %d\n, maxLen, bestLeft); return 0; }3.4 多语言性能与易错点对照表语言读入方式易错点典型耗时表现JavaScanner或BufferedReaderint溢出、Scanner慢大数据时BufferedReader优势明显Pythonsys.stdin.buffer.readinput()逐行读太慢、类型默认int但求和需注意极限数据可能比C慢几倍JSfs.readFileSync数字精度、输入写错路径中等规模足够Gobufio.Scanner需要手动ParseInt、Buffer默认上限性能接近CCcin加sync关闭或scanf忘记关同步、string转换性能最佳Cscanf数组静态大小不足、格式化类型写错性能最佳这张表是我自己实际跑数据总结出来的不是理论推断。现场机考不建议临时换语言你平时用什么刷题顺手就选什么重点是模板要提前背熟尤其是IO部分不要现想。4. 机考现场实操双机位环境与调试经验4.1 双机位环境准备华为OD机考现在很多批次采用双机位要求电脑前摄像头是主机位手机作为副机位从侧后方拍摄你的桌面和双手区域。具体角度和距离以官方考试系统说明为准但有几个通用经验可以先准备起来。提前一天把摄像头、麦克风权限全部打开浏览器如果弹出“是否允许使用摄像头”的提示一定点允许不要进入考试后再去找权限设置。手机副机会用到微信小程序或者指定APP需要提前扫码绑定并测试一下手机支架摆放位置保证画面能看到桌面、键盘和屏幕。桌面清理也很重要。考试系统对周围环境有监测逻辑桌面放纸质资料、第二台手机、书籍都很容易被判定异常。我的习惯是把桌面清到只剩电脑、鼠标、键盘和一个水杯水杯放远一些避免遮挡副机位视角。另外电脑上那些会弹更新提示的软件比如向日葵、云盘、各种IM全部退出或设为勿扰模式。切屏次数一旦过多可能会触发防作弊警告这个风险没必要冒。4.2 输入输出模板与自测方法ACM模式下你的代码必须从标准输入读数据向标准输出写结果。不要在本地调试时用print输出额外的提示语提交时忘记删那就是WA。也别在代码里写死样例数据我之前就见过考友把int[] a {3,1,2,4,1}留在代码里样例能过实际测试全零。考试时我会先花一分钟写好IO模板再只修改中间算法逻辑。不同语言这个模板各有固定写法上面代码段里的读入方式可以直接照抄。自测的时候除了题目给的样例一定要再补四个边界用例数组整体都满足条件比如3 10输入1 2 3期望输出3 0所有元素都超过K比如3 5输入6 7 8期望输出0 0注意题面是否允许空窗口单点恰好等于K比如1 5输入5期望输出1 0最大窗口在中间而不是开头比如7 4输入4 1 1 1 4 1 1期望输出3 1这四个用例能覆盖掉绝大多数边界问题。如果跑完自己心里还没底可以用暴力解法写一个对拍器随机生成小规模数组对比暴力结果和滑动窗口结果是否一致。这个习惯在平时练习时特别有用能帮你快速定位逻辑错误。4.3 现场避坑清单下面这些坑是我自己踩过或者在考友群里看到高频反馈后总结出来的每一条都对应过真实丢分案例。第一读入第二行时如果数据量特别大可能会被拆成多行不要默认第二行一定包含全部n个数。稳妥做法是持续读到拿满n个数字为止。第二如果题面要求输出从1开始的编号而你的数组下标是0基输出时一定要加1。这个错误非常隐蔽样例数据有时恰好让0基和1基结果都看起来一样但实际测试点会区分。第三注意输出格式。有的版本要求只输出长度有的要求输出“最小起始时间和最大结束时间”如果题目要两个数别只打印一个。第四如果代码一直超时优先看是不是读入方式太慢再考虑算法复杂度。Java的Scanner、Python的逐行input、Go的默认Scanner Buffer都是常见性能瓶颈。第五编译错误时要看具体是哪一行。C语言数组开小了会越界Java包名不要写Go未使用的变量会编译失败JS的/dev/stdin在Windows本地跑不了但OJ上正常。这些小细节考前都要跑通。5. 常见问题与排查速查表5.1 报错场景与调过办法现象可能原因解决办法样例过了但提交WA类型溢出、下标输出格式不对、题面要求1基把sum和a都换成long检查输出是否加1本地IDE跑正常OJ上RE数组越界、递归栈溢出、File路径读不到检查left是否超过right1改读标准输入大数据超时使用了O(n^2)暴力、读入方式太慢换滑动窗口Java换BufferedReaderPython用sys.stdin.bufferPython用sys.stdin.buffer仍然超时while收缩次数过多、解释器本身慢换前缀和二分或改用其他语言输出结果缺一个数只print了长度没print起始下标对照题面输出格式两个数用空格分隔编译错误提示找不到符号代码里有多余包名、类名错误ACM模式类名一般为Main删掉package声明这套排查逻辑不仅适用于这道题几乎所有OJ问题都能用。先确认读入输出没问题再确认类型范围最后才怀疑算法本身。5.2 变题识别与思维迁移“最佳升级时间窗”这个题有很多变身第一眼可能看着不像但思路完全一样。如果限制从“区间和不超过K”变成“区间内最大值不超过K”那么滑动窗口依然有效但sum的维护不再成立需要额外用一个单调递减队列维护窗口最大值。这个变形就是“滑动窗口最大值”的经典题代码会更长但框架仍然是双指针。如果把K限制去掉改成“求最长连续子数组使得所有元素互不相同”这是“无重复字符的最长子串”的翻版窗口合法性判定换成哈希表计数即可。如果改成“求最短连续子数组使得区间和至少为K”则双指针依然可行但更新逻辑从cur maxLen变成cur minLen并且收缩循环要把所有合法窗口都试完不能只收缩到第一个满足点就停。比较坑的一个变体是数组元素可能为负数。一旦出现负数区间和不再随区间长度单调递增滑动窗口就不能保证正确必须退回到前缀和加有序容器或者线段树。我在备考时会刻意把这类题标记为“滑窗失效预警”提醒自己看到负数条件时快速换思路。还有一类变体要求统计“满足区间和不超过K的连续子数组个数”而不是最长长度。这时滑动窗口的每个右端点对应left到right共right-left1个合法起点累加所有right的贡献即可这个技巧在原题基础上加一行就能实现。5.3 能力延伸从机考到面试手撕代码华为OD的主管面有时候会现场手撕代码我个人观察到“最长连续子数组满足某条件”这类题目出镜率很高。原因很实际它代码量短、能在10分钟内考察完候选人写码和讲解能力又足够区分思路是否清晰。我自己的复习建议是先把这道题的双指针模板写到不用思考闭着眼能敲出来的程度。然后试着给一个完全没学过算法的人讲明白“为什么左边指针不需要回退”。讲得清楚通常说明你是真懂而不是背代码。最后再把前缀和二分版本也默写一遍这两版互相印证比盲目刷十道新题更有用。如果还有余力可以用同样的模板去解几道变形题每道变形题写15分钟就够。我的体会是机考题目的难点往往不是某一个高级数据结构而是你能不能识别出“这题其实是个滑窗”。识别能力只能靠多写、多对比来积累没有捷径。最后再分享一个小技巧本地准备一个“暴力对拍模板”每写一道滑窗题都随机生成小数据对比暴力解和滑窗解。这件事看起来费时间但能省下很多复盘时间尤其是边界条件出问题时它一秒就能告诉你哪个用例挂了。祝大家C卷顺利。