CSP-邻域均值(差分与前缀和)

试题背景

顿顿在学习了数字图像处理后,想要对手上的一副灰度图像进行降噪处理。不过该图像仅在较暗区域有很多噪点,如果贸然对全图进行降噪,会在抹去噪点的同时也模糊了原有图像。因此顿顿打算先使用邻域均值来判断一个像素是否处于较暗区域,然后仅对处于较暗区域的像素进行降噪处理。

问题描述

待处理的灰度图像长宽皆为 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题很有帮助。以上便是我对这道题的看法,很高兴与大家分享。

猜你喜欢

转载自blog.csdn.net/CXR_XC/article/details/129647147