如何在2D矩阵中找到孔?

时间:2013-06-21 08:10:46

标签: algorithm search flood-fill

我知道这个标题似乎有点含糊不清,因此我附上了一张有助于清楚理解问题的图片。我需要在白色区域内找到洞。一个洞被定义为一个或多个在白色区域内具有值“0”的单元格我的意思是它必须被单元格完全包围,值为“1”(例如,在这里我们可以看到三个标记为1,2和3的孔)。我想出了一个非常天真的解决方案: 1.在整个矩阵中搜索值为“0”的单元格 2.遇到此类单元格(黑色)时运行DFS(Flood-Fill)并检查是否可以触摸主矩形区域的边界 3.如果我们可以在DFS期间触及边界,那么它不是一个洞,如果我们无法到达边界那么它将被视为一个洞

现在,这个解决方案有效,但我想知道是否还有其他有效/快速的解决方案。

请让我知道你的想法。感谢。

enter image description here

5 个答案:

答案 0 :(得分:9)

您已经拥有的洪水填充:沿着矩阵的BORDER运行并填充它,即 将全零(黑色)更改为2(填充黑色),将1更改为3(填充白色);忽略2和3的那个 来自早期的洪水填充。

例如,对于矩阵,您从左上方开始,并使用区域填充黑色区域 11.然后你向右移动,找到你刚刚填满的黑色牢房。再向右移动,找到一个 白色区域,非常大(实际上矩阵中的所有白色)。洪水填充它。然后你向右移动 再一次,沿着整个上部和右部边界延伸的另一个新的黑色区域。到处走, 你现在找到两个你之前填充的白色单元格并跳过它们。最后你会在底部边界找到黑色区域。

然后扫描矩阵:您找到的仍为彩色0的所有区域都是黑色的洞。你可能还有白洞。

另一种方法,有点像“被逮捕的洪水填充”

在第一个矩阵的边界周围运行。如果找到“0”,则设置 到“2”。如果找到“1”,则设置为“3”。

现在绕着新的内边界(那些接触你刚刚扫过的边界的细胞)。 接触2的零细胞变为2,接触3的细胞变为3。

您必须扫描两次,顺时针一次,逆时针扫描一次,检查细胞“向外”和“之前”当前细胞。那是因为你可能会发现这样的事情:

22222222222333333
2AB11111111C
31

