2

我正在使用具有两种状态“开”和“关”的正方形网格。我有一个相当简单的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 单元格必须恰好是一个组的成员。因此,一种解决方案是获取与新关闭的单元格相邻的每个单元格,并查看它们是否相互连接(例如,使用 * 寻路)。这将产生 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 实例(一个用于需要增强的每种匹配类型)并在正方形发生变化时全部更新它们。我需要对此进行基准测试,看看它是否真的有帮助,但它看起来像是用一些内存换取一些速度的标准案例。

4

3 回答 3

1

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

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

我希望这是有道理的 :)

于 2009-06-27T01:00:07.207 回答
1

这与在围棋游戏(日本的 Igo)中计算(假设网格上的 4 个连通性)连接的棋子串是相同的问题,并且增量计算是高性能围棋游戏算法的关键之一。

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

基于我对这个问题的有限理解,我建议您在打开元素时使用 union-find 来合并标记组,并在关闭网格元素时从头开始重新计算相关组。为了优化这一点,每当您打开和关闭网格元素时,首先处理关闭情况,这样不会浪费联合查找操作。如果您想拥有更高级的增量算法,您可以开始维护每个元素的增量连接数据,但它很可能不会得到回报。

于 2009-06-27T04:15:43.303 回答
0

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

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

[更新 1]如果您知道新关闭的单元格C在 group 中G,那么您可以重新运行 CCL 算法,但只需重新标记 group 中的单元格G。其他 ON 单元格可以保留其现有标签。您不必检查网格的其余部分,与整个网格的初始 CCL 相比,这可以显着节省。(作为一个狂热的 Nurikabe 解谜者,这应该至少节省 33% 的解谜题,并且在进行中的谜题中节省非常可观的费用,不是吗?“33%”来自我的猜测,解开的谜题大约是 2 /3 黑色和 1/3 白色。)

为此,您必须存储每个组中包含的单元格列表,以便您可以快速迭代组中的单元格G并仅重新标记这些单元格。

于 2009-06-26T23:05:30.217 回答