
如果你正在准备华为OD机考抽到的是C卷那你对“双机位”三个字应该不陌生——电脑摄像头全程拍脸手机从侧后方固定机位监考切屏、后台搜索、查文档都会被记录。这套环境下的答题节奏和平时在自己IDE里写代码完全不同尤其是“采样过滤”这类看着不难、实则暗藏细节的题成了很多人在考场上翻车的第一站。先说一句大实话华为OD机考是ACM模式不是力扣那种函数填空所有输入输出都要你自己用代码处理。很多人刷习惯了核心代码模式一上机连“数据怎么读进来”都要卡几分钟。“采样过滤”这类题之所以值得单独拿出来讲就是因为它完美结合了三个高频考点规则拆解能力、边界处理能力、多语言迁移能力。你无论用Java、Python、JS、C/C还是Go核心逻辑都一样但每个语言都有各自的坑。这篇文章我会把这道题从题目模型到五种语言的完整实现全部拆开讲包括参考值更新的两种策略、状态机怎么设计、考场上怎么自测边界用例。文章偏实战建议你打开编辑器跟着写一遍光看不练是记不住的。1. 双机位C卷里这道题为什么值得单独拿出来讲1.1 考场环境先认清ACM模式加全程录像华为OD机考的双机位监考意味着你的屏幕操作全程可见一旦切出去查资料、看IDE自动提示、翻本地笔记后台都有可能记录行为。所以备考阶段就要养成“盲打代码”的习惯不能依赖编辑器补全也不能指望考场上临时搜API文档。更关键的是输入输出模式。LeetCode式刷题的接口通常是给你一个函数签名比如public int[] filter(int[] samples, int minVal, int maxVal, int maxDiff, int minNormal)你只需要返回结果。但华为OD机考是ACM模式题目只描述输入格式和输出格式你得自己写Scanner、自己写readline、自己拼输出。很多第一次参加OD机考的人题目逻辑其实想清楚了结果卡在读取多行输入上最后超时或者报运行时错误。“采样过滤”恰好就是这种场景的典型代表。它不是一个复杂算法不涉及图论、动态规划、贪心证明它考的是你能不能把一个业务规则准确翻译成代码并且在边界条件下不犯错。这类题在C卷中出现的频率相当高难度适中属于那种“会者不难、难者不会”的分水岭题型。1.2 题名有迷惑性它其实是一道状态机题“采样过滤”这个名字听起来像数学题或者信号处理题容易让人想复杂。实际上它就是一道非常典型的状态机模拟题核心状态只有三个当前是否已经存在参考值、连续正常计数值、参考值本身。很多人在这一题上栽跟头不是因为不会写循环而是因为规则里有几个模糊地带没有想清楚。比如第一个采样点算不算差值正常一个值范围正常但与前一个参考值差值超标的点要不要更新参考值连续计数是从0开始还是从1开始这些细节在不同版本的题目描述里有细微差异直接决定了答案对不对。这类题考的就是你把一段话翻译成几行if分支的能力。正因为如此它特别适合用来检验一个人的编码基本功。你可以在备考阶段用它做专项训练尤其是针对“读题后快速建模”和“多语言切换”这两个能力。2. 过滤规则拆解范围、差值、连续计数怎么协同2.1 先把规则翻译成参数表不同题库对这道题的文字描述略有差异但核心规则基本一致。我在备考时整理了这样一套参数模型你可以直接参考参数含义举例n采样序列长度6samples采样值序列[10, 13, 15, 8, 20, 30]minVal采样值下限低于它判为异常0maxVal采样值上限高于它判为异常20maxDiff相邻正常采样之间允许的最大差值5minNormal连续正常判定阈值达到后才输出有效值2过滤规则可以拆成四句话范围判断如果sample[i]不在[minVal, maxVal]范围内直接判为异常输出0。差值判断将当前值与“参考值”的差绝对值与maxDiff比较超过则判为异常。连续计数一个点只有同时通过范围判断和差值判断才被计入“连续正常计数”某个点异常时计数清零。输出判断若当前连续正常计数达到minNormal则该点输出原值否则输出0。第一句话很好理解第二句话的关键在“参考值”这个词——它指的是最近一个有效采样值这个值会在遍历过程中动态变化。第三句话则是整个题的状态核心。第四句话决定了最终每个位置输出的是原值还是0。2.2 参考值更新策略是最容易做错的地方参考值更新是这道题最大的坑点。常见的做法有两种策略A只有完全正常的点范围正常且差值正常才更新参考值。策略B只要范围正常的点即使差值异常也更新参考值。这两种策略在平时的例子中可能输出相同但一旦遇到连续跳变的数据结果会完全不同。我构造一个序列来说明比如samples [50, 30, 31, 32] minVal 0, maxVal 100, maxDiff 10, minNormal 2先按策略B推演i0v50范围正常没有参考值差值判断直接通过validCount112输出0ref50。i1v30范围正常但|30-50|2010差值异常validCount清零输出0ref更新为30。i2v31范围正常|31-30|110差值正常validCount112输出0ref31。i3v32范围正常|32-31|110差值正常validCount2达到阈值输出32。最终结果是0 0 0 32。再按策略A推演i0同上validCount1输出0ref50。i1v30范围正常但|30-50|2010差值异常validCount清零输出0ref保持50。i2v31范围正常但|31-50|1910差值异常validCount清零输出0ref保持50。i3v32范围正常但|32-50|1810差值异常validCount清零输出0ref保持50。最终结果是0 0 0 0。看到区别了吗同样是过滤一个输出末尾的32一个全部清零。哪个对就取决于题目里对“有效参考值”的准确定义。我在刷题平台上见到的常见版本多数采用策略B只要值本身在合法范围内就算差值异常也把它作为后续判断的新基准。这背后也有业务逻辑支撑——传感器采到一个跳变值虽然和前面比差异大但它本身是合法读数后续数据更可能围绕它波动而不是围绕旧值波动。2.3 手算推演完整走一遍状态流转为了让你彻底看明白状态是怎么走的我用一个具体例子把所有状态变化列出来。输入6 10 13 15 8 20 30 0 20 5 2逐步演算索引当前值范围正常差值正常连续计数是否输出参考值010是是无参考值直接通过1否1210113是是|13-10|352是2213215是是|15-13|253是1538是否|8-15|750否8420是否|20-8|1250否20530否3020否0否20保持输出结果为0 13 15 0 0 0这个表就是你的代码应该产生的完整状态流转。写完代码后建议自己拿纸笔对着一张类似的表格手动走一遍再和程序输出对比能排查出很多选手思维上的误区。3. 先用Python把状态机写明白再往其他语言上平移3.1 为什么先写Python版本我的习惯是拿到这类题先用Python写一版“验证逻辑是否正确”。原因很简单Python代码量最小字符串处理、数组切片、输入解析都很直接调试成本低。等逻辑验证通过了再花时间翻译成Java或C版本针对语言特性做优化。Python版本的一个重点是输入解析。ACM模式下输入可能跨多行也可能一行内有多个空格分隔。我用sys.stdin.read()一次性读全部内容再按空白切分这样最稳妥不会因为行读完而漏读。import sys def main(): data sys.stdin.read().strip().split() if not data: return idx 0 n int(data[idx]); idx 1 samples [] for _ in range(n): samples.append(int(data[idx])); idx 1 min_val int(data[idx]); idx 1 max_val int(data[idx]); idx 1 max_diff int(data[idx]); idx 1 min_normal int(data[idx]); idx 1 res [0] * n ref 0 has_ref False valid_count 0 for i, v in enumerate(samples): range_ok min_val v max_val diff_ok (not has_ref) or (abs(v - ref) max_diff) if range_ok and diff_ok: valid_count 1 ref v has_ref True if valid_count min_normal: res[i] v elif range_ok: # 范围正常但差值异常更新参考值但清空连续正常计数 ref v has_ref True valid_count 0 res[i] 0 else: # 范围异常不更新参考值清空连续正常计数 valid_count 0 res[i] 0 print( .join(map(str, res))) if __name__ __main__: main()这段代码的核心逻辑全部浓缩在for循环里。三个分支对应三种状态范围正常且差值正常计数加一更新参考值达到阈值就输出原值。范围正常但差值异常更新参考值计数清零输出0。范围异常参考值不变计数清零输出0。3.2 两个最容易搞错的细节第一个细节是has_ref的初始值。第一个采样点没有“前一个参考值”可供比较这时候差值判断应该直接通过而不是去减一个初始化的0。如果初始化ref0而不加has_ref判断那么第一个点如果是负数或者正值都会和0比一次结果就可能错误。所以我在代码里用has_ref标记是否已经存在参考值而不是用ref本身是否等于某个特殊值来判断。第二个细节是范围正常但差值异常的分支到底更新不更新ref。我上面已经用[50,30,31,32]这个例子说明了两种策略的差异。如果你在做题时不确定该用哪种有个笨办法构造一个“连续跳变后逐步恢复稳定”的序列把你的两种实现各跑一遍看输出和题目样例是否一致。如果题目给的样例没有覆盖这个分支那就去看题目文字描述里对“参考值”的原始定义。另一个容易忽略的点是输出格式。ACM模式对输出格式要求很严多一个空格、少一个换行都可能导致Presentation Error。Python里用 .join(map(str, res))比手动循环print更好天然不会在末尾多出空格。3.3 从Python推导题目隐含假设Python版本写完后我会反推一遍题目隐含的三个假设确保自己没有误解题意。第一个假设是范围异常的点是否影响参考值。我的答案是不影响因为它的读数已经超出仪表量程属于“无效读数”不能作为后续参考。第二个假设是连续正常计数是在异常清0还是在“范围正常但差值异常”时也清0。题目描述通常会说“异常点会使计数器归零”而差值异常显然也是一种异常所以我的解法中是清0的。第三个假设是输出结果长度必须和输入长度一致。也就是说过滤后不会像传统滤波算法那样把数组变短而是每个位置要么输出原值要么输出0。这一点决定了你不能用append拼接有效值来缩短数组必须初始化一个和n等长的结果数组。想清楚这三个假设后再看题目你会发现自己对规则的理解已经到位了。4. Java、JS、C、Go同样逻辑四种容易踩的坑4.1 Java版本Scanner不是不能用大数据量别碰Java最容易踩的坑就是用Scanner直接读大量输入。Scanner的nextInt()每次解析都会做很多类型检查和正则匹配在数据量达到10^5级别时会明显变慢严重时直接TLE。我在华为OD机考刷题时习惯用BufferedReader加StringTokenizer组合速度和稳定性都更有保障。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()); int[] samples new int[n]; st new StringTokenizer(br.readLine()); for (int i 0; i n; i) { samples[i] Integer.parseInt(st.nextToken()); } st new StringTokenizer(br.readLine()); int minVal Integer.parseInt(st.nextToken()); int maxVal Integer.parseInt(st.nextToken()); int maxDiff Integer.parseInt(st.nextToken()); int minNormal Integer.parseInt(st.nextToken()); int[] res new int[n]; int ref 0; boolean hasRef false; int validCount 0; for (int i 0; i n; i) { int v samples[i]; boolean rangeOk v minVal v maxVal; boolean diffOk !hasRef || Math.abs(v - ref) maxDiff; if (rangeOk diffOk) { validCount; ref v; hasRef true; res[i] validCount minNormal ? v : 0; } else if (rangeOk) { ref v; hasRef true; validCount 0; res[i] 0; } else { validCount 0; res[i] 0; } } StringBuilder sb new StringBuilder(); for (int i 0; i n; i) { if (i 0) sb.append( ); sb.append(res[i]); } System.out.println(sb); } }Java版本里值得注意的一个细节是Integer.parseInt遇到非数字字符串会抛NumberFormatException。如果题目输入的某个数超出int范围或者格式不干净这行会直接让程序崩溃。不过采样值通常不会超过int范围所以这个问题更多是提醒你输入数据别被污染。输出方面用StringBuilder拼接比反复System.out.print快很多。我见过一个同学每个结果都System.out.println一次数据量一大直接超时。考场上一定要养成“一次性拼接完再输出”的习惯。4.2 JavaScript(Node.js)版本异步IO是最大的坑JSer在机考上最容易翻车的不是算法而是Node.js的异步IO模型。如果直接用readline模块很容易出现主函数跑完了数据还没读完的情况。我建议直接用fs.readFileSync(/dev/stdin, utf8)同步读取全部输入简单粗暴且不会出错。const fs require(fs); function main() { const input fs.readFileSync(/dev/stdin, utf8).trim().split(/\s/); let idx 0; const n parseInt(input[idx]); const samples []; for (let i 0; i n; i) { samples.push(parseInt(input[idx])); } const minVal parseInt(input[idx]); const maxVal parseInt(input[idx]); const maxDiff parseInt(input[idx]); const minNormal parseInt(input[idx]); const res new Array(n).fill(0); let ref 0; let hasRef false; let validCount 0; for (let i 0; i n; i) { const v samples[i]; const rangeOk v minVal v maxVal; const diffOk !hasRef || Math.abs(v - ref) maxDiff; if (rangeOk diffOk) { validCount; ref v; hasRef true; res[i] validCount minNormal ? v : 0; } else if (rangeOk) { ref v; hasRef true; validCount 0; res[i] 0; } else { validCount 0; res[i] 0; } } console.log(res.join( )); } main();JS版本容易踩的坑有三个。第一个是parseInt第二个参数。如果代码是parseInt(08)在老版本Node里可能解析出0因为前导0被当成八进制。稳妥做法是parseInt(input, 10)强制十进制避免意外。第二个是fs.readFileSync(/dev/stdin)在某些在线评测平台是否可用。华为OD机考使用的平台支持Node.js环境/dev/stdin路径在Linux系统上是可以用的。如果你本地用Windows测试路径会不同建议在考场先跑一个最简单的“读一行、输出一行”的测试用例验证IO环境。第三个是new Array(n).fill(0)的用法。fill(0)会正确把数组初始化为全0这是ES6的方法机考环境通常支持。不要用new Array(n).map(() 0)因为new Array(n)创建的是稀疏数组map会跳过空槽位结果还是空数组。4.3 C版本关掉同步流避免cin超时C的坑相对最少主要就是输入输出同步问题。如果用了cin却忘了关同步大数据量下容易超时。核心代码逻辑和其他语言完全一致。#include bits/stdc.h using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin n; vectorint samples(n); for (int i 0; i n; i) { cin samples[i]; } int minVal, maxVal, maxDiff, minNormal; cin minVal maxVal maxDiff minNormal; vectorint res(n, 0); int ref 0; bool hasRef false; int validCount 0; for (int i 0; i n; i) { int v samples[i]; bool rangeOk v minVal v maxVal; bool diffOk !hasRef || abs(v - ref) maxDiff; if (rangeOk diffOk) { validCount; ref v; hasRef true; res[i] validCount minNormal ? v : 0; } else if (rangeOk) { ref v; hasRef true; validCount 0; res[i] 0; } else { validCount 0; res[i] 0; } } for (int i 0; i n; i) { if (i) cout ; cout res[i]; } cout \n; return 0; }这里我用了#include bits/stdc.h这个万能头文件在大部分在线评测系统上都能编译通过。如果考场用的编译器不允许这个头文件就换成iostream、vector、cstdlib、cmath这几个具体头文件。abs函数在C中针对int类型重载返回int绝对值如果差值可能超过int范围建议自己写一个long long abs(long long x)。这道题一般不会但养成用long long处理中间计算的习惯没坏处。还有vectorint res(n, 0)这种初始化比先声明再resize再循环赋值要简洁也更能避免“忘记初始化导致垃圾值”的问题。4.4 Go版本bufio.Scanner的缓冲区上限是暗坑Go在华为OD机考中这几年越来越常见。它编译快、运行快就是API比较啰嗦。最容易踩的坑是bufio.Scanner默认最大token长度是64KB如果某一行输入特别长扫描器会直接报错退出。所以我会把缓冲调大比如1MB。package main import ( bufio fmt os strconv strings ) func main() { scanner : bufio.NewScanner(os.Stdin) scanner.Buffer(make([]byte, 1024*1024), 1024*1024) var nums []int for scanner.Scan() { fields : strings.Fields(scanner.Text()) for _, f : range fields { v, _ : strconv.Atoi(f) nums append(nums, v) } } idx : 0 n : nums[idx] idx samples : make([]int, n) for i : 0; i n; i { samples[i] nums[idx] idx } minVal : nums[idx] idx maxVal : nums[idx] idx maxDiff : nums[idx] idx minNormal : nums[idx] idx res : make([]int, n) ref : 0 hasRef : false validCount : 0 for i, v : range samples { rangeOk : v minVal v maxVal diffOk : !hasRef || abs(v-ref) maxDiff if rangeOk diffOk { validCount ref v hasRef true if validCount minNormal { res[i] v } } else if rangeOk { ref v hasRef true validCount 0 } else { validCount 0 } } out : make([]string, n) for i, v : range res { out[i] strconv.Itoa(v) } fmt.Println(strings.Join(out, )) } func abs(x int) int { if x 0 { return -x } return x }Go里最需要注意的是strconv.Atoi会返回两个值一个是数字一个是错误。我在代码里用_忽略了错误前提是保证输入格式完全合法。如果考场上想更严谨可以加个判断但通常不需要在这上面耗时间。另外Go没有内置abs函数很多人第一次写Go时会下意识地调用math.Abs但math.Abs接收和返回的都是float64用在这里会导致类型不匹配。要么自己写一个int版abs要么用if v ref比较之前先转换为float64再转回来。自己写最直接。4.5 五种语言的对照语法不同状态机结构一模一样把五种语言的代码放在一起看你会发现在for循环内部的三个分支几乎是同一个套路范围正常且差值正常count加一更新ref够阈值输出原值。范围正常但差值异常更新refcount清零输出0。范围异常count清零ref不动输出0。这其实就是核心算法思想与具体编码语言解耦的最好例证。刷题时先把核心逻辑在脑子里或纸上画成状态转移图代码只是把它翻译出来。翻译得久了你会发现Java、C、Go、Python、JS之间的切换越来越自然剩下的差异主要是IO写法、类型声明、标准库API这些“语法皮囊”。5. 机考实战从读题到AC的完整策略5.1 拿到题先别急着打码用十分钟做三件事我在实际考场上如果遇到“采样过滤”这类题不会立刻打开编辑器写循环而是先把输入样例抄在草稿纸上手动走一遍流程确认自己对规则的理解没有偏差。具体来说就是做三件事第一把输入参数列出来确认每个变量名和取值范围。尤其要看清楚n到底代表序列长度还是别的含义minNormal是从第几个点开始输出。第二构造一个能覆盖所有分支的用例。我会在草稿纸上设计几组极端数据比如第一个点就超范围、所有点都正常但连续点数不够、一个点范围正常但差值异常。这些用例不是乱写的它们是为了验证代码里的每个if分支都能被走到。第三想清楚输出格式。是用空格分隔还是换行分隔结尾允不允许有多余空格这题一般是空格分隔单行输出所以我会用字符串拼接。这三件事看起来只花几分钟但能避免一大半“逻辑对了却过不了样例”的问题。很多人在考场上读完题就狂敲代码最后死在题意理解偏差上反而更浪费时间。5.2 构造一套自己的边界测试用例准备几个固定测试用例每次写完代码都用它们做回归是我刷题时比较受益的习惯。针对这道题我常用这几个用例第一个用例最小规模输入 1 50 0 100 10 2 输出 0这个用例验证n1且minNormal2时即使范围差值都正常也会因为计数不够而输出0。第二个用例阈值刚好为1输入 2 50 51 0 100 10 1 输出 50 51验证minNormal1时连续计数从第一个点达标所有正常点都应该原值输出。第三个用例全超范围输入 3 1000 1001 1002 0 100 10 2 输出 0 0 0验证范围异常时的清零逻辑和参考值不更新逻辑。第四个用例就是前面说的跳变恢复场景输入 4 50 30 31 32 0 100 10 2 输出 0 0 0 32这个用例专门用来区分参考值更新策略。如果你的实现采用了策略A跑这个用例会输出全0说明和预期不一致需要检查。第五个用例大数据量性能测试输入 100000 0 1 2 3 4 5 ...递增序列 0 100000 1 1 输出 0 1 2 3 4 5 ...原样这个用例主要验证IO和循环不会超时。尤其是Java的Scanner版本和C未关同步的cin版本跑这个用例就可能露出性能问题。5.3 考场上的时间分配和自测技巧华为OD机考的题量和时间总体偏紧我自己的节奏是先快速扫一遍三题优先做自己最有把握的那一题建立信心和分数基础。遇到“采样过滤”这类模拟题时正常应该在20到30分钟内完成从读题到AC的过程。如果超过40分钟还在一个细节上卡住建议先跳过留着回头再调试。自测时不要只测题目给的样例。题目给的样例通常覆盖不全所有分支你需要自己构造前面的四个用例逐个验证。每改一次代码就把所有用例重跑一遍。如果有用例突然不一致说明引入了新的回归问题。另外打印中间状态是一个很好的调试手段。在for循环里临时加上System.err或console.error输出把每次迭代的rangeOk、diffOk、validCount、ref都打出来和手算表对照很快就能定位到是哪个分支出了问题。提交前把这些调试输出删掉避免影响判题。5.4 双机位监视下的备考方向双机位监考环境下你没法在考场上临时搜索“Java Scanner如何读一行数字”“Go怎么取绝对值”“JS readline怎么同步读”这类语法问题。所以备考阶段一定要把常用语言的IO模板背熟形成肌肉记忆。我自己的做法是准备一个“多语言模板速查表”把每道高频题型的输入读取、输出拼接、核心循环结构都整理成标准范式。考前不看具体题目只把速查表过一遍。这样到了考场上即使心里紧张手指也能条件反射式地敲出正确的IO代码。双机位还有一个隐性影响因为全程录像很多人会在心理上有压迫感感觉背后一直有人盯着一紧张就把简单题写复杂了。针对这个我建议在备考后半段每个礼拜都做一次“模拟考”架一个摄像头对着自己限时120分钟期间不切屏、不查资料、不暂停直接在牛客或平台题库上刷一套真题卷。等你习惯了被镜头盯着的环境真正上考场时心理负担会小很多。再补充一个关于“采样过滤”这类题型的延伸思考它的核心思路是维护一个可变参考值和一个连续状态计数这种模式在华为OD机考的其他题目中也很常见比如“最长连续区间”“数组去重后保持相对顺序”“股票最大收益判断”等。如果你把这道题的状态机吃透了后面遇到这类需要动态维护“上一次有效值”的题目会非常有感觉。我在实际带人刷题的过程中见过太多人栽在同一类细节上不是不会写分支而是没有先想清楚参考值到底怎么更新。希望这篇文章能帮你把这道题的逻辑彻底理顺上考场前再也不用为它焦虑。