ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

图像模糊处理:二维数组边界与四舍五入的经典陷阱

2026/9/30 4:22:07 拓冰建站 浏览量
图像模糊处理:二维数组边界与四舍五入的经典陷阱 题目编号里的 1128 和 1.8 13一个是《信息学奥赛一本通》里的题号一个是 OpenJudge NOI 题库里多维数组章节的第 13 题两个编号指向的是同一道题——图像模糊处理。作为二维数组章节的经典题目它代码量很小逻辑也很直白但带过的学生里每次提交的 WA 率都不低。原因不是题目难而是它恰好踩中了几个新手最容易犯的毛病边界判断写不全、四舍五入处理错、以及计算结果直接覆盖原始数组。这篇文章就从这三个角度把题目拆开把代码从“能过样例”改到“边界也扛得住”顺带聊聊它背后和真实图像处理的关系。1. 先读题谁在变谁不变1.1 原题逻辑重述题面不复杂。输入一个 n 行 m 列的矩阵每个元素是 0 到 255 之间的整数表示图像对应像素点的灰度值。处理规则只有两条最外一圈像素的灰度值保持不变相当于给图像加了一圈“保护框”。其余中间像素新灰度等于它本身加上上下左右四个邻居一共五个数值的平均值并四舍五入到最接近的整数。最终输出处理后的 n 行 m 列矩阵。n 和 m 的范围是 1 到 100时间空间都非常宽裕哪怕用最朴素的 O(n*m) 算法也能轻松通过。所以这道题的难点从来不是性能而是正确性。这里有一个容易被忽略的隐藏信息如果 n 等于 1 或者 m 等于 1那整张图根本没有“中间点”所有像素都属于最外圈应当原样输出。你拿普通数据测试没什么问题但代码一旦在这类极限输入上崩溃或者算错基本就是边界条件没写全导致的。1.2 题目想考察什么作为数组章节的代表题它集中考察四件事二维数组读入、二维数组遍历、边界位置判断、平均值四舍五入。四件事单独拎出来都不难凑在一起就很容易翻车。很多同学看到题会一拍大腿说“我会了”写完一提交却收到 WA原因就在于他把“数组相邻”理解成了一个抽象下标问题没有把“像素点边界”“邻居”“平均值”这些概念落到代码细节上。同时这道题也是少有的“编码容易、一次 AC 难”的入门题。它非常适合用来训练一个习惯自己构造测试数据。当你学会针对边界位置、最小规模、极端数值设计测试用例时你排查 BUG 的效率会肉眼可见地提升。后面第四章就是围绕这个习惯展开的。1.3 行和列别搞反输入第一行是 n 和 mn 是行数m 是列数。接下来是 n 行每行 m 个数。这里见过太多学生把两层循环写反结果第一轮读入就错位。记忆方法很笨但有效先出现的 n 对应外层循环因为输入是一行一行给的m 是每一行内部的个数对应内层循环。写代码时模拟一遍输入过程基本不会错。2. 核心思路二维数组加邻域均值2.1 为什么天然适合用二维数组图像本身就是二维网格每个像素和上下左右四个方向联系。你用一维数组也能存但算邻居下标时要额外做除法取模既别扭又容易错。直接开一个 a[n1][m1] 存原始图再开一个 b[n1][m1] 存结果图是最符合直觉的方案。我刻意把行和列都多开一位让下标从 1 开始计数。这个习惯在竞赛圈很常见好处是自然的访问 a[i-1][j] 时i 最小是 1即使逻辑上碰到 a[0][j]也不会出现负数下标数组稍微开大一点这些访问都落在合法内存区域内不容易触发奇怪的运行时错误。2.2 邻域平均值的计算方式中间点 (i,j) 的邻居是固定的四个方向上 (i-1,j)、下 (i1,j)、左 (i,j-1)、右 (i,j1)再加上自己 (i,j)一共五个数。把它们相加再除以 5得到的就是新灰度。这个“十字邻域”的结构是整道题最核心的部分也解释了为什么这题归在二维数组而不是一维数组章节里。计算过程里最需要注意的就是四舍五入。在 C/C 中两个 int 相除会直接截断小数部分比如 12/5 得到 2而不是 2.4。如果你直接写 sum/5 再存进 int四舍五入这一步就丢了。正确的做法要么让浮点参与除法要么用整数技巧手动进位这两种方案我都会在第三章给出完整写法。2.3 最核心的坑计算结果不能覆盖原数组这一节必须单独拿出来强调因为它是这道题出现 WA 的头号原因也是最有迁移价值的经验。假设你为了省一个数组直接在 a 数组上原地修改先算 a[2][2]赋成新值再算 a[2][3] 时需要读 a[2][2] 作为左邻居但这个左邻居已经不是原始灰度而是被更新过的结果了。一次污染看似影响不大实际上会像接力赛一样传染下去a[2][4] 又用到 a[2][3]a[3][2] 又用到 a[2][2]越往后错得越离谱最后整张图全是错误数据。我让学生理解这个问题时常打一个比方你有一排杯子每个杯子里是各自的原始果汁。规则是每次取自己杯子和左右两个邻居杯子里的原味果汁各一点混成一杯新口味再把新口味倒回自己的杯子。如果从左往右处理你刚把第一个杯子的果汁换新做第二个杯子时取到的“左边邻居”已经是混合过的了风味早就跑偏。只有先把所有杯子的取样结果存在另一个托盘里最后再一次性倒回才能保证每一杯用的都是原始口味。对应到代码就是原始图存在 a计算结果全部写进 ba 始终保持只读。全部算完后再输出 b。这就是“读原值、写新数组”的原则后面做动态规划、图论算法时同样用得上。3. 两种四舍五入写法代码全解析3.1 浮点写法直观适合第一次理解先给一个最容易看懂的版本#include iostream using namespace std; int a[105][105], b[105][105]; int main() { int n, m; cin n m; for (int i 1; i n; i) for (int j 1; j m; j) cin a[i][j]; for (int i 1; i n; i) { for (int j 1; j m; j) { if (i 1 || i n || j 1 || j m) { b[i][j] a[i][j]; } else { double sum a[i][j] a[i-1][j] a[i1][j] a[i][j-1] a[i][j1]; b[i][j] (int)(sum / 5.0 0.5); } } } for (int i 1; i n; i) { for (int j 1; j m; j) { if (j 1) cout ; cout b[i][j]; } cout endl; } return 0; }逐段解释。a 和 b 都开 105×105是因为 n、m 最大 100多开一些给边界访问留余地。读入阶段循环从 1 开始所以输入矩阵第 1 行存在 a[1]第 n 行存在 a[n]非常直观。计算阶段里边界判断 if (i 1 || i n || j 1 || j m) 命中即原样复制。非边界点则先求五数之和用 double 保存再除以 5.0 得浮点平均值加 0.5 后强转 int。因为浮点数加 0.5 再截断等价于四舍五入2.4 变 2.9 截断成 22.6 变 3.1 截断成 32.5 变 3.0 截断成 3。输出阶段用 if (j 1) 控制行内空格这样行尾不会残留多余空格在任何判题系统下都是安全的。3.2 整数写法竞赛更推荐的手动进位浮点写法在这道题里不会出精度问题但 OI 圈一直有“能整不浮”的习惯因为浮点数在极端情况下可能出现 2.500000001 或 2.499999999 这类表示误差。整数写法能完全避开这块风险代码也更干净for (int i 1; i n; i) { for (int j 1; j m; j) { if (i 1 || i n || j 1 || j m) { b[i][j] a[i][j]; } else { int sum a[i][j] a[i-1][j] a[i1][j] a[i][j-1] a[i][j1]; b[i][j] (sum 2) / 5; } } }核心就一行(sum 2) / 5。为什么加 2因为 sum 5q r其中 r 是 0 到 4 的整数sum/5 的商 q 是整数部分r/5 是小数部分。四舍五入要看 r/5 是否大于等于 0.5也就是 r 是否大于等于 2.5。r 是整数所以 r 3 时进位。再加上 2 之后(sum2)/5 q (r2)/5r2 在 r3 或 4 时大于等于 5整数除法正好进位。于是这一个表达式就把四舍五入做完了。如果不喜欢“加 2”的技巧也可以写成分支形式int q sum / 5; int r sum % 5; if (r 3) q; b[i][j] q;效果完全一样只是多几行。我建议至少理解其中一种并且能在考场上稳定默写出来。3.3 另一种边界处理先外圈再内核如果不喜欢在嵌套循环里写复杂判断可以把边框复制和内部计算拆成两个循环。第一轮用四个 for 循环把上下左右四条边拷贝进 b第二轮 i 从 2 到 n-1、j 从 2 到 m-1 只计算内部点。这样做逻辑清晰而且天然规避 n1 或 m1 时内部循环不执行的问题。对于这道题两种处理方式都能过我建议你选自己顺手、不容易写错的那种。我自己教学生时更倾向于“先复制边框再算内核”因为当 n、m 很小或者很大时这个结构能减少判断出错的可能。4. 边界条件与经典错误自查4.1 必测的几组极端数据我平时调试这题会先跑这几组数据再提交输入 1 1只有一个像素 5输出必须是 5输入 1 5一行五个像素全部边界原样输出输入 5 1一列五个像素全部边界原样输出输入 2 2整张图 4 个点全是外圈原样输出输入 3 3构造一个全 255 的图内部点平均值显然还是 255输出全 255再构造一个灰度值递增的 3×3 图手算验证中间点。第一组和第二组专门用来检查 n1、m1 这种极限能暴露访问 a[i1][j] 越界的问题最后一组能验证四舍五入是否写对。绝大多数第一次提交就 WA 的程序至少有一组过不了。4.2 手算一个小例子假设 3 行 3 列输入3 3 1 2 3 4 5 6 7 8 9四个角 1、3、7、9 是边界原样输出。四条边上的 2、4、6、8 也是边界原样输出。只有中心的 5 需要计算sum 5 2 4 6 8 25平均值 5四舍五入还是 5。最终输出等于输入。这个例子的意义不是让你手工算多复杂的数据而是帮你推演出“哪些点是边界哪些点是内部”。一旦代码里边界判断写漏四个角或者某条边就会悄悄出错。4.3 数组越界和 RE有些同学喜欢把数组开成 a[100][100]然后循环从 0 开始读到 99这本身没有错但一旦想用 a[i-1][j] 时就需要特别处理 i0 的情况。更常见的问题是把下标从 1 用到 n数组却只开到 100当 in 时访问 a[n1][j]就越界到数组末尾之外的内存行为未定义有时候不报错有时候输出奇怪的数据。最稳妥的做法是数组在题目上限基础上多开几个单位写成 a[105][105] 或更大。多开一点没有成本却能在很大程度上避免边界访问踩雷。这是竞赛中一直强调的“数组多开 5 到 10”习惯的来源。4.4 输出格式的最后一道坎OJ 判题通常忽略行尾空格和额外换行但也存在严格模式。与其赌平台宽容度不如在输出时用 if (j 1) cout ; 控制。这样每一行行尾干干净净全平台通用。如果习惯用 scanf/printf 输入输出注意格式串对应好 int别把二维数组的遍历顺序写反。5. WA 之后怎么排查常见问题速查表5.1 一个典型的“看不见的越界”案例之前带一个学生他样例测试全过但提交到 OpenJudge 就是 WA。我让他把 n 改成 100、m 改成 100构造一组全 0 数据结果输出到第 50 行左右出现了奇怪的数字。他自己也懵说逻辑没问题数据怎么会变。后来打印中间变量发现他在循环中判断边界时把 i n 写成了 i n。这看起来是个很低级的错误但确实会真实发生。赋值表达式把 i 强行改成 n整个遍历直接乱套而且编译器不一定报错运行时也不一定崩溃。这提醒我们当“逻辑没问题”但结果不对时先把所有判断条件重读一遍重点看有没有少写一个等号。5.2 常见问题速查表下面这张表覆盖了新手最常见的几类 WA 和 RE建议收藏备用现象可能原因排查方法普通样例能过极限数据崩溃数组开小或越界数组开 105×105循环范围 1..n、1..m结果大面积偏小 1平均值用了整数除法没四舍五入用 (sum2)/5 或浮点 0.5结果越到后面越离谱原地更新新值污染了原始数据结果写进 ba 保持只读边框像素发生变化边界判断漏写了 in 或 jm打印 i、j对照边界条件程序报 RE在 in 时访问了 a[i1][j]内部点判断改成 i2 in-1输出全在同一行忘记在每行结束输出换行内层循环结束后 cout endl5.3 一个高效的调试技巧当你觉得代码哪里不对但找不到时把 n、m 调成 3 或 4用一组自己手算过的数据在代码里加 cout 输出每个内部点的 sum、商、余数跟手算结果逐项对照。很多时候错误会在第一个内部点就暴露根本不用看完整个矩阵。如果收到的是 WA 而不是 RE不要上来就怀疑 OJ 数据有问题。先把极端数据测试、边界条件、输出格式这三件事检查一遍这三个环节是藏雷最多的地方。我用这个顺序帮别人排查过很多次八九不离十。6. 这题背后从均值模糊到真实图像处理6.1 一个更形象的坐标模型把整张图想象成一张网格纸每个格子是一个像素。模糊处理的目标是让“突变”的像素被周围像素“中和”整张图看起来更柔和。题目使用的十字邻域是对“模糊”的一种简化模拟实际图像处理软件里更常见的是方块邻域比如取 3×3 九宫格的全部平均值。九宫格均值是很多边缘检测和降噪算法的基础操作而这个“取平均值”的过程在图像处理术语里叫卷积。你以后如果接触 OpenCV一句 cv::blur(src, dst, cv::Size(3, 3)) 就能完成 3×3 窗口的均值模糊底层思路和这道题一脉相承差别只在于邻域形状和边界处理策略。6.2 真实世界的边界策略这道题规定外圈像素保持不变是为了方便竞赛出题而简化出来的规则。真实图像模糊时边界像素通常也要参与计算问题是边界以外的“虚拟像素”从哪里来。工程上有几种常见策略在图像外补一圈 0复制边缘像素或者做镜像填充。不同策略会带来不同的边缘效果。你现在不需要深入掌握这些细节但可以带着这个认知。未来在真实项目中用图像处理库时会看到很多函数带 borderType 参数它就是用来选择边界策略的。理解了这个小点再看那些参数会容易很多。6.3 性能优化思路以后还会遇到n、m 最大只有 100逐个点求五邻域平均完全没压力。但如果图像变成 1920×1080 甚至更大每个点都重复访问邻居计算量会随邻域大小线性增长。竞赛里后续会学到二维前缀和预处理一张积分图之后任意矩形区域的和都能在 O(1) 时间得到均值模糊也就变成了几次加减法。这道题正好是引导你去思考“直接计算 vs 预处理加查询”的跳板。很多同学在学前缀和时觉得抽象如果你能先把均值模糊这个场景记在心里后面理解“积分图”会顺畅很多。说实话这题本身不难但它像一面放大镜把二维数组学习里最容易被忽视的几类错误照得清清楚楚。我后来带学弟学妹时会把“能不能原地更新”当成一个通用问题反复强调因为它不仅出现在图像模糊处理里后面学前缀和、学图论算法时同样要判断新状态能否直接覆盖旧状态。最后再分享一个不起眼但很实用的小习惯碰到矩阵、图像、棋盘这类二维模型先在草稿纸上画一个 3×3 的小例子手动把流程走一遍再开始敲代码。很多人省掉了这一步结果写完要用几倍的时间去调试。希望这篇内容能帮你在 OJ 上顺利拿到 AC。