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

时间:2010-07-07 02:09:51

标签: algorithm graph cyclic

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

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

8 个答案:

答案 0 :(得分:7)

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

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

  1. 选择一个随机路段,将其添加到一个集合中,并将其标记为
  2. 从此细分中分出来,即。按照未标记的两个方向关联连接的部分(如果已标记,我们已经在这里)
  3. 如果找到的路段尚未在集合中,请添加并标记
  4. 继续使用新细分,直到找不到与该集合中当前所有细分相关联的任何其他未加标记的细分
  5. 如果还有未标记的片段,则它们是新片段的一部分,选择一个随机片段并从另一个片段开始返回
  6. 注意:根据官方Catan Rules,如果另一个游戏在两个细分受众群之间的联合上构建解决方案,则可以破坏道路。你需要检测到这一点,而不是通过和解。

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

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

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

    1. 在集合中选择一个随机路段,该路段只有一个连接的路段(即您选择一个端点)
    2. 如果你不能这样做,那么整个集合就是循环(一个或多个),所以在这种情况下选择一个随机段
    3. 现在,从您选择的细分市场中,进行递归分支深度优先搜索,跟踪您到目前为止找到的当前道路的长度。始终标记路段,不要分支到已标记的段。这将允许算法在“吃掉自己的尾巴”时停止。

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

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

答案 1 :(得分:2)

我建议进行广度优先搜索。

对于每位球员:

  • 将默认已知最大值设置为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 :(得分:2)

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

  • 使用播放器的所有边缘从主图表中导出图形,添加连接到边缘的主图形的顶点
  • 制作一个结尾列表(顶点,其中度= = 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();
}

答案 3 :(得分:1)

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

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

答案 4 :(得分:0)

我要做什么:

  1. 选择带道路的顶点
  2. 最多选择两条道路
  3. 沿着这条路走;如果它分支在这里回溯,并选择更长的
  4. 如果当前顶点位于访问列表中,则回溯到3
  5. 将顶点添加到访问列表,从3
  6. 递归
  7. 如果3处没有道路,则返回长度
  8. 当你最多跟随两条道路时,将它们加起来,注意长度
  9. 如果初始顶点有3条道路,则回溯到2。
  10. 对不起prolog-ish术语:)

答案 5 :(得分:0)

这是我用来确定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

答案 6 :(得分:0)

如果有人需要,这是我的版本。写在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
}

答案 7 :(得分:-1)

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

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

虽然效率很高,但并不总能找到实际上最短/最长的路径。