【问题标题】:Efficiently finding the largest surrounding square in 2D grid有效地找到二维网格中最大的周围正方形
【发布时间】:2015-05-17 21:19:44
【问题描述】:

在使用 2D 网格地图的游戏中,我面临以下情况:

我需要找到围绕玩家位置(红点)的最大边界方块。或者至少一个最大尺寸的边界正方形,因为可能有更多。注意:方形,不是矩形。

在这种情况下,很容易看出最大的正方形是 8x8:

如果我在地图中添加障碍物,现在最大的可能性是 5x5:

我正在寻找一种快速有效的方法来找到包含玩家位置的(或一个)最大方格。

我目前正在做的是一种蛮力:

  • 1x1 方格始终是可能的(玩家位置本身)。
  • 然后我尝试所有可能包含玩家的 2*2 方格,即 4 个可能的不同方格,并且我对每个方格进行 2*2 循环,检查所有网格单元格是否清晰(不是墙壁或障碍物)。
  • 如果 2*2 方格是可能的,那么我会尝试围绕玩家的所有可能的 3*3 方格(即 9 个不同的方格),并为每个方格进行 3*3 循环,以检查是否没有碰撞。李>
  • 等等,直到尺寸为 N*N 为止,不可能有正方形。

有效,很直接,但是感觉效率很低。显然我在这里做了很多冗余检查,我想知道是否有更聪明/更快的方法来做到这一点。有谁知道有效地做到这一点的算法吗?

(编辑)重要的补充:除了玩家四处移动之外,障碍物或墙壁是动态添加或移动的,因此缓存或预计算优化在这里有点难以实现。

【问题讨论】:

  • 有很多事情可以做,但是根据您的网格的最大大小,大多数优化可能由于初始化开销大而效率低下。是否有预定义的最大尺寸?
  • @Amit 好吧,网格在水平和垂直方向上可能有数百个单位,但我在实践中注意到最大可能的正方形很少大于 10,也永远不会大于 20。我d 说我得到的最常见的尺寸(对于最大可能的正方形)通常是 3-6。
  • @Amit 因为您提到了“初始化”,所以我添加了一条关于地图有些动态的评论。我实际上想过预先计算各种边界框信息,但由于地图不固定,这在此处实际上不起作用。
  • @你能发布一些代码来展示矩阵的存储方式吗?
  • @גלעדברקן 它只是一个具有固定宽度和高度的 int map[](我使用的是 C++,但希望保持这个主题语言独立)。有不同种类的障碍物和方块,但基本上 0 = 空,非零 = 墙或障碍物。

标签: algorithm optimization raster bounding-box


【解决方案1】:

我认为您可以通过在每个阶段仅检查现有“最大正方形”的有效边界来改进您的算法。用图表来解释这一点可能更容易......但基本上。你应该做的就是

 **Growth Algorithm**
 repeat
   search the bounding sub-squares on the valid sides of the largest valid square found so far
   increase size of square on 'valid' sides and mark newly blocked sides as invalid
 until all sides invalid

 then check if largest valid square can be translated 1 unit diagonally in any of 4 directions
 if so repeat the growth algorithm on each new square until none get bigger

这意味着您应该只需要测试最终有效方块的每个子方块一次。因此,如果正方形在其一侧是 n,则为 n^2 过程。 IO 不认为您可以做得更好,因为您确实需要检查每个子方格的有效性。

【讨论】:

  • 非常感谢您的思考和清晰的图表!实际上,我一直在考虑类似的“增长优势”方法。但问题是:我验证或阻止正方形边缘的顺序或优先级(从而决定我仍然可以扩展的方向)会产生影响。 [1/2]
  • 例如,如果您以我的第三张图像中的情况(有障碍物)为例,如果我展开所有边缘,我会首先找到一个被阻挡的边缘,然后是顶部(与您目前的过程相同)然后右边和下边被障碍物挡住,所以我最终得到一个 4*4 的正方形。而正确答案是 5*5(以蓝色表示,通过扩展 3* 左/上和 1* 右/上获得)。 [2/5]
  • @RocketNuts 是的,我现在看到了问题所在。我没有时间详细查看它,但是可能,当您最终在各个方向上都被阻塞时(这将发生在您的示例中的第三个图表之后,带有额外的正方形),您可以添加一个步骤,您可以在其中查看 '阻塞的最大正方形可以在 4 个方向中的任何一个方向上对角滑动(不会“丢失”您的起始点) - 如果是这样,您将在每个方向上重复该算法以查看它是否可以进一步扩展。这将适用于您的图表 3(以最低的额外成本),但我无法确认它是否在所有情况下都有效。
  • @RocketNuts 我用一种手动的方式更新了算法来描述它。
  • 我只想在这种方法中添加一件事:与其检查所有边缘,不如只检查每个循环中的两个边缘(上和左、上和右、下和右、下和左)。以这种方式工作将允许您检查每个周期仅增加 1 个单位的正方形,而如果您检查所有边,您可以得到一个大小为 n+2 而不是 n+1 的正方形。它在复杂性方面似乎更好,但需要更多的努力来实现(更多的有效性检查等)。使用增长 1 个单位的方格还可以进行更精细的检查。如果有不清楚的地方请告诉我。
【解决方案2】:

对此使用变体:Maximum size square sub-matrix with all 1s

算法:

Scan up, down, left and right only from the dot to generate the bounds for the 
solution matrix S, not more than 20 in each direction (that's my contribution... :)

Copy the first row and first column (within the bounds of S) from M[][] to S[][], switching 
zeros for ones and ones for zeros.

For the remaining entries (i=1 to length S, j=1 to length S) do:

If M[i][j] is 0 then
   S[i][j] = min(S[i][j-1], S[i-1][j], S[i-1][j-1]) + 1
   If S[i][j] is within distance of dot
      Best = max(S[i][j], Best)
Else
   S[i][j] = 0

【讨论】:

  • 有趣,是的,增长的正方形只能在四个对角线方向上发生。我首先想到的一个问题是,随着每一次进一步的迭代,会出现 4 个新的变化。如果我递归地这样做,数字会变得巨大。但是有很多重复:对角线步骤的不同顺序实际上会导致相同的正方形,所以也许有一种跳过重复的聪明方法。
  • 嗯,是的,增长发生在 4 个对角线方向中的任何一个,但最终结果是一个正方形,它简单地(从玩家位置)向上、向右、向下扩展特定数量的单位,和左方向。我只需要记住在 4 个直线方向上展开的单元数,以确定我已经拥有哪些可能性。我要多考虑一下!
【解决方案3】:

我认为一个视频价值一千张图片。 Algorithm demonstration.

在视频中你可以看到这个算法如何找到周围的正方形,不幸的是我没有画整个过程它只画了好的匹配,但我认为你会发现有趣,你会注意到整个过程只看好的匹配.

你可以自己运行视频的例子,处理3,我把整个代码放在this gist

最相关的代码(用python编写)是这个

def checkOutBoundaries(areaX1, areaY1, areaX2, areaY2):
    return areaX1 < 0 or areaY1 < 0 or areaX2 >= gridSize or areaY2 >= gridSize

def isAreaCompatible(type, oldAreaX1, oldAreaY1, oldAreaX2, oldAreaY2, areaX1, areaY1, areaX2, areaY2):
    global grid
    for y in range(areaY1, areaY2+1):
        for x in range(areaX1, areaX2+1):
            print "checking point (%s,%s) old area is (%s,%s) (%s,%s) new area is (%s,%s) (%s,%s)" % (x,y,oldAreaX1, oldAreaY1, oldAreaX2, oldAreaY2, areaX1,areaY1,areaX2,areaY2)    
            if x >= oldAreaX1 and x <= oldAreaX2 and y >= oldAreaY1 and y <= oldAreaY2: 
                print "This area belongs to previous area, won't check"
            else:         
                if grid[y][x].type != type: print "false"; print "This area have a different color/type so it's not compatible"; return False;
    return True;

def explore(type, x1, y1, x2, y2):
    #Right and bottom
    print "----- Right and bottom ------"
    areaX1 = x1;
    areaY1 = y1;
    areaX2 = x2+1;
    areaY2 = y2+1;
    if not checkOutBoundaries(areaX1, areaY1, areaX2, areaY2):
        if isAreaCompatible(type, x1, y1, x2, y2, areaX1, areaY1, areaX2, areaY2):      
            addAnim(areaX1, areaY1, areaX2, areaY2);
            addMatch(areaX1, areaY1, areaX2, areaY2);
            explore(type, areaX1, areaY1, areaX2, areaY2);

    #Bottom and left    
    print "----- Bottom and left ------"
    areaX1 = x1-1;
    areaY1 = y1;
    areaX2 = x2;
    areaY2 = y2+1;
    if not checkOutBoundaries(areaX1, areaY1, areaX2, areaY2): 
        if isAreaCompatible(type, x1, y1, x2, y2, areaX1, areaY1, areaX2, areaY2):
            addAnim(areaX1, areaY1, areaX2, areaY2);
            addMatch(areaX1, areaY1, areaX2, areaY2);
            explore(type, areaX1, areaY1, areaX2, areaY2);

    #Left and top
    print "----- Left and top ------"
    areaX1 = x1-1;
    areaY1 = y1-1;
    areaX2 = x2;
    areaY2 = y2;
    if not checkOutBoundaries(areaX1, areaY1, areaX2, areaY2):     
        if isAreaCompatible(type, x1, y1, x2, y2, areaX1, areaY1, areaX2, areaY2):
            addAnim(areaX1, areaY1, areaX2, areaY2);
            addMatch(areaX1, areaY1, areaX2, areaY2);
            explore(type, areaX1, areaY1, areaX2, areaY2);

    #Top and right
    print "----- Top and right ------"
    areaX1 = x1;
    areaY1 = y1-1;
    areaX2 = x2+1;
    areaY2 = y2;
    if not checkOutBoundaries(areaX1, areaY1, areaX2, areaY2):     
        if isAreaCompatible(type, x1, y1, x2, y2, areaX1, areaY1, areaX2, areaY2):
            addAnim(areaX1, areaY1, areaX2, areaY2);
            addMatch(areaX1, areaY1, areaX2, areaY2);
            explore(type, areaX1, areaY1, areaX2, areaY2);

【讨论】:

    【解决方案4】:

    这是一种完全不同的方法:

    您持有一个大小为 [Length, 2] 长度的二维矩阵,长度为地图的大小(我假设宽度/高度对称,但如果不只是选择一个轴)。 我将调用第一列(长度)列(您可以将整个算法旋转 90 度并调用此行)。 每列包含 2 个值 - 列中有效范围的最小位置和最大位置。

    您使用以下算法填充此矩阵:

    从点列开始,查找并存储点上方和下方连续范围的最小和最大位置。如果点在 x,y:

    int colMin, colMax;
    for(colMin = y; colMin > 0;) {
       if(Matrix[x, colMin - 1].IsValid)
           colMin--;
       else break;
    }
    for(colMax = y; colMax < maxHeight;) {
       if(Matrix[x, colMax + 1].IsValid)
           colMax++;
       else break;
    }
    MinMaxValid[x, 0] = colMin;
    MinMaxValid[x, 1] = colMax;
    

    您使用以下算法在两个方向上独立进行:

    int minCol = 0, maxCol = maxWidth; // These hold the min/max range of valid columns
    for(int col = x - 1; col >= 0; col--) {
       for(colMin = y; colMin > MinMaxValid[col + 1, 0];) { // Cell is only valid if it overlaps to the next range
          if(Matrix[col, colMin - 1].IsValid)
              colMin--;
          else break;
       }
       for(colMax = y; colMax < MinMaxValid[col + 1, 1];) { // Cell is only valid if it overlaps to the next range
          if(Matrix[col, colMax + 1].IsValid)
              colMax++;
          else break;
       }
       if((colMax - colMin) >= (x - col)) { // if the found range is smaller than the distance for x, it can't be a part of the square
          MinMaxValid[col, 0] = colMin;
          MinMaxValid[col, 1] = colMax;
       }
       else {
          minCol = col + 1; 
          break; // We're done on this side
       }
    }
    
    for(int col = x + 1; col < maxWidth; col++) {
       for(colMin = y; colMin > MinMaxValid[col - 1, 0];) { // Cell is only valid if it overlaps to the previous range
          if(Matrix[col, colMin - 1].IsValid)
              colMin--;
          else break;
       }
       for(colMax = y; colMax < MinMaxValid[col - 1, 1];) { // Cell is only valid if it overlaps to the previous range
          if(Matrix[col, colMax + 1].IsValid)
              colMax++;
          else break;
       }
       if((colMax - colMin) >= (col - x)) { // if the found range is smaller than the distance for x, it can't be a part of the square
          MinMaxValid[col, 0] = colMin;
          MinMaxValid[col, 1] = colMax;
       }
       else {
          maxCol = col - 1;
          break; // We're done on this side
       }
    }
    

    现在您的 MinMaxValid 已填写完毕。下一部分是遍历行并找到最大的正方形:

    int maxSquareSize = 0, maxSquareCol = minCol;
    for(int col = minCol; (MinMaxValid[col, 1] - MinMaxValid[col, 0]) >= (maxCol - col); col++) {
       for(int squareSize = MinMaxValid[col, 1] - MinMaxValid[col, 0] + 1; squareSize > maxSquareSize; squareSize--) {
          if((Min(MinMaxValid[col, 1], MinMaxValid[col + squareSize - 1, 1]) - Max(MinMaxValid[col, 0], MinMaxValid[col + squareSize - 1, 0])) >= (squareSize) {
             maxSquareSize = squareSize;
             maxSquareCol = col;
             break;
          }
       }
    }
    

    最后一部分是这样的:

    任何列都可以加入一个与其高度一样大的正方形。为此,当前列范围与col + squareSize - 1 处的范围的并集必须至少为 squareSize 高。

    现在最精彩的部分来了!每当场景发生变化(正方形变得无效、点移动等)时,您只能使 MinMaxValid 矩阵的某个范围无效并根据需要重新评估。我没有包含这个逻辑,因为这个答案足够长,但这是最简单的部分(基本上,你缩小列范围以适应新场景,然后再次重新扫描两个方向)。

    我必须说我没有尝试过这个,但即使它不起作用(请让我知道,以便我可以删除这条评论:-),它必须接近一个有效的解决方案。

    【讨论】:

    • 非常感谢 Amit 的广泛回答。我还没来得及完全掌握它,我将进一步详细分析它并告诉你!
    猜你喜欢
    • 2018-08-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多