【问题标题】:What is the most efficient way to determine if a ship has been hit in a game of battleship?在战舰游戏中确定一艘船是否被击中的最有效方法是什么?
【发布时间】:2019-07-05 05:49:19
【问题描述】:

在这两种情况下,船只不重叠,可能彼此相邻。

案例 1 - 所有船都是1xN 并且是垂直或水平的

  • Ship 对象包含起点的坐标(顶部
    左)、方向和大小
    • 每次射击时,我们都会遍历所有船只,对于每艘船只,我们都会计算它们的坐标并确定其中一艘是否 坐标匹配镜头坐标

这似乎已经很低效了,因为对于每个镜头,我们都必须遍历所有船只。

案例 2 - 所有船都是任意大小,棋盘是 10 亿乘 10 亿方格,有 100 万艘船

  • 使用以前的方法肯定行不通,因为我们必须保留每艘船的所有坐标列表,并且每次拍摄都需要大量时间来处理

跟踪船只位置/坐标的最有效方法是什么,以便解决方案能够优雅地扩展?

【问题讨论】:

  • 我错过了什么吗?你不会只对那些确实有船的方格感兴趣吗?因此,您创建了一个包含船只坐标的二维矩阵,并通过 x/y 坐标访问该矩阵。
  • 这可能是 k-d 树 或类似空间索引结构的情况。
  • @NevilleKuyt,使用 2D 矩阵,我可以看到我如何能够追踪命中,但我将如何准确追踪哪艘船被命中?我假设 2D 矩阵代表游戏板,如果游戏板太大而无法放入内存怎么办?
  • 您需要定义“高效”的含义(对一个 CPU 的时间有效,对内存消耗有效,对开发人员时间有效,对功耗有效,对“许多 CPU”有效,.. .,多种可能性的某种混合)。您的第一个解决方案(遍历ship 对象)是最有效的(仅用于内存消耗),@NevilleKuyt 的解决方案(2D 数组)可能是最有效的(仅用于 CPU 时间),以下答案中的解决方案可能是对于某些未知的 CPU 时间和内存消耗组合,最有效。
  • @Brendan,这是一个很好的观点,我会在接下来的问题中记住这一点。我想我只是在寻找尽可能多的建议,但在我的脑海中,我的意思是 CPU 效率,但是,内存效率也在这里发挥作用。

标签: algorithm performance optimization data-structures


【解决方案1】:

我认为这对于四叉树来说是很自然的 (https://en.wikipedia.org/wiki/Quadtree)。

这些通过递归地将二维区域划分为 4 个子区域来工作。它利用了许多子区域相同的事实。我将引用维基百科:

四叉树是一种树数据结构,其中每个内部节点正好有四个子节点。四叉树是八叉树的二维模拟,最常用于通过递归地将二维空间细分为四个象限或区域来划分二维空间。与叶单元关联的数据因应用程序而异,但叶单元表示“感兴趣的空间信息单元”。

细分区域可以是正方形或矩形,也可以是任意形状。这种数据结构在 1974 年被 Raphael Finkel 和 J.L. Bentley 命名为四叉树。类似的划分也称为 Q-tree。所有形式的四叉树都有一些共同特征:

它们将空间分解成可适应的细胞 每个单元(或存储桶)都有一个最大容量。当达到最大容量时,桶分裂 树目录遵循四叉树的空间分解。

这应该可以让您有效地存储和查询您的空间。

【讨论】:

  • 哈希在这里相当没用,看看唯一相同的区域可能是那些根本没有船的区域(它们可以用空子树指针表示)。但除此之外,这是一个好主意,占用 O(N*log(M)) 大小和 O(log(M)) 查询时间。
  • @Abstraction 对于 10 亿乘以 10 亿网格上的一百万艘任意大小的船只,我预计会有很多相同的区域,尽管我的直觉可能是错误的。取决于输入的性质。
  • 查看原始帖子的 cmets - OP 需要识别被击中的船只。
  • @Abstraction 知道了。同意。
【解决方案2】:

我不得不不同意四叉树解决方案 - 虽然它仍然很快并且可以完成工作,但它完全忽略了所存储数据点的任何固有结构。如果您想要一个快速的、现成的解决方案,那么这可能就是要走的路,因为许多语言将支持四叉树或至少 k-d 树 (k=2)。如果您想要更快一点且使用更少内存的东西,请考虑以下几点:

遍历垂直方向的飞船,并按它们的 x 坐标将它们分箱成数组,存储它们的 y 坐标、大小和 id 的元组(按 y 坐标排序)。然后构造另一个数组,其中包含这些数组的元组及其对应的 x 坐标,按 x 坐标排序。要查找标记是否击中垂直船,然后对第一个数组执行二进制搜索以查找该列中的所有船舶,然后执行第二次二进制搜索以找到最大 y 坐标等于或小于的船舶马克的。检查mark_y < ship_y + ship_size,如果是,标记击中那艘船,否则什么也没击中。对水平舰船重复此过程,并可选择优先检查更多舰船的方位以首先命中。

虽然这听起来与四叉树非常相似,但有一个重要的区别——我们在开始另一个轴之前将网格完全沿一个轴划分。我们这样做是为了不存储船只延伸经过的每个点;我们只将它们的锚点存储在左上角。我们不能用四叉树来做到这一点,因为我们需要专门查询该列(或水平船的行)以查找船是否“高于”标记延伸穿过标记。如果船舶随着棋盘的大小而缩放,这将导致在朴素四叉树上的渐近加速。如果它们保持在固定的N,那么它仍然会减少log2(N)的查询次数,并减少N的内存消耗。

【讨论】:

    【解决方案3】:

    让我们暂时假设您可以以一种幼稚的方式将所有信息保存在内存中。

    您的棋盘大小为 N x M,并且有 S 艘任意大小的船。我将做一些假设:

    1. 船是长方形的。允许不规则形状的船很容易,但矩形更容易谈论。
    2. 船侧平行于网格线。同样,允许任意方向也很容易。

    您将您的船表示为:

    Ship
        Id
        Top
        Left
        Width
        Height
        Hits // array of [Height x Width] Booleans that indicates which positions have been hit.
        HitCount // number of positions that have been hit. When HitCount == (Width x Height), the ship is sunk.
    

    而你的板子是一个 N x M 数组引用的船只。所以一个有两艘船的 5 x 4 板可能看起来像:

         0     1     2     3     4
    0  ship1 ship1 ship1 NULL  NULL
    1  ship1 ship1 ship1 NULL  ship2
    2  NULL  NULL  NULL  NULL  ship2
    3  NULL  NULL  NULL  NULL  ship2
    

    现在,有人在第 1 行第 3 列拍摄。您查看数组,发现该方格中没有船。干净的小姐。下一个镜头转到 (2, 4)。您对数组进行索引并看到它是船 2。您查找船 2,将棋盘位置 (2, 4) 转换为船的 Hits 数组(在这种情况下,它将是 (0, 1),并记录命中。如果之前没有命中该位置,则增加 HitCount。当 HitCount == (Width x Height) 时,船沉没。

    这种表示允许在拍摄时直接查找:无需搜索。用渐近术语来说,它需要 O((N x M) + S) 内存,并且镜头处理是 O(1)。实际上,最坏情况下的内存是2*(N x M),因为它需要为每艘船提供Height*Width 内存,而在最坏的情况下,这些船可能会填满整个棋盘。

    关键是,如果你有足够的内存来表示整个数据结构,那么查找是非常有效的,并且不依赖于棋盘的大小或船的数量。

    您可以使用一些简单的技巧来减少上述结构的内存占用。例如,为Hits 使用位数组或类似的东西而不是布尔数组可以节省一些空间。将船只存储在一个数组中并在板上使用索引而不是参考可以将用于板的空间减少一半。但即使有内存优化,这种结构也只能走这么远。使用 16 GB 的 RAM,您的最大电路板尺寸可能约为 100,000 x 100,000。

    如果你真的想要一个十亿乘十亿的电路板,拥有一百万艘任意大小的船只,上面的表示将占用大量内存。电路板本身将是 2^60 个单元格。您必须在使用稀疏数据结构方面发挥真正的创造力,但由于船舶可以是任意大小,您必须考虑到最坏的情况:船舶占用了如此多的空间,以至于稀疏数据结构没有给您储蓄。最终,您必须想出一个可以溢出到磁盘的表示,或者可以分布在一个大型集群中的多台机器上。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-03-05
      • 1970-01-01
      • 1970-01-01
      • 2014-01-18
      • 1970-01-01
      • 2018-01-05
      • 2021-11-30
      • 2018-01-05
      相关资源
      最近更新 更多