如何高效地找到特定大小的开放矩形?

16

背景

我有一个矩形区域分成许多小正方形(这是我正在制作的游戏)。我在代码中将其表示为简单的二维boolean数组:

  ┌──┬──┬──┬──┬──┐
  │  │  │  │  │  │ X = X-value, also increasing width
  ├──┼──┼──┼──┼──┤ Y = Y-value, also increasing length
  │  │  │  │  │  │
  ├──┼──┼──┼──┼──┤
  │  │  │  │  │  │
  ├──┼──┼──┼──┼──┤
  │  │  │  │  │  │
  ├──┼──┼──┼──┼──┤
^ │  │  │  │  │  │
Y └──┴──┴──┴──┴──┘
   X >

有些方块会被建筑物等长方形占据,例如这个输入 (**代表已占据的方块):

┌──┬──┬──┬──┬──┐
│**│**│  │  │  │ 3 taken rectangles
├──┼──┼──┼──┼──┤
│**│**│**│**│  │
├──┼──┼──┼──┼──┤
│  │  │**│**│  │
├──┼──┼──┼──┼──┤
│  │  │**│**│  │
├──┼──┼──┼──┼──┤
│**│  │**│**│  │
└──┴──┴──┴──┴──┘
在2D的布尔数组中,“taken”方块被设置为true,而“open, non-taken”方块被设置为false。
我的问题
我需要找到所有指定大小的“open”矩形(未取走)。这是因为我需要找到放置下一个建筑物的所有可能空间。例如,在前面的图中,如果我想获取所有“open”的1x2矩形,我应该得到以下输出:
┌──┬──┬──┬──┬──┐
│**│**│1 │12│2 │ 3 taken rectangles (input)
├──┼──┼──┼──┼──┤ 4 open 1x2 rectangles (output)
│**│**│**│**│  │ Open rectangles are numbered
├──┼──┼──┼──┼──┤ 
│3 │3 │**│**│  │ Rectangles can overlap.
├──┼──┼──┼──┼──┤ The '12' square represents the overlapping
│4 │4 │**│**│  │ of rectangle 1 and 2.
├──┼──┼──┼──┼──┤ 
│**│  │**│**│  │ (Haha, my diagram kind of looks like old Minesweeper)
└──┴──┴──┴──┴──┘

我的工作经验

下面是我所做过的测试(暴力搜索,代码使用C#和少量伪代码):

List<Rectangle> findOpenSpaces(boolean[][] area, int width, int length) {
    List<Rectangle> openSpaces = new List<Rectangle>();
    boolean open = true;
    // Loop through all rectangles with size "width" and "length"
    for x = 0 to (total area length) - length {
        for y = 0 to (total area width) - width {
            // Check this rectangle to see if any squares are taken
            Rectangle r = new Rectangle(x, y, width, length);
            if checkRectangle(area, r) returns true {
                // rectangle was fully open, add to the list
                openSpaces.Add(r);
            }
        }
    }
    return openSpaces;
}

boolean checkRectangleIsOpen(boolean[][] area, Rectangle r) {
    for i = r.x to r.width {
        for j = r.y to r.length {
            if area[i][j] is true {
                // means that this square in the rectangle is taken,
                // thus the rectangle is not fully open
                // so return false (not open)
                return false;
            }
        }
    }
    return true; // no square in the rectangle was taken
}

struct Rectangle { // just a struct to help store values
    int x, y, width, length;
}

问题

上述(伪)代码能够工作,但是如果你看它的话,它的时间复杂度会达到O(n^4) :( (我想是因为有四个嵌套的for循环,但我不是专家)。另外,在游戏中,矩形区域的总大小为50x50,而不是像我这里举的例子那样是5x5。每当我执行此操作时,游戏都会明显地出现卡顿。

我在谷歌上搜过这个问题,但我不确定应该如何称呼它。如果有人能为我展示一个有效的算法来完成这个任务,我将非常感激。谢谢 :)


2
也许值得考虑其他数据结构。例如,如果放置在地图上的对象不太多,您可以将它们放在列表中,这样当您放置新对象时,只需对列表中的每个对象执行某种碰撞检测即可。或者您可以将所有占用的坐标放入列表中。 - TobiMcNamobi
1
显然,强度降低是最好的选择,但如果总体积受限,那么您可能可以通过一些并行性使事情变得足够快。例如,用位掩码表示棋盘将允许您使用单个操作在两个相邻行的所有列之间执行AND、OR、XOR等操作。"位棋盘"通常在诸如国际象棋和黑白棋等游戏中用于快速计算合法移动等。 - Adrian McCarthy
代码审查可能更适合这个问题:codereview.stackexchange.com :) - flotothemoon
@1337哦,我还没听说过代码审查,我以为这更像是一个算法问题,所以我发布在这里 :) - user837703
有趣的是,我因为在评论中建议而不是投票支持它而在审核中失败了。 - flotothemoon
3个回答

7

一些快速的思考会表明,你不能比 O(NM) 更快地完成此操作,因为至少有这么多可能的输出结果。您已经有一个 O(N^2M^2) 的解决方案,所以让我们看看是否能找到一个 O(NM) 的解决方案。

我所做的是,不是找到可以形成大小为 a x b 的矩形的位置,而是找到不能形成该矩形的位置。

也就是说,像这样(伪代码):

for each taken square:
    for each rectangle starting position:
        if rectangle at starting position includes this taken square,
            mark starting position as non-tenable

如果您的矩形很小(就像上面的1x2示例),那么这种(高效实现的)方法是完全足够的。然而,如果要采取不依赖于矩形大小的方法来获得渐近速度. . .
考虑到取点(x,y),则与该点重叠的矩形本身形成一个矩形,并且该范围内的任何内容都无法使用。因此,我们想将(离散的)范围标记为不可用。这可以通过使用 2-D Fenwick树完成,每次更新或查询的成本为O(log N log M)。要将范围(x1, y1, x2, y2)标记为“已使用”,我们需要:
  1. (x1,y1)增加一
  2. (x1,y2)减少一
  3. (x2,y1)减少一
  4. (x2,y2)增加一
因此,对于每个使用的正方形,我们最终会执行4个2D Fenwick树操作,其中正方形最多为O(NM)。然后,当我们完成时,我们检查每个可能的起始位置的值:如果为零,则为有效的起始位置。如果不是零,则无法使用。
总成本:O(NM log N log M)。虽然您可以以更快的速度执行此操作,但该方法的实现速度/大小比非常好(2-D Fenwick树大约需要10行即可实现)。

1
无法比O(N^2)更快地完成此操作,因为至少有这么多的输出。您的意思是说您不能比O(mn)更快地完成此操作,其中mn分别是网格的高度和宽度吗? - Jim Mischel
1
他假定m=n。提示:通过巧妙选择示例(例如:4x5棋盘),您可以传达关于任务的隐含知识,并且这可以避免这种错误的假设。 - Karoly Horvath
我有点假设N是两个维度中的最大值。这是我的坏习惯,我会修复它的!谢谢 :) - kestrel
使用这个Fenwick树如何有效地回答实现findOpenSpaces的原始问题呢? - Sentient

1
这里有一个使用动态规划和集合论来减少时间复杂度的解决方案:-
1. Evaluate Free[i][j] which counts all free tiles in rectangle (i,j) to (n,n).
2. Rect[i][j] is whether there is rectangle (h,w) starting at (i,j).
3. Count[i][j] = Free[i][j] - Free[i][j+w] - Free[i+h][j] + Free[i+h][j+w]
4. Rect[i][j] = (Count[i][j] == h*w)

时间复杂度 :-

Free[i][j]可以再次使用DP进行评估,方法如下:

Free[i][j] = Row[i][j]+Col[i][j]+Free[i+1][j+1],其中Row[i][j]是从j开始计算第i行中空闲瓷砖的数量,而Col[i][j]是从i开始计算第j列中的数量。 ColRow数组都可以使用DPO(NM)内评估。因此,在预处理Col & Row之后,您可以在O(NM)内评估Free[i][j]

在预处理Free[i][j]之后,Rect[i][j]可以在O(NM)内评估。

总复杂度 : O(NM)


1
我对数学不是很了解,但每当我看到这种一个点里有多个数据点的挑战时...从程序角度来看,我很想尝试一下一个小对象。
如果你编写一个带有邻居对象引用的类作为盒子,而不仅仅是数组或矩阵会怎样呢?
在游戏启动时,您需要使用所有交叉引用构建行的繁忙过程来建立邻居盒子。
但在玩耍时,您只需一次更新一个盒子+(邻居*定义的盒子大小)。
如果定义的盒子大小超过2-或者您关心多个尺寸-可能仍然需要大量重复,但比所有盒子的嵌套循环少得多。
当您想要在整个网格中找到“空白”时,您只需要在每个盒子上调用一次函数,因为答案已经作为盒子对象状态提前维护好了。

这对于维护每个网格位置的“使用或未使用”状态是很好的,但不适用于查找可用位置的连续块。 - Ben Voigt
意图是存储自身和每个邻居的知识,直到定义块的深度。 - Mike M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接