【问题标题】:Find all peaks in a matrix NxN查找矩阵 NxN 中的所有峰值
【发布时间】:2021-04-04 18:22:26
【问题描述】:

我正在尝试创建一个算法,使用简单的方法在 N x N 矩阵中找到 所有峰值。但我在角落、第一行和最后一行以及第一列和最后一列面临一些问题。我正在考虑如下问题:

[ ][c][ ][ ] a is considered a 2d peak
[d][a][e][ ] or a hill iff a >= b,
[ ][b][ ][ ] a >= d, a >=c, a >=e
[ ][ ][ ][ ]

但是,当我需要将角评估为下面的表示时,c 和 d 不存在。我必须评估太多条件才能得到更通用的东西。即,对于下面的a,如果row-1 < 0,我需要检查位置a是否有效,那么我不需要检查以上的任何内容a 并且如果 col-1

[a][e][ ][ ] 
[b][ ][ ][ ] 
[ ][ ][ ][ ] 
[ ][ ][ ][ ]

但是当我们把 a 带到其他角落甚至评估其他位置例如上面示例中的 e 时,我需要证明自己是否 e 位于 row-1 存在的有效位置,以防止评估该位置并得到错误(在上面的示例中,如果e >= the element in row-1,我肯定会得到错误检查。我'考虑到角落、第 0 行和第 n 行以及第 0 和第 n 列,我已经实现了一段代码,但我停下来开始思考如何使它更易读和更简单。

void find_hill(){
    int i, j;
    for(i = 0; i < n; i++){
        for(j = 0; j < n; j++){
            //check corners
            if(i-1 < 0){
                if(j-1 < 0){
                   //check if arr[i][j] >= arr at i,j+1 and i+1,j
                }
                if(j+1 > n-1){
                   //check if arr[i][j] >= arr at i,j-1 and i+1,j 
                }
            }                
        }
    }     
}

我想在这里讨论如何找到解决方案。我应该从哪里开始做一些简单的事情?我想以某种方式使用类似于洪水填充的方法,但它分别填充每个位置,可能它不会工作,因为我最终会遇到同样的问题!

【问题讨论】:

  • 你可以写一个函数get(i, j, n),如果ij有效则返回arr[i][j],否则返回INT_MIN
  • @MOehm 是的,但这如何解决我需要检查 i-1、j-1、i+1 和 j+1 的问题,而不需要对角和顶部进行太多表达式,不是它的邻居之一的底部和侧面?
  • 通过将条件检查移到函数中,您可以在客户端代码中无条件地探测所有四个方向。通过设置默认值INT_MIN,您可以确保峰值测试会在边缘和角落找到峰值。不过,您仍然必须明确检查所有四个邻居。
  • (我的意思是看here。这两个答案有相似的方法来避免区分角落和边缘情况。)
  • 很抱歉我没有正确解释INT_MIN。 (不过,这不是 int 的最小大小,而是它的最小可能值。这些和类似的常量在 &lt;limits.h&gt; 中定义。)我只能说“你知道的一个小值小于矩阵”。如果您的所有值都是正数,则该值可能为 0。

标签: c algorithm matrix search multidimensional-array


【解决方案1】:

我建议以下技巧:复制第一行和最后一行和列,如下所示:

[ ][a][b][c][ ]
[a][a][b][c][c]
[d][d][e][f][f]
[g][g][h][i][i]
[ ][g][h][i][ ]

然后让你的循环从 1 开始并在 n-1 结束。

【讨论】:

  • 这完全没问题,具体取决于您解决此问题的实际环境。与大多数艺术一样,编程是关于实现各种价值观之间的和谐。除非你只是为了它而挑战自己,如果你在实际环境中有千兆字节的 RAM 可用,你应该编写可读、可维护等的代码,而不是不浪费 CPU 周期或 RAM 字节的代码.后一种行为就是我们所说的过早优化。这不仅浪费你的努力,而且从长远来看实际上会适得其反。 @KennetEmerson
  • @KennetEmerson ...这被证明对性能至关重要,但这很可能会破坏此代码与其他代码交互的方式,并且您将产生多米诺骨牌效应。但这意味着您应该从一开始就为您正在计算的内容、正确的通信方式、技术、库等选择正确的抽象模型。如果您从一开始就知道某些计算将是一项繁重的工作,那么您应该从一开始就为它选择完美的算法并设计它的环境以服从它,而不是其他方式。当然。但是..
  • ...你应该避免各种你可以在未来很容易做到的优化,直到你确定它们是必要的。如果你知道如何正确地设计代码,你就会知道什么需要从一开始就决定,什么可以先快速编写,然后根据需要进行扩展。这是要点之一——虽然你的代码从一开始就应该是“干净的”,但尽量避免编写超出严格必要的代码。如果您需要“报废”以进行性能优化,那感觉就像删除占位符一样。你真的不会产生任何费用。
  • 但是为什么会适得其反呢?如果只是增益(可忽略且不可观察的更好性能)不值得付出成本(您花在思考、研究、写作、测试上的时间),那么我不会这么说。但是你也在创建一个更长、可读性更低的代码,更多的代码行可能会有一个愚蠢的错字,这些错字将通过编译、测试等等,但最终会成为一个难以解决的错误。 .. 在将来解决错误时需要维护、阅读和理解的更多代码等等等等等等。
  • 这需要经验,而不仅仅是像我在这里给你的讲座,才能真正理解它,但基本上是为了简化它,让我们说少即是多往往是正确的。只要您实现目标,代码越少越好。您的目标不是尽可能多地节省资源。你的目标是什么,当然是一个没有简单答案的问题,但它可能是金钱、知识、证明自己、革新某事等等。无论是什么,你可能根本无法通过将一件事优先于其他事情来实现它成本。
【解决方案2】:

在算法范围内,有几种选择。

您可以增加矩阵的大小。在理论上,这是一个完全可以接受的选择,因为它只会增加与矩阵宽度成比例的内存量和计算量。但实际上,如果您使用的是相对较小的矩阵,它可能会将它们的大小增加到 9 倍(如果 1x1 矩阵增加到 3x3)。如果你发现这种不优雅,你会发现很多东西都不优雅,避免这种不优雅的“解决方案”通常也比那些东西更不优雅和麻烦。

另一个选项显然是循环内的条件语句。它在理论上是完美的,因为它根本不会增加内存,只会为你已经在做的每个循环周期添加一个常数......实际上它确实做到了,它使每个循环都需要额外的时间来检查条件,所以无论你处理很多小矩阵,还是更少但更大的矩阵,你都让这段代码的性能变差了。如果该代码曾经或很可能成为许多性能问题的瓶颈或生成器,那么这实际上是最糟糕的选择。如果不太可能是这样,那么这是一个不错的选择,因为它以明确和简洁的方式显示了您的意图。因此,如果它是最好的选择取决于上下文,并且您可以通过使用或不使用此解决方案来引起其他程序员的愤怒,这取决于它是他们只想快速阅读的代码的一部分,还是分析的部分显示对应用程序性能低下负有最大责任。

最后,您可以为所有特殊情况编写单独的代码部分 - 分别检查四个角、四个边和矩阵的其余部分。这不会导致“不必要的”内存使用,并将导致最快的工作代码。不过,与其他两个选项相比,阅读起来会很糟糕。

也就是说,我不是在这里讨论这个故事的软件工程方面。您可以使用各种技术(如 fe)使这些选项中的每一个看起来更好、更具可读性和/或可维护性。在另一个答案选项中已经提到过创建一个 get(x,y) 函数来验证它的输入 - “循环内条件”场景的变体。

【讨论】:

  • 明白你的意思。我认为你是对的。考虑到您之前关于尝试通过过早优化解决问题会适得其反的评论,可能会导致我们对简单的解决方案视而不见,例如复制第一行和最后一行和列。也许在您说完之后,我可能会开始思考比针对进一步问题进行优化更简单的想法。感谢您的贡献。
【解决方案3】:

这类似于将内核应用于图像,例如 Sobel 过滤器或其他使用滑动窗口方法的过滤器。通常情况下,我会忽略边界,但是当您想检测矩阵边界上的山丘时,您需要识别窗口类型。

如果你不关心内存副本,你可以在嵌套的 for 内创建一个 3x3 的窗口,如果窗口在角落,则用中间的值填充相应的值。也许是这样的

int window[3][3];

for (int i = 0; i < n; ++i)
{
    for (int j = 0; j < n; ++j)
    {
        window[1][1] = arr[i][j];
        window[0][1] = (i - 1 < 0) ? window[1][1] : arr[i-1][j];
        window[2][1] = (i == n - 1) ? window[1][1] : arr[i+1][j];

        window[1][0] = (j - 1 < 0) ? window[1][1] : arr[i][j-1];
        window[1][2] = (j == n - 1) ? window[1][1] : arr[i][j+1];

        // Process window
        // ...
    }
}

【讨论】:

    【解决方案4】:
    void find_hill() {
    
        int i, j, k, l;
        
        for(i = 0; i < n; i++){
        
            for(j = 0; j < n; j++){
            
                for(k = i-1; k <= i+1; k++) {
                    
                    for(l = j-1; l <= j+1; l++) {
                        
                        if( k > 0 && k < n && l > 0 && l < n && (i-k)*(j-l) == 0 && arr[i][j] >= arr[k][l] ) {
                            
                            arr[i][j] is a peak;
                        }
                    }
                }                
            }
        }     
    }
    

    你也可能有这种情况

    if( !(k == i && j == l) && k > 0 && k < n && l > 0 && l < n && (i-k)*(j-l) == 0 && arr[i][j] > arr[k][l] )
    

    !(k == i &amp;&amp; j == l) 可以让您免于在 9 次中运行 1 次的其余条件,但增加了在 9 次中运行 9 次的权重。

    在这 9 次中有 1 次中,您还会执行条件内的代码,因此最好添加 !(k == i &amp;&amp; j == l)

    【讨论】:

    • 我认为它增加了算法的时间复杂度,我愿意至少在O(n^2) 上继续使用。
    • @KennetEmerson 你认为这个复杂度是多少?
    • @KennetEmerson 我不是针对你的。我只是发现用源代码回答这类特定问题的人应该考虑它如何影响这个网站的动态。这个问题很好,但是答案应该更具理论性。如果有人正在寻找工作源代码来解决他们的问题,他们应该会发现堆栈溢出对他们不起作用,否则只会有越来越多的人不来这里学习而是卸载。
    • @KennetEmerson kl 的循环总是运行 3 次,无论 n 的大小如何
    • @Davide - 有一些实际学习的空间。你看,如果有人想要并且知道如何正确地自己学习,他们可以从你的源代码中学习。如果他们不这样做,他们会觉得他们学到了一些东西,但他们没有。如果你表达了这个概念,他们仍然需要真正理解它并将其翻译成代码。无论如何,如果问题中唯一要做的就是提供工作源代码,您应该报告锁定或删除问题,而不是回答它。
    猜你喜欢
    • 2022-12-12
    • 1970-01-01
    • 1970-01-01
    • 2016-10-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多