如何在二维矩阵中找到孔?

Posted

技术标签:

【中文标题】如何在二维矩阵中找到孔?【英文标题】:How can I find hole in a 2D matrix? 【发布时间】:2013-06-18 07:18:01 【问题描述】:

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

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

请告诉我你的想法。谢谢。

【问题讨论】:

您希望如何返回有关孔的信息?您想要孔中的细胞列表吗?你想让他们按他们所在的洞分组吗?还是您只需要知道是否有孔,或者有多少孔?或者,每个孔可能需要一个代表单元。或者您只是希望能够指定一个单元格坐标并询问该单元格是否在一个洞中?或者您可能想要另一个 2D 矩阵作为输出,其中每个单元格都被标记为“空洞”或“非空洞”。 输出应该是: 1. 如果有任何孔 2. 细胞在孔中也应该被标记,以便我可以理解这个细胞在孔中。我的意思是我可以随意修改输入缓冲区来标记漏洞。 【参考方案1】:

使用您已经拥有的 Floodfill:沿着矩阵的 BORDER 运行并对其进行填充,即 将所有零(黑色)更改为 2(填充黑色),将一个更改为 3(填充白色);忽略来自较早洪水填充的 2 和 3。

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

计算您找到并设置的颜色数量可能已经提供了矩阵中是否存在个洞的信息。

否则,或者要找到它们的位置,请扫描矩阵:您发现的所有颜色仍为 0 的区域都是黑色的洞。你可能也有白色的洞。

另一种方法,类似于“被阻止的洪水填充”

围绕第一个矩阵的边界运行。在你找到“0”的地方,你设置 到“2”。找到“1”的地方,设置为“3”。

现在绕过新的内边框(那些与您刚刚扫描的边框接触的单元格)。 接触 2 的零单元格变为 2,接触 3 的 1 单元格变为 3。

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

22222222222333333
2AB11111111C
31

单元格 A 实际上是 1。您检查它的邻居并找到 1(但检查 那个 没有用,因为您还没有处理它,所以您无法知道它是否为 1或者应该是 3 - 顺便说一下)、2 和 2。A 2 不能改变 1,所以单元格 A 仍然是 1。同样的单元格 B 也是 1,依此类推.当您到达单元格 C 时,您发现它是 1,并且有一个 3 邻居,所以它切换到 3...但是从 A 到 C 的所有单元格现在都应该切换。 p>

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

22222222222333333
211111111DC333333
33

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

22222222222333333
23333333333333333
33

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

如果您发现您扫描了整个边框而没有甚至一次切换单个单元格,您可以停止该过程。检查这将花费您 2(W*H) 次操作,因此只有在有 很多 个漏洞的情况下才真正值得。

最多 W*H*2 步,您应该完成了。

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

【讨论】:

我认为“Arrested Flood-Fill”是迄今为止最好的解决方案。我要稍微调整一下。步骤是: 1. 共享主矩形边界的任何单元格都是非孔单元格,并标记为 2。 2. 现在,我将不再检查两次扫描,而是检查我的意思是 (x,y) 的四个邻居我将检查 (x-1,y)、(x,y+1)、(x+1,y) 和 (x,y-1)。如果这些邻居中的任何一个是非空洞单元格,那么 (x,y) 也必须是非空洞单元格并标记为 2。在这种情况下,我们可以简单地忽略白色单元格。 3.最后,如果我们遇到一个黑色单元格,其邻居是非孔/白色,那么它肯定是一个孔单元,我们可以开始填充并将孔的所有单元标记为 3。 4. 完成一个 Flood-填充我们必须从第 2 步继续。这种方法的主要关键是“孔单元不能是非孔单元的邻居”。非常感谢您的解决方案,当然如果您发现我的方法有任何缺陷,请告诉我。 您实际上只需要两个邻居:一个位于外边界内,另一个是您在当前步骤“之前”扫描的一个。有一个问题需要额外的步骤,但它不适合此评论:-) -- 所以,添加到答案中...... 嗯...好吧我正在调查它 你能再解释一下“位于外边界内的那个”这个术语吗?我认为您在谈论邻居(x-1,y)和(x,y-1)。我对么?如果是这样,那我想我明白了。【参考方案2】:

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

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

这个算法将是 O(x*y)。

【讨论】:

一个单元可能连接到它的左邻居和它的顶部邻居,并且这些邻居本身可能已被分配到不同的组。所以现在你必须合并这两个组,这意味着访问单元格不止一次,这意味着运行时间大于 O(x*y)。 联合查找算法是一种有效的方法,包括合并组。谷歌它有很多信息。但它仍然超过 O(x*y)。 是的,我忘了它并更新了我的答案。由于合并可以在恒定时间内完成,因此复杂度仍然为 O(x*y)。 @Chechulin:感谢您的解决方案。我现在正在调查。 如果您在恒定时间内进行合并,则查找组成员需要非常时间。你只是推迟了额外的工作。仍然需要确定细胞是否在洞中。【参考方案3】:

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

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

【讨论】:

感谢您的解决方案,但就算法复杂性而言,这不会与我的解决方案几乎相同吗? 是的,线性的。没有比线性更好的了:如果算法是次线性的,我可以在算法未访问过的单元格中放置洞,而算法将无法找到它们。 你关心悲观复杂性,对吧?不平均?你没有在你的问题中提到哪一个...... 因为如果您关心平均复杂度并且需要找到任何漏洞(不是所有漏洞),那么还有一些改进空间。 是的,这是最坏的情况,因为我需要找到所有漏洞并标记每个单元格【参考方案4】:

您的算法在全球范围内都可以。这只是通过将洪水填充探索与单元扫描合并来优化它的问题。这只会最小化测试。

总体思路是在扫描表的同时逐行执行flood fill探索。因此,您将需要跟踪多个并行洪水填充。

然后表格从上到下逐行处理,每一行从右到左处理。顺序是任意的,如果您愿意,可以颠倒。

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

这个算法的好处是您只需要跟踪第 i 行和第 i-1 行中的段及其标识号。因此,当您处理第 i 行时,您将拥有在第 i-1 行中找到的段列表及其关联的标识号。

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

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

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

    发现第 i-1 行中的段未连接到第 i-1 行中的段。你找到了一个洞的最低部分。如果它的标识号为负,则它连接到边界,您可以忽略它。否则,恭喜你,你找到了一个洞。

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

第 i-1 行:2 333 444 111 第 i 行:**** *** ***

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

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

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

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

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

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

【讨论】:

感谢您的精彩解释。实际上我需要确定白色区域是否有洞。我还需要标记空洞区域,以便了解任何单元格属于空洞区域还是非空洞区域。 这个算法会告诉你白色区域是否有洞。如果您只需要返回一个布尔值,您可以在找到第一个洞时返回 true。如果它未连接到边界,则它位于白色区域内。您如何需要标记?一种简单的方法是建立连接段的链表。然后你有一个相对紧凑的孔表示。您可以使用它在属于每个孔的单元格中设置孔编号值。【参考方案5】:

here 描述的蛮力算法如下。

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

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

由于您只需要考虑孔洞(值为 0 的单元格被值为 1 的单元格包围),因此您必须使用两遍。

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

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

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

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

【讨论】:

以上是关于如何在二维矩阵中找到孔?的主要内容,如果未能解决你的问题,请参考以下文章

在二维矩阵中找到最大数量为 0 的行

如何应用泛洪算法找到加权二维矩阵的指定源和目标位置之间的最佳路径

Clickhouse - 矩阵逐项加法:如何对二维数组求和?

搜索二维矩阵II

在二维矩阵上使用 std::max_element

如何在numpy的二维矩阵中随机采样