在 O(n) 时间内找到 n x n 矩阵中的局部最小值

Posted

技术标签:

【中文标题】在 O(n) 时间内找到 n x n 矩阵中的局部最小值【英文标题】:Find local minimum in n x n matrix in O(n) time 【发布时间】:2013-09-02 17:27:01 【问题描述】:

所以,这不是我的家庭作业问题,而是取自 coursera 算法和数据结构课程的未评分作业(现已完成)。

给你一个由不同数字组成的 n × n 网格。如果一个数字小于其所有邻居,则该数字是局部最小值。 (数字的邻居是紧邻上方、下方、左侧或右侧的一个。大多数数字有四个邻居;侧面的数字有三个;四个角有两个。)使用分而治之的算法设计范式来计算局部最小值,仅在数字对之间进行 O(n) 次比较。 (注意:由于输入中有 n2 个数字,因此您无法查看所有数字。提示:考虑一下哪种类型的重复可以为您提供所需的上限。)

由于数字没有任何顺序,除了 O(n2) 次比较之外,我看不出我们怎么能逃脱惩罚。

【问题讨论】:

O(log(n))O(n) 中进行操作? 有 n^2 个元素,所以 O(n) 会很棒。当然,如果可能的话,O(log(n)) 会很棒 另外,我们需要注意n 是什么。最初您说n 是行数和列数,这意味着有n^2 数字。每个数字大约有 4 个邻居,所以大约有 4*n^2/2 对,是吗?如果我们应该在O(m) 中这样做,那么m 的含义是对的数量,那么这就是O(n^2)。此外,我们只是在计算单个局部最小值——而不是最小元素? 是的,4*n^2/2 中可能有一些重复计算,但我认为它仍然是 O(n^2)。是的,我们只需要一个局部最小值,而不是全局最小值。 我已编辑此问题的标题以匹配其内容。这里根本没有关于“log n”的内容。这个问题其实很清楚。不过,我还不知道答案。 【参考方案1】:

我认为这实际上很容易。

将问题转化为 3-D 问题,看看算法为何有效。将矩阵放在桌子上。假设每个单元都有柱子伸出,柱子的高度与其值成正比。将球放在任何柱子上。让球始终落在最低高度的相邻柱子上,直到达到局部最小值。

【讨论】:

这给出了正确的答案,但它太慢了。球可能会滚下第一列,向右两步,向上第三列,向右两步,向下第五列……一直到它落在对面的角落。 (很容易构建一个发生这种情况的示例。)它触及了一半的正方形,并且比较了大约四倍的数量,即 O(n^2),而不是 O(n)。 听起来不错,但它是 O(log(N)) 吗?可能,但值得在我认为的答案中添加复杂性分析。 我相信这个解决方案是O(n^2)。无论你从哪里开始,你的克星都可以设计一个有围墙的迷宫,带你穿过一半左右的牢房。 @bcorso:很公平,但在这种情况下,实际上存在最坏情况的 O(n) 算法。看我的回答。 (此外,当这样的问题说 O(n) 算法时,我相信暗示了“最坏情况”。如果他们在谈论平均值,他们通常会这么说。)【参考方案2】:

我们可以通过查看 Jared 的回答如何出错来调整 Words Like Jared 的回答。

这个答案中的想法——这是一个很好的答案——是“滚下山”。这只是意味着,如果您在一个元素上,请检查它是否是局部最小值。如果是这样,你就完成了;否则,步到其最近邻居中的最小者。最终这必须终止,因为每一步都是针对较小的元素,而这不能在有限数组中永远持续下去。

这种方法的问题在于“滚动”可能会到处乱窜:

20 100 12  11 10 100  2
19 100 13 100  9 100  3
18 100 14 100  8 100  4
17  16 15 100  7   6  5

如果你从左上角开始“下坡”,你将访问数组中大约一半的元素。这太多了,所以我们必须稍微限制一下。

首先检查中间列和中间行。找到所有这些中最小的元素并从那里开始。

从那里滚动“下坡”一步,进入四个象限之一。您将进入其中一个象限,因为中间列和/或行中的相邻元素较大,因此两个相邻象限中只有一个可能是“下坡”。

现在考虑如果你从那里“滚下山”会发生什么。显然,您最终会达到局部最小值。 (我们实际上不会这样做,因为它会花费太长时间。)但是,在滚动的过程中,你永远不会离开那个象限......因为这样做,你必须越过无论是中间列还是中间行,并且这些元素都不小于您开始的位置。因此,该象限在某处包含局部最小值。

因此,在线性时间内,我们已经确定了一个必须包含局部最小值的象限,并且我们将 n 减半。现在只需递归。

