一组图块边界的高效算法


12

我有一个已知的有限大小的瓷砖网格,可以形成地图。地图内的某些图块放置在称为“区域”的集合中。该区域是相连的,但对其形状一无所知。在大多数情况下,它是一个相当规则的斑点,但它可能在一个方向上被拉长,甚至可能有孔。我有兴趣找到该领土的(外)边界。

也就是说,我想要一个列表,列出所有与区域中的某个区域相关但未在区域中的区域。找到这个的有效方法是什么?

额外的困难是,我的图块是十六进制的,但是我怀疑这并没有太大的区别,每个图块仍标记有x和y整数坐标,并且给定一个图块,我可以轻松找到它的邻居。下面是一些示例:黑色是领土,蓝色是我要查找的边界。 领土和边界示例 这本身并不是一个难题,用伪python的一种简单算法是:

def find_border_of_territory(territory):
    border = []
    for tile in territory:
        for neighbor in tile.neighbors():
            if neighbor not in territory and neighbor not in border:
                border.add(neighbor)

但是,这很慢,我想要更好的东西。我在整个领土上有一个O(n)循环,在所有邻居上有另一个循环(虽然很短,但仍然),然后我必须检查两个列表的成员资格,其中一个列表的大小为n。这给定了O(n ^ 2)的可怕比例。我可以通过使用集而不是边界和地区列表来将其简化为O(n),以便快速检查成员资格,但这仍然不是很好。我希望在很多情况下,由于面积和线比例缩放的原因,领土很大但边界很小。例如,如果区域是半径为5的十六进制,则其大小为91,而边框的大小仅为36。

谁能提出更好的建议?

编辑:

要回答以下一些问题。区域的大小范围从20到100左右不等。形成区域的图块集是一个对象的属性,而这个对象需要所有边界图块的集合。

最初,领土是作为一个街区创建的,然后大部分都是一块一块地获得。在这种情况下,确实最快的方法是保留一组边界并仅在获得的图块上更新它。有时,可能会对该地区进行重大更改-因此需要重新计算。

我现在认为,执行简单的边界查找算法是最好的解决方案。这带来的唯一额外复杂性是确保可能需要每次都重新计算边界,但不超过此数目。我非常有信心可以在当前框架中可靠地完成此操作。

至于时间,在我当前的代码中,我有一些例程需要检查该区域的每个图块。不是每回合,而是在创作时以及随后的创作中。即使它只是完整程序的很小一部分,也要花费我的测试代码运行时间的50%以上。因此,我渴望尽量减少重复。但是,测试代码涉及的对象创建要比程序正常运行(自然)要多得多,因此我意识到这可能不太相关。


10
我认为,如果对形状一无所知,则O(N)算法似乎很合理。我想,任何更快的方法都不需要查看区域的每个元素,只有在您对形状有所了解的情况下,这才起作用。
阿米普

3
您可能不需要经常这样做。同样,n不是很大,比瓦片的总数少得多。
Trilarion

1
如何创建/更改这些区域?如何经常做他们改变?如果按图块选择它们,那么您可以在移动时建立邻居列表,除非它们经常更改,否则您可以存储一系列区域及其边界,并在移动时添加或删除它们(因此,需要不断地重新计算它们)。
DaveMongoose

2
重要提示:这是实际诊断和分析的性能问题吗?问题集很小(真的只有几百个元素),我真的不认为这个O(n ^ 2)或O(n)应该是一个问题。听起来像是在不会每帧运行的系统上过早的优化。
迪洛斯

1
因为最多有6个邻居,所以简单的算法为O(n)。
埃里克

Answers:


11

查找算法通常最好使用简化算法的数据结构来完成。

在这种情况下,您的领地。

区域应该是边界和元素的无序(O(1)哈希)集。

每当您将一个元素添加到区域中时,您就在相邻的图块上进行迭代,看看它们是否应该是边框图块;在这种情况下,如果它们不是元素图块,则它们是边框图块。

每当您从区域中减去元素时,请确保其相邻的图块仍在该区域中,然后查看您是否应成为边框图块。如果需要快速处理,请让边界图块跟踪其“相邻计数”。

每当您在区域中添加平铺或从中删除平铺时,这都需要O(1)的工作。到达边界需要O(边界长度)。只要您要比在区域中添加/删除元素多得多地知道“边界是什么”,就应该会获胜。


9

如果您还需要在您的区域中间找到孔的边缘,那么我们可以做的最好的事情就是在区域范围内线性化。内部的任何瓷砖都可能是我们需要计算的孔,因此我们需要至少查看一次以区域轮廓为边界的区域中的每个瓷砖,以确保找到了所有孔。

但是,如果您只关心查找外部边界(而不是内部孔),那么我们可以更有效地做到这一点:

  1. 找到一条将您的领土分开的优势。您可以通过...

    • (如果您至少知道一个区域图块,并且知道您的地图上只有一个连接的区域图块)

      ...从您所在区域的任意图块开始,然后朝地图的最近边缘移动。这样做时,请记住从区域图块过渡到非区域图块的最后一条边。一旦您击中地图的边缘,此记住的边缘就是您的起始边缘。

      该扫描在图的直径上是线性的。

    • 或(如果您不知道任何区域图块在哪里,或者您的地图可能包含多个断开连接的区域)

      ...从地图的边缘开始,沿每一行扫描,直到碰到地形图块。从非地形到地形的最后一条边是您的起点。

      此扫描在地图区域中可能是最差的线性关系(其直径为二次方),但是如果您有任何限制搜索的限制(例如,您知道该区域几乎总是越过中间行),则可以改善这种最差的情况-案例行为。

  2. 从第1步中找到的起始边缘开始,围绕地形周边进行跟踪,将外部的每个非地形图块添加到边框集合中,直到返回起始边缘为止。

    此边缘跟随步骤在地形轮廓的周边而不是其区域中呈线性。缺点是代码更加复杂,因为您需要考虑边缘可以进行的每种转弯,并避免重复计算入口处的边框瓦片。

如果您的示例可以将您的实际数据大小表示为几个数量级,那么我本人会去进行朴素的区域搜索-在这么少的图块上仍然会非常快,并且编写起来非常简单,了解和维护(通常会减少错误!)


7

注意:图块是否在边界上仅取决于它及其邻居。

因此:

  • 懒惰地运行此查询很容易。例如:您不需要在整个地图上搜索边界,而仅在可见的位置上搜索。

  • 并行运行此查询很容易。实际上,我可以映像一些着色器代码来实现此目的。而且,如果您需要其他可视化功能,则可以渲染为纹理并使用它。

  • 如果图块发生变化,则边界仅在局部发生变化,这意味着您无需再次计算整个事物。

您还可以预先计算边界。也就是说,如果您要填充十六进制,则可以确定此时瓷砖是否为边界。这意味着:

  • 如果使用循环填充网格,则可以使用它来确定边界。
  • 如果从空网格开始并选择要更改的图块,则可以在本地更新边界。

不要使用列表作为边界。如果确实需要,请使用一组(我不知道您想要什么边界。)。但是,如果您将某个图块定为边界或不是该图块的属性,则不必转到另一个数据结构进行检查。


2

将您的区域上移一个图块,然后右上移,然后右下移,依此类推。然后删除原始区域。

合并所有六个集合应为O(n),对O(n.log(n))进行排序,集合差为O(n)。如果原始图块以某种排序格式存储,则合并后的集合也可以按O(n)排序。

我不认为有一种算法不会小于O(n),因为您需要至少访问一次每个图块。


1

我刚刚写了一篇有关如何执行此操作的博客文章。这使用@DMGregory提到的第一种方法,该方法从边缘单元开始,然后沿周边移动。它使用C#而不是Python,但是应该很容易适应。

https://dillonshook.com/hex-city-borders/


0

原始帖子:

我无法在此站点上发表评论,因此我将尝试使用伪代码算法进行回答。

您知道每个领土最多有六个邻国是边界的一部分。对于区域中的每个图块,将六个相邻图块添加到潜在边界列表中。然后从边界中减去区域中的所有图块,仅剩下边界图块。如果您使用无序集来存储每个列表,则将是最好的选择。希望我会有所帮助。

编辑还有比简单迭代更有效的方法。正如我试图在下面的答案(现已删除)中指出的那样,最好的情况下可以达到O(1),最坏的情况下可以达到O(n)。

将图块添加到区域O(1)-O(N)中:

如果没有邻居,您只需创建一个新区域。

如果是一个邻居,则将新图块添加到现有区域。

对于5个或6个邻居,您知道它已全部连接,因此可以将新的图块添加到现有区域。这些都是O(1)操作,更新新边界区域也是O(1),因为它是一组与另一组的简单合并。

对于2个,3个或4个相邻区域,您可能必须合并多达3个唯一区域。这是合并区域大小的O(N)。

从区域O(1)-O(N)移除图块:

邻居数为零时,会擦除该区域。O(1)

与一位邻居一起从领土上移除瓷砖。O(1)

如果有两个或多个邻居,则最多可以创建3个新领土。这是O(N)。

在过去的几周中,我用业余时间开发了一个演示程序,该程序是一个基于十六进制的简单区域游戏。尝试通过将领土彼此相邻来增加收入。红色,绿色和蓝色这3个玩家竞争,通过策略性地在有限的游戏场上放置磁贴来获得最大的收入。

您可以在此处(.7z格式)hex.7z下载游戏

简单的鼠标控制LMB放置一个图块(只能放置在悬停突出显示的位置)。得分最高,收入最低。看看您能否提出有效的策略。

代码可以在这里找到:

鹰/鹰测试

要从源代码进行构建,您需要Eagle和Allegro5。两者均使用cmake进行构建。十六进制游戏目前使用CB项目构建。

把那些倒票倒过来。:)


这实际上是OP中算法的工作方式,尽管在包含之前检查相邻图块比最后将它们全部删除要快一些。
ScienceSnake

基本上是一样的,但是如果您只减去一次就可以提高效率
BugSquasher

我已经完全更新了我的答案,并删除了下面的多余答案。
BugSquasher
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.