试题背景
顿顿在学习了数字图像处理后,想要对手上的一副灰度图像进行降噪处理。不过该图像仅在较暗区域有很多噪点,如果贸然对全图进行降噪,会在抹去噪点的同时也模糊了原有图像。因此顿顿打算先使用邻域均值来判断一个像素是否处于较暗区域,然后仅对处于较暗区域的像素进行降噪处理。
问题描述
待处理的灰度图像长宽皆为 n 个像素,可以表示为一个 n×n 大小的矩阵 A,其中每个元素是一个 [0,L) 范围内的整数,表示对应位置像素的灰度值。
对于矩阵中任意一个元素 Aij(0≤i,j<n),其邻域定义为附近若干元素的集和:
Neighbor(i,j,r)={Axy|0≤x,y<n and |x−i|≤r and |y−j|≤r}
这里使用了一个额外的参数 r 来指明 Aij 附近元素的具体范围。根据定义,易知 Neighbor(i,j,r) 最多有 (2r+1)2 个元素。
如果元素 Aij 邻域中所有元素的平均值小于或等于一个给定的阈值 t,我们就认为该元素对应位置的像素处于较暗区域。
下图给出了两个例子,左侧图像的较暗区域在右侧图像中展示为黑色,其余区域展示为白色。
现给定邻域参数 r 和阈值 t,试统计输入灰度图像中有多少像素处于较暗区域。
输入格式
输入共 n+1 行。
输入的第一行包含四个用空格分隔的正整数 n、L、r 和 t,含义如前文所述。
第二到第 n+1 行输入矩阵 A。
第 i+2(0≤i<n)行包含用空格分隔的 n 个整数,依次为 Ai0,Ai1,⋯,Ai(n−1)。
输出格式
输出一个整数,表示输入灰度图像中处于较暗区域的像素总数。
解题思路:
拿到这道题,我们首先梳理一下,题目让我们做什么。简单的想,我们要先遍历矩阵,然后对每一个元素,先确定其邻域的范围(包括这个元素本身),然后再遍历这个邻域,对里面的所有元素求和,再求平均值,最后将平均值与阈值比较,若小于等于阈值,就将处于较暗区域的像素总数加1。
我们不妨先按这个思路写一下。果然,只得了70分,显示时间超限。这个方法比较暴力,没有经过优化,时间超限也在我的意料之中。
70分的代码:
# include <iostream>
# include <algorithm>
using namespace std;
const int maxn = 1000;
int n, L, r, t;
int A[maxn][maxn];
int ans = 0; //结果
int main()
{
cin >> n >> L >> r >> t;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
scanf("%d", &A[i][j]);
}
}
int a, x1, x2, y1, y2;
//为避免比较时的误差,sum,aver,len要定义为浮点型
float aver, sum, len;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
a = A[i][j];
//x1,x2,y1,y2用于确定邻域范围
x1 = max(i - r, 0);
x2 = min(i + r, n - 1);
y1 = max(j - r, 0);
y2 = min(j + r, n - 1);
sum = 0;
len = 0;
//求出邻域内所有元素的平均值
for (int k = x1; k <= x2; k++) {
for (int l = y1; l <= y2; l++) {
sum = sum + A[k][l];
len++;
}
}
aver = sum / len;
if (aver <= t) {
ans++;
}
}
}
cout << ans << endl;
return 0;
}
现在我们开始考虑改进算法。对现有的代码,遍历矩阵这一步显然无法优化,我们自然将目光放到“对邻域的遍历求和”这一步上。邻域内有多少元素是可以直接算出来的,这一步如果可以不用循环而直接求出邻域内所有元素的和,那么就能大大节省时间。问题是能做到吗?答案是当然能。
这里用到了差分和前缀和的思想。我们先创建一个差分矩阵B,在最初输入矩阵时,对输入的每一值Aij ,我们预先进行如下处理:确定Aij 的邻域范围,对于范围内的每一个值,在以后“邻域求和”时,都要加上Aij (你在我的邻域内,我当然也在你的邻域内),所以我们预先将该领域的所有元素都加上Aij 。这里,我们使用差分的思想,对于邻域的每一行,我们将第一个元素加上Aij ,最后一个元素的后一个元素减去Aij 。输入结束后,我们再将差分矩阵B还原(求前缀和),这时候矩阵B中的每一个值就是该位置原来的元素邻域求和的结果。之后,再按原先的方法进行处理即可。
100分的代码:
# include <iostream>
# include <algorithm>
using namespace std;
const int maxn = 1000;
int n, L, r, t;
int A[maxn][maxn];
float B[maxn][maxn] = { {0} }; //差分矩阵B
int ans = 0; //结果
int main()
{
cin >> n >> L >> r >> t;
int a, x1, x2, y1, y2;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
scanf("%d", &A[i][j]);
//x1,x2,y1,y2用于确定邻域范围
x1 = max(i - r, 0);
x2 = min(i + r, n - 1);
y1 = max(j - r, 0);
y2 = min(j + r, n - 1);
//差分
for (int k = y1; k <= y2; k++) {
B[x1][k] += A[i][j];
B[x2 + 1][k] -= A[i][j];
}
}
}
//还原(前缀和)
for (int i = 0; i < n; i++) {
for (int j = 1; j < n; j++) {
B[j][i] = B[j][i] + B[j - 1][i];
}
}
float aver, len;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
a = A[i][j];
//x1,x2,y1,y2用于确定邻域范围
x1 = max(i - r, 0);
x2 = min(i + r, n - 1);
y1 = max(j - r, 0);
y2 = min(j + r, n - 1);
//直接算出邻域范围
len = (x2 - x1 + 1) * (y2 - y1 + 1);
//求出邻域内所有元素的平均值
aver = B[i][j] / len;
if (aver <= t) {
ans++;
}
}
}
cout << ans << endl;
return 0;
}
总结:
这里以解题为主,差分和前缀和的思想,我就不做解释了(不知道的小伙伴可以去查一下相关的文章,有很多讲得都很细致)。CSP的第二题,一般都会有比较暴力直接的方法,但大多只能得70分;想要拿100分,就建议大家学习一下差分、DFS剪枝、动态规划等的算法思想,对满分通过第2题很有帮助。以上便是我对这道题的看法,很高兴与大家分享。