这个算法需要时间 2n + 2n/2 + 2n/4 + ...,等于 4n,也就是 O(n) 所以我们完成了。

有趣的是,我们根本没有使用“rolling downhill”,除了关键部分:证明算法有效。

[更新]

作为Incassator points out,这个答案并不完全正确,因为在你“只是递归”之后,你可能会再次滚出象限......

最简单的解决方法是在“下坡”之前找到中间行、中间列、和边界中最小的元素。

【讨论】:

我们正在寻找 a 局部最小值而不是 最小值。 为什么考虑边界会改变什么?当我们递归发现最小值在子矩阵的边界上时,我们仍然不知道它是否是整个矩阵的局部最小值,因为我们在递归时没有它下面的元素。【参考方案3】:

Nemo 接受的答案很好,但并不完全正确:

因此,在线性时间内,我们已经确定了一个必须包含局部最小值的象限,并且我们将 n 减半。现在只需递归。

我指的是“只是递归”位。问题是我们不能直接这样做,因为在下一次迭代中我们可能会找到一个局部最小值不是原始网格的局部最小值(下面的 x 表示一些任意大的数字):

 x  x 39  x  x 50  x  x  x  x  x
 x  x 38  x  x 49  x  x  x  x  x
37 36 33 34 35 48  x  x  x  x  x
 x  x 32  x  1 10  x  x  x  x  x
 x  x 31  x  x 47  x  x  x  x  x
46 45 30 44 43 60 51 52 53 54 55
 x  x  2  x  x 56  x  x  x  x  x
 x  x  x  x  x 57  x  x  x  x  x
 x  x  x  x  x 58  x  x  x  x  x
 x  x  x  x  x 59  x  x  x  x  x

在第一次迭代中,我们发现 10 是中间行和中间列的最小值。我们向左走(因为 1 小于 10)。所以我们的下一次迭代是在左上象限。但是现在中间行和列的最小值将是 31(如果象限的边界被认为是其中的一部分,则为 30)。然后,您将得出结论,这是一个局部最小值。但它不适用于完整的网格。

我们可以通过多种方式纠正这个不幸的缺陷。我是这样解决的:

在每次迭代中,除了网格本身之外,我们还会跟踪当前的最小候选者(即第一次迭代后上例中的 1;在初始状态下,我们可以说最小候选者是加号无穷)。我们计算中间行和列的最小值并将其与最小候选者进行比较。如果后者更小,我们将递归到包含最小候选者的象限。否则我们会忘记之前的候选者,然后才检查新的中间行/列最小值是否实际上是局部最小值。如果不是,那么像往常一样递归到我们从它向下倾斜的任何象限(并跟踪新的最小候选)。

或者,您可以修改this presumably MIT lecture 中所述的过程:在每次迭代时,您可以查看中间行/列网格边界,而不是查看中间行/列。然后算法再次正确。

你选择你喜欢的方式。

【讨论】:

当矩阵中有 2 个局部最小值时,我们如何解决关系。对于例如 9,10 的矩阵,15,8 有两个局部最小值,即。 9(第一个元素 0,0)和 8(最后一个元素 (1,1)) @chebus 您需要找到 a 局部最小值。无论你碰巧找到哪一个。因此,如果在算法的迭代过程中您有多个最小元素,您可以继续使用其中的任何一个。 为什么考虑网格边界会改变什么?在您给出的示例中,如果边界在其每个空间中都有 100,那么当我们在左上象限递归时,我们仍然发现 30 是子矩阵的局部最小值,而实际上它不是(因为下面的 2 30) @BigBear 10 仍然是中间行/列和边界的最小值。所以下一个递归是第一象限的右下部分。【参考方案4】:

嗯,这就是你如何分而治之的。

1) 将 n x n 矩阵划分为四个 n/2 x n/2 子矩阵。

2) 继续递归地划分子矩阵,直到得到一个 2 x 2 矩阵

3) 检查 2 x 2 矩阵的任何元素是否是局部最小值。

递归方程为:T(n) = 4*T(n/2) + O(1)

4*T(n/2) 用于 4 n/2 x n/2 子矩阵,O(1) 用于检查 2 x 2 子矩阵是否具有局部最小值

主定理说这是一个 O(n^2) 最坏情况界限。

但我认为我们可以得到一个最佳情况 O(n) 界限,

("红色警报!---最好的情况是假的,只是假的---红色警报!")。

如果我们在步骤 3 中找到局部最小值后退出递归堆栈。

伪代码:

private void FindlocalMin(matrix,rowIndex,colIndex,width)
    if(width == 1) checkForLocalMinimum(matrix,rowIndex,colIndex); return; //2x2 matrix
    FindlocalMin(matrix,rowIndex,colIndex,width/2);  
    FindlocalMin(matrix, (rowIndex + (width/2) + 1) ,colIndex,width/2);
    FindlocalMin(matrix,rowIndex, (colIndex + (width/2) + 1) ,width/2);
    FindlocalMin(matrix,(rowIndex + (width/2) + 1), (colIndex + (width/2) + 1) ,width/2);


private void checkForLocalMinimum(.........)
    if(found Local Minimum in 2x2 matrix) exit recursion stack; 

这是java implementation

【讨论】:

【参考方案5】:

代码适用于 3*3 或更大的矩阵,根据算法第 4 版书籍练习的练习开发。

它的工作原理如下

    寻找给定矩阵的中间行并从中找出最小值 对于该最小值,请查找上方和下方元素以确保其为局部最小值 如果不限制搜索矩阵
      Row :选择比中间行的元素小的元素所在的行的一半。 colmun : 选择在该行中找到最小值的索引。
    重复步骤 1-3

公共类MinimumOfMatrix

private static int findLocalMinimum(int[][] matrix, int rowStart, int rowEnd, int colStart, int colEnd) 

    int midRow = (rowStart + rowEnd) / 2;
    int minPos = findMin(matrix, midRow, colStart, colEnd);

    if (minPos >= (colStart + colEnd) / 2)
        colStart = (colStart + colEnd) / 2;
    else
        colEnd = (colStart + colEnd) / 2;

    if (matrix[midRow][minPos] < matrix[midRow + 1][minPos]
            && matrix[midRow][minPos] < matrix[midRow - 1][minPos]) 
        return matrix[midRow][minPos];
     else if (matrix[midRow][minPos] > matrix[midRow + 1][minPos]) 
        return findLocalMinimum(matrix, midRow, rowEnd, colStart, colEnd);
     else 
        return findLocalMinimum(matrix, rowStart, midRow, colStart, colEnd);
    



private static int findMin(int[][] matrix, int midRow, int colStart, int colEnd) 
    int min = Integer.MAX_VALUE;
    int pos = -1;

    for (int i = colStart; i < colEnd; i++) 
        if (matrix[midRow][i] < min) 
            min = matrix[midRow][i];
            pos = i;
        

    

    return pos;


public static void main(String[] args) 
    // Best Case
    /*
     * int[][] matrix=  1,-2,4,-6,1,8, -3,-6,-8,8,1,3, 1,2,6,-2,-8,-6,
     * -2,9,6,3,0,9, 9,-1,-7,1,2,-6, -9,0,8,7,-6,9 ;
     */

    // Two Iteration Down Case
    /*
     * int[][] matrix=   1,-2, 4,-6, 1, 8, -3,-6,-8, 8, 1, 3,  1, 2, 6, 9, 0,
     * 6, -2, 9, 6,-1,-1, 9,  9,-1,-7, 1, 2,-6, -9, 0, 8, 7,-6, 9 ;
     */

    /*
     * //Left Down Case int[][] matrix=   1,-2, 4,-6, 0, 8, -3,-6,-8, 8,-2, 3,
     * -2, 9, 6,-1, 1, 9,  1, 0, 6, 9, 2, 6,  9,-1,-7, 1, 2,-6, -9, 0, 8,
     * 7,-6, 9 ;
     */

    int[][] matrix =   1, -2, 4, ,  -3, -6, -8, ,  -2, 9, 6, 

    ;

    System.out.println(findLocalMinimum(matrix, 0, matrix.length, 0, matrix.length));


【讨论】:

它的工作原理如下 1. 查找给定矩阵的中间行并从中找到最小值 2. 查找上下元素以确保它是局部最小值 3. 如果不限制搜索矩阵乘以 1。行:选择比中间行元素小的元素所在的行的一半。 2. colmun :选择在该行中找到最小值的索引。 4. 重复步骤 1-3 问题说矩阵由不同的整数组成,但我在你的例子中看到了重复。

以上是关于在 O(n) 时间内找到 n x n 矩阵中的局部最小值的主要内容,如果未能解决你的问题,请参考以下文章

在 O(n) 时间内找到数组中的 10 个最大整数

在 O(n) 时间内找到数组中的重复元素

我们如何在 O(n) 时间和 O(1) 空间复杂度内找到数组中的重复数字

有没有办法在少于 O(n) 的时间内找到集合中的最小元素?

2021-10-14:被围绕的区域。给你一个 m x n 的矩阵 board ,由若干字符 ‘X‘ 和 ‘O‘ ,找到所有被 ‘X‘ 围绕的区域,并将这些区域里所有的 ‘O‘ 用 ‘X‘ 填充。力扣1

给定一个双调数组和数组中的元素 x,在 2log(n) 时间内找到 x 的索引