问题 在算法上找到Settlers of Catan游戏中最长的道路


我正在为一堂课写一本卡特定居者。额外的信用功能之一是自动确定哪个玩家拥有最长的道路。我已经考虑过了,看起来深度优先搜索的一些细微变化可能会起作用,但我无法弄清楚如何处理循环检测,如何处理玩家的两个初始道路网络的加入,和其他一些细枝末节。我怎么能在算法上做到这一点?

对于那些不熟悉游戏的人,我会尝试简洁而抽象地描述问题:我需要在无向循环图中找到最长的路径。


12091
2017-07-07 02:09


起源

我希望你不是在寻找任何真正有效的东西,最长的路径就是NP完全! - Jan Gorzny
我一直在做一些检查,但我会查看Adjacency矩阵。我会发布一个答案说这个,但我无法找到最长的非循环路径的算法。此外,对于Settlers地图中的道路数量,它可能会有些复杂,特别是如果您有多个玩家的不同地图大小。 - fire.eagle
@Jan:当我发现最长的路径是NP完全时,我很失望,但我认为问题的特殊性会产生一些优化,允许在多项式或更好的时间内求解。 - Jay
复杂性并不是那么大,因为没有玩家将拥有超过10-15个路段,因此即使是低效的算法也会立即搜索它。如果游戏的实施是你的目标,我不会担心这里的大O.如果它正在学习图算法,这是另一回事:) - Amadan
不要忘记,如果在其中间建立了另一个玩家聚居地或城市,则可以断开道路。 - Cameron MacFarland


答案:


这应该是一个相当容易实现的算法。

首先,将道路分成不同的集合,其中每组中的所有道路段都以某种方式连接。有这样做的各种方法,但这里有一个:

  1. 选择一个随机路段,将其添加到一个集合中并标记它
  2. 分支出这个部分,即。按照未标记的两个方向关联连接的部分(如果已标记,我们已经在这里)
  3. 如果找到的路段尚未在集合中,请添加并标记
  4. 继续从新细分开始,直到找不到与该集合中当前的细分相关的任何其他未标记的细分
  5. 如果还有未标记的片段,则它们是新片段的一部分,选择一个随机片段并从另一个片段开始返回1

注意:按官方说 卡坦规则如果另一个游戏在两个部分之间的联合上建立了解决方案,则可以打破道路。您需要检测到这一点,而不是通过结算。

注意,在上面和后面的步骤中,只考虑当前的玩家细分。您可以忽略其他段,就好像它们甚至不在地图上一样。

这会为您提供一个或多个集合,每个集合包含一个或多个路段。

好的,对于每一组,请执行以下操作:

  1. 在集合中选择一个随机路段,该路段只有一个连接的路段(即,您选择一个端点)
  2. 如果你不能这样做,那么整个集合就是循环(一个或多个),所以在这种情况下选择一个随机段

现在,从您选择的细分市场,进行递归分支深度优先搜索,跟踪您到目前为止找到的当前道路的长度。始终标记路段,不要分支到已标记的段。这将允许算法在“吃掉自己的尾巴”时停止。

每当您需要回溯时,因为没有更多分支,请记下当前长度,如果长度超过“之前的最大值”,则将新长度存储为最大长度。

为所有套装做这件事,你应该有最长的路。


7
2017-07-07 07:33



这个答案是全面和正确的。 - Borealid
我将第3步修改为仅查找同一玩家的路段,从没有其他玩家的节点分支出来。 - Cameron MacFarland
是的,让我编辑一下,谢谢@Cameron。 - Lasse Vågsæther Karlsen
这听起来像是解决我问题的好方法。谢谢! - Jay
该算法不完整。如果在第2部分的选项1中选择了错误的随机路段,它将无法找到最佳解决方案。我认为它也可能在选项2中失败。为了保证完整性,您必须为每个端点道路运行第2部分。设置(对于选项1)或集合中的每条道路(对于选项2)。如果需要,我可以提供这两种情况的例子。 - Zeimyth


我会建议广度优先搜索。

对于每个球员:

  • 将默认已知最大值设置为0
  • 选择一个起始节点。如果它具有零个或多个连接的邻居,则跳过,并以广度优先的方式找到从其开始的所有玩家路径。捕获:一旦一个节点被遍历,对于该回合中剩下的所有搜索,它被“停用”。也就是说,它可能不再被遍历。

    如何实施?这是一种可以使用的广度优先算法:

    1. 如果此初始节点或多个路径没有路径,请将其标记为已停用并跳过它。
    2. 保持路径队列。
    3. 将仅包含初始死端节点的路径添加到队列中。停用此节点。
    4. 弹出队列中的第一条路径并“爆炸”它 - 也就是说,找到所有有效路径,它们是有效方向上的路径+ 1步。通过“有效”,下一个节点必须通过道路连接到最后一个节点,并且还必须激活它。
    5. 在最后一步中停用所有步进的节点。
    6. 如果前一个路径没有有效的“爆炸”,则将该路径的长度与已知最大值进行比较。如果大于它,则为新的最大值。
    7. 将所有爆炸路径(如果有)添加回队列。
    8. 重复4-7直到队列为空。
  • 执行此操作一次后,转到下一个激活的节点并重新开始该过程。停用所有节点时停止。

  • 对于给定的玩家,你现在拥有的最大值是你最长的道路长度。

请注意,这效率稍低,但如果性能无关紧要,那么这将有效:)

重要提示,感谢Cameron MacFarland

  • 假设具有不属于当前播放器的城市的所有节点始终自动停用。

Ruby伪代码(假设一个 get_neighbors 每个节点的功能)

def explode n
  exploded = n.get_neighbors             # get all neighbors
  exploded.select! { |i| i.activated? }  # we only want activated ones
  exploded.select! { |i| i.is_connected_to(n) } # we only want road-connected ones
  return exploded
end

max = 0

nodes.each do |n|                 # for each node n
  next if not n.activated?        # skip if node is deactivated
  if explode(n).empty? or explode(n).size > 1
    n.deactivate                  # deactivate and skip if
    next                          # there are no neighbors or
  end                             # more than one

  paths = [ [n] ]                 # start queue

  until paths.empty?              # do this until the queue is empty

    curr_path = paths.pop         # pop a path from the queue
    exploded = explode(curr_path) # get all of the exploded valid paths

    exploded.each { |i| i.deactivate }  # deactivate all of the exploded valid points

    if exploded.empty?                  # if no exploded paths
      max = [max,curr_path.size].max    # this is the end of the road, so compare its length to
                                        # the max
    else
      exploded.each { |i| paths.unshift(curr_path.clone + i) }  
                                        # otherwise, add the new paths to the queue
    end

  end

end

puts max

2
2017-07-07 07:06





不太晚,但仍然相关。我在java中实现它,请参阅 这里。算法如下:

  • 使用来自播放器的所有边从主图中导出图形,添加连接到边的主图的顶点
  • 创建一个结尾列表(顶点,其中度= = 1)和拆分(顶点,其中度= = 3)
  • 为每个末尾添加一个检查路径(possibleRoute),并且每隔两个边缘+找到一个顶点组合(3,因为度数为3)
  • 走每条路。可以遇到三件事:结束,中间或分裂。
  • 结束:终止possibleRoute,将其添加到找到的列表中
  • 中间:检查是否可以连接到其他边缘。如果是这样,请添加边缘。如果没有,请终止路由并将其添加到找到的路由列表中
  • 分裂:对于两个边,做中间。当两条路线连接时,复制第二条路线并将其添加到列表中。
  • 使用两种方法检查连接:查看新边缘是否已存在于possibleRoute(无连接)中,并通过将顶点和原始顶点作为参数询问新边缘是否可以连接。道路可以连接到道路,但前提是顶点不包含来自其他玩家的城市/定居点。道路可以连接到船只,但仅当顶点持有正在检查路线的玩家的城市或道路时。
  • 循环可能存在。如果尚未存在,则每个遇到的边都会添加到列表中。当选中所有可能的路径时,但此列表中的边缘量小于播放器边缘的总量,则存在一个或多个循环。在这种情况下,任何未经检查的边缘都将成为新的可能路径,并再次检查该路线。
  • 通过简单地遍历所有路线来确定最长路线的长度。返回遇到等于或多于5个边的第一个路径。

此实现支持Catan的大多数变体。边缘可以自己决定是否要连接到另一个,请参阅

SidePiece.canConnect(point, to.getSidePiece());

道路,船舶,桥梁都实施了SidePiece接口。道路有实施

public boolean canConnect(GraphPoint graphPoint, SidePiece otherPiece)
{
    return (player.equals(graphPoint.getPlayer()) || graphPoint.getPlayer() == null)
            && otherPiece.connectsWithRoad();
}

2
2018-02-12 19:29





简单的多项式时间深度优先搜索不太可行,因为问题是NP难的。您将需要一些需要指数时间来获得最佳解决方案的东西。由于问题很小,但在实践中应该没问题。

可能最简单的解决方案是动态编程:保留一个表T [v,l]存储每个节点v和每个长度l一组路径长度为l并以v结尾。显然T [v,1] = { [v]}你可以通过收集来自T [w,l-1]的所有路径为l> 1填写T [v,l],其中w是v的邻居,但是还没有包含v,然后附加v这与Justin L.的解决方案类似,但避免了一些重复的工作。


1
2017-07-07 08:52





我要做的是:

  1. 选择一个带有道路的顶点
  2. 最多选择两条道路
  3. 沿着这条路走;如果它分支在这里回溯,并选择更长的那个
  4. 如果当前顶点位于访问列表中,则回溯到3
  5. 将顶点添加到访问列表,从3递增
  6. 如果3处没有道路,则返回长度
  7. 当您最多跟随2条道路时,将它们加起来,记下长度
  8. 如果在初始顶点有3条道路,则回溯到2。

对不起prolog-ish术语:)


0
2017-07-07 02:25



我担心我没有抓住你的想法......我不明白第3步。 - Jay
你走到了十字路口。记住访问列表。走一条路,注意这条腿的长度。将访问列表重置为记住的值。走另一条路,记下长度 那 腿。超出此顶点的最大长度是两者中的较长者。如果没有十字路口,那就更简单了。 - Amadan


这是我用来确定Catan Excel VBA模拟器中给定玩家(“用户”)最长路的代码片段。

我刚才意识到它没有考虑到有关定居点的规则,但这不应该很难融入。

Private Frays As Dictionary
Private RoadPths As Collection
Public Function GetLongestRoad(g As Board, oUser As User) As Long
    Dim oRoad As Road
    Dim v, w
    Dim fNum As Long
    Dim rCount As Long

    Set Frays = New Dictionary
    Set RoadPths = New Collection

    ' get list of roads that happen to be linked to other roads ("Frays")
    For fNum = 55 To 126
        If g.Roads(fNum).Owner = oUser.Name Then
            For Each v In RoadLinks(g, fNum)
                If g.Roads(v).Owner = oUser.Name Then Frays(fNum) = v
            Next v
        End If
    Next fNum

    ' begin recursion
    For Each v In Frays
        RecurSegmts g, v & ";" & Frays(v)
    Next v

    rCount = 0
    For Each v In RoadPths
        w = Split(v, ";")
        If rCount < UBound(w) Then rCount = UBound(w)
    Next v

    GetLongestRoad = rCount + 1
End Function

Private Sub RecurSegmts(g As Board, Segmts As Variant)
    Dim SegmtArr, NextRoad
    Dim LoopsBack As Boolean
    Dim i As Long

    RoadPths.Add Segmts
    SegmtArr = Split(Segmts, ";")

    For Each NextRoad In RoadLinks(g, CLng(SegmtArr(UBound(SegmtArr))))
        LoopsBack = False
        For i = 0 To UBound(SegmtArr)
            If CLng(NextRoad) = CLng(SegmtArr(i)) Then LoopsBack = True
        Next i

        If LoopsBack = False Then Call RecurSegmts(g, Segmts & ";" & NextRoad)

    Next NextRoad
End Sub

0
2017-12-15 20:18





如果有人需要,这是我的版本。写在 Typescript

longestRoadLengthForPlayer(player:PlayerColors):number {         让longestRoad = 0

    let mainLoop = (currentLongestRoad: number, tileEdge: TileEdge, passedCorners: TileCorner[], passedEdges: TileEdge[]) => {
        if (!(passedEdges.indexOf(tileEdge) > -1) && tileEdge.owner == player) {
            passedEdges.push(tileEdge)
            currentLongestRoad++
            for (let endPoint of tileEdge.hexEdge.endPoints()) {
                let corner = this.getTileCorner(endPoint)!

                if ((corner.owner == player || corner.owner == PlayerColors.None) && !(passedCorners.indexOf(corner) > -1)) {
                    passedCorners.push(corner)
                    for (let hexEdge of corner.hexCorner.touchingEdges()) {
                        let edge = this.getTileEdge(hexEdge)
                        if (edge != undefined && edge != tileEdge) {
                            mainLoop(currentLongestRoad, edge, passedCorners, passedEdges)
                        }
                    }
                } else {
                    checkIfLongestRoad(currentLongestRoad)
                }
            }
        } else {
            checkIfLongestRoad(currentLongestRoad)
        }
    }

    for (let tileEdge of this.tileEdges) {
        mainLoop(0, tileEdge, [], [])
    }

    function checkIfLongestRoad(roadLength: number) {
        if (roadLength > longestRoad) {
            longestRoad = roadLength
        }
    }

    return longestRoad
}

0
2017-11-30 12:25





您可以使用Dijkstra,只需更改条件即可选择最长的路径。

http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm

这是有效的,但并不总能找到实际上最短/最长的路径,尽管它在大多数时间都可以工作。


-1
2017-07-07 07:37



对于棋盘游戏来说,胜利取决于谁拥有最长的道路,一个chancy算法可能不是一个好的选择。 - Borealid
是的,取决于他所称的功能是决定胜利的因素。如果它不会导致玩家获胜/失败,我会说效率比找到实际的最短路径更重要。 - martiert