渐进式连接组件标签

时间:2009-06-26 22:55:18

标签: algorithm language-agnostic path-finding

我正在处理一个有两种状态的正方形网格,“ON”和“OFF”。我有一个相当简单的Connected Component Labeling算法,可以找到所有“ON”组件。通常,但并非总是如此,只有一个“ON”组件。

我希望构建一种算法,该算法接收开/关单元格的矩阵,组件标记(可能格式化为单元格的散列集列表),以及自标签形成以来已更改的单元格列表,并输出新标签。显而易见的解决方案是从头开始重新计算,尽管这不是很有效。通常,已更改的单元格列表很小。

如果更改列表只是已打开的单元格,则很容易做到:

Groups G;
Foreach changed cell C:
  Group U = emptygroup;
  U.add(C);
  Foreach Group S in G:
    if (S contains a cell which is adjacent to C)
      G.Remove(S);
      U.UnionWith(S);
  G.add(C);

但是,如果更改包含任何已关闭的单元格,我不知道该怎么做。请记住,所有ON单元格必须是一个组的成员。因此,一种解决方案是采用与新OFF单元相邻的每个单元,并查看它们是否彼此连接(例如,使用*路径查找)。这将产生1-4个连续的组(除非细胞是其组中的唯一细胞,因此具有0个相邻细胞以进行检查,在这种情况下它产生0个组)。然而,这只比从头开始要好一点,因为通常(但并不总是)将这些相邻的方块连接在一起就像找到一个连续的组一样困难(除非有人建议采用智能方法)。此外,如果有很多改变的细胞,这有点可怕......尽管我承认通常没有。

上下文,对于那些坚持知道我为什么这样做的人: Nurikabe谜题中的一条规则是你可能只有一组连续的墙。我试图解决的一个问题的简化是为了获得更高的速度(和寻找路径一起玩)。基本上,我希望检查连续的墙壁而不浪费从以前的测试中获得的信息。我试图看看我的求解器中有多少地方可以利用以前的信息来提高速度,因为当O(f(Δ))时使用O(f(N))算法似乎很痛苦算法就足够了(N是拼图的大小,Δ是自算法最后运行以来所做的更改)。

分析确实表明改进这个算法会对执行时间产生影响,但这是一个有趣而非盈利的项目,所以它并不重要,除了能够衡量变化是否有任何影响。

注意: 我省略了解释我当前的算法,但它基本上是通过在找到的第一个ON方格上执行基于堆栈的Flood Fill算法,然后检查是否还有更多ON方块(这意味着有更多一组,它没有费心去检查。)

编辑:增强理念:Yairchu和John Kugelman的建议在我的脑海中逐渐形成了这种改进,这本身并不是解决这个问题的方法,但可能会成为代码的一部分,其他几段代码运行得更快:

当前循环:

foreach (Square s in m.Neighbors[tmp.X][tmp.Y])    
{
    if (0 != ((byte)(s.RoomType) & match) && Retval.Add(s)) curStack.Push(s);
}

改进理念:

foreach (Square s in m.NeighborsXX[tmp.X][tmp.Y])    
{
    if (Retval.Add(s)) curStack.Push(s);
}

这将需要维护几个m.NeighborsXX实例(每种类型的匹配需要增强一个)并且每当方块改变时都更新它们。我需要对此进行基准测试以确定它是否真的有用,但它看起来像是以某种速度交易一些内存的标准情况。

3 个答案:

答案 0 :(得分:1)

不是一个完整的解决方案,但这里有:

  • 对于每个连接的组件,在内存中保留生成树
    • 树属性A:我们的生成树有一个概念,哪个节点在“上方”(就像在搜索树中一样)。选择哪个是任意的
  • 让我们讨论删除和添加边
  • 添加边缘时:
    • 通过检查两个节点的根是否相同来检查两个节点是否在同一个组件中
      • 树属性B:树应该是密集的,因此这个检查将是O(log n)
    • 如果在同一组中则不做任何事
    • 如果他们在不同的组中,则使用新边缘加入树。
      • 这需要转换其中一棵树的“形状”(谁是谁的定义)所以我们的新边缘可能“高于”它
  • 删除边缘时:
    • 如果此边缘不参与组的生成树,则不执行任何操作。
    • 如果是,我们需要检查该组是否仍然连接
      • 来自一个组的DFS尝试到达另一个
      • 最好从两者中的较小者那里做到
        • 树属性C:我们为树中的每个节点维护其子树的大小
        • 使用属性C,我们可以告诉两个组的大小
      • 由于属性B:通常较小的组将非常小而较大的组将非常大
      • 如果这些组已连接,那么我们的行为就好像我们添加了连接边
      • 如果组没有连接,那么我们应该爬树来维护属性C(从祖先的子树大小中减去先前连接的子树的大小)
  • 问题:我们如何维护属性B(树是密集的)?

我希望这是有道理的:)

答案 1 :(得分:1)

这与在Go(日本的Igo)游戏中计算(假设网格上的4个连接)连接的石头串相同的问题,并且递增地执行它是高性能Go游戏算法的一个关键。

话虽如此,在这个领域也很简单,当你打开一个网格元素(在棋盘上添加一块石头)时,因为那时你只能加入以前未连接的组件。有问题的情况是当你关闭一个网格元素(由于算法中的撤消而移除一块石头),因为单个组件可以被分割成两个断开连接的元素。

基于我对该问题的有限理解,我建议您在打开元素以合并标记的组时使用union-find,并在关闭网格元素时从头开始重新计算相关组。为了优化这一点,无论何时打开和关闭网格元素,首先处理OFF-case,以便不浪费union-find操作。如果您想拥有更高级的增量算法,您可以开始为每个元素保持增量连接数据,但很可能无法获得回报。

答案 2 :(得分:0)

有趣的问题!这是我最初的想法。希望我会有更多,并会在他们来时更新这个答案......

[更新2] 由于您只关心一个群组,因此A *搜索似乎很理想。你有没有想过A *搜索与重新标记?我不得不认为写得好的A *搜索会比洪水填充更快。如果不是,您可以发布实际代码以获得优化帮助吗?

[更新1] 如果你知道新的OFF单元C在组G中,那么你可以重新运行CCL算法,但只需要重新运行标记组G中的单元格。其他ON单元可以保留其现有标签。您不必检查网格的其余部分,与整个网格的初始CCL相​​比,这可以节省大量成本。 (作为一个狂热的Nurikabe求解器我自己,这应该至少节省33%的解决难题,并且非常显着节省正在进行的拼图,不是吗?“33%”来自我的猜测解决的谜题大概是2/3黑色和1/3白色。)

要执行此操作,您必须存储每个组中包含的单元格列表,以便您可以快速遍历组G中的单元格,并仅重新标记这些单元格。