单元格A实际上是1.你检查它的邻居并找到1(但是由于你还没有处理它而无法检查那个,所以你不知道它是不是1或者应该是3 - 顺便说一下,2和2. A 2不能改变1,所以单元格A保持为1.同样的单元格B再次为1,依此类推。当你到达单元格C时,你发现它是一个1,并且有一个3邻居,所以它切换到3 ...但是从A到C的所有单元格现在应该切换

处理这个问题最简单但最有效的方法是顺时针扫描单元格,这会给你错误的答案(顺便提一下C和D是1)

22222222222333333
211111111DC333333
33

然后逆时针再次扫描它们。现在当你到达单元格C时,它有一个3邻居并切换到3.接下来你检查单元格D,其前一个邻居是C,现在是3,所以D再次切换到3。最后你会得到正确的答案

22222222222333333
23333333333333333
33

并且对于每个单元格,您检查了两个顺时针方向的邻居,一个逆时针方向。此外,其中一个邻居实际上是您之前检查过的单元格,因此您可以将其保存在就绪变量中并保存一个矩阵访问权限。

如果您发现在没有甚至一次切换单个单元格的情况下扫描了整个边框,则可以暂停该过程。检查这将花费你2(W * H)的操作,所以如果有批次的话,这真的是值得的。

在最多W * H * 2步骤中,你应该完成。

您可能还想检查渗透算法并尝试调整该算法。

答案 1 :(得分:2)

创建某种“LinkedCells”类,它将存储彼此链接的单元格。然后按照从左到右,从上到下的顺序逐个检查单元格,对每个单元格进行以下检查:如果它的相邻单元格为黑色 - 将此单元格添加到该单元格的组中。否则,您应该为此单元格创建新组。你应该只检查顶部和左边的邻居 UPD:抱歉,我忘记了合并群组:如果两个相邻的小区都是黑色且来自不同的群组 - 您应该将群组合并为一组。

如果“LinkedCells”类连接到边缘,则它应该有一个标志。默认情况下为false,如果向此组添加边缘单元格,则可以将其更改为true。如果要合并两个组,则应将新标志设置为先前标志的||。 最后,您将拥有一组组,每个组具有错误的连接标志将是“洞”。

该算法为O(x * y)。

答案 2 :(得分:1)

您可以将网格表示为图形,其中单个单元格作为顶点,并且边缘出现在相邻顶点之间。然后,您可以使用广度优先搜索或深度优先搜索从两侧的每个单元格开始。由于您只能找到连接到侧面的组件,因此未访问的黑色单元是孔。您可以再次使用搜索算法将孔分成不同的组件。

编辑:最坏情况复杂度必须与单元格数量成线性关系,否则,给算法输入一些信息,检查哪些单元格(因为你是次线性的,会有很大的未访问的点)算法没有考虑过并在那里打个洞。现在你有一个输入,算法没有找到其中一个漏洞。

答案 3 :(得分:1)

您的算法全局正常。这只是通过将洪水填充探测与细胞扫描相结合来优化它。这只会最小化测试。

一般的想法是在扫描桌子时逐行执行洪水填充勘探。因此,您需要跟踪多个并行洪水填充。

然后从上到下逐行处理该表,并从右到左处理每一行。订单是任意的,如果您愿意,可以反过来。

识别连续值为0的连续单元序列。您只需要值为0的第一个和最后一个单元格的索引来定义段。 正如您可能猜到的那样,一个细分也在进行中。因此,我们将在段中添加一个识别号,以区分不同的洪水填充。

此算法的优点在于您只需跟踪第i行和第i行中的段及其标识号。因此,当您处理第i行时,您将获得在行i-1中找到的段列表及其关联的标识号。

然后,您必须在第i行和第i-1行处理段连接。我将在下面解释如何提高效率。

现在你必须考虑三种情况:

  1. 发现第i行中的某个段未连接到第i-1行中的某个段。为其分配新的孔识别(递增的整数)。如果它已连接到表格的边框,请将此数字设为负数。

  2. 发现第i-1行中的段未连接到第i-1行中的段。你找到了洞的最低段。如果它有一个否定的识别号码,它将连接到边框,您可以忽略它。不然,恭喜你,你找到了一个洞。

  3. 发现第i行中的一个段连接到第i-1行中的一个或多个段。将所有这些连接段的标识号设置为最小标识号。请参阅以下可能的用例。

  4. row i-1:   2  333 444 111
    row i  :  ****  *** ***
    

    第i行中的段应该都获得标识相同洪水填充的值1。

    通过从左到右依次保持它们并比较段索引,可以有效地完成行i和行i-1中的段匹配。

    首先按最低起始索引处理细分。然后检查它是否连接到另一行的起始索引最低的段。如果否,则处理案例1或2.否则继续识别连接的段,跟踪最小的识别号。当找不到更多连接的段时,将第i行中找到的所有连接段的标识号设置为最小的标识值。

    连接测试的索引比较可以通过将(first-1,last)存储为段定义进行优化,因为段可以通过它们的角连接。然后,您可以直接比较索引裸值并检测重叠段。

    选择最小标识号的规则可确保您自动获取已连接段的负数,并且至少有一个连接到边界。它传播到其他部分和洪水填充。

    这是一个很好的编程练习。您没有指定所需的确切输出。所以这也留作练习。

答案 4 :(得分:0)

here所述的强力算法如下。

我们现在假设我们可以在单元格中写入不同于0或1的值。

您需要一个洪水填充函数,它接收要从中开始的单元格的坐标,以及一个整数值,以写入所有保持值为0的连接单元格。

由于您只需要考虑孔(值为0且由值为1的单元格包围的单元格),您必须使用两次传递。

第一次访问只访问接触边界的细胞。对于包含值0的每个单元格,使用值-1进行泛洪填充。这告诉您此单元格的值不同于1并且具有与边框的连接。在此扫描之后,具有值0的所有单元格属于一个或多个孔。

要区分不同的孔,需要进行第二次扫描。然后扫描尚未扫描的矩形(1,1)x(n-2,n-2)中的剩余单元格。每当您的扫描命中一个值为0的单元格时,您就会发现一个新的洞。然后用你选择的整数填充这个洞,以区别于其他。之后,继续扫描,直到访问过所有单元格。

完成后,您可以将值-1替换为0,因为不应该有任何0。

此算法有效,但效率不如我提出的其他算法。它的优点是它很简单,不需要额外的数据存储来保存段,孔识别和最终段链接参考。