12

I'm writing a Settlers of Catan clone for a class. One of the extra credit features is automatically determining which player has the longest road. I've thought about it, and it seems like some slight variation on depth-first search could work, but I'm having trouble figuring out what to do with cycle detection, how to handle the joining of a player's two initial road networks, and a few other minutiae. How could I do this algorithmically?

For those unfamiliar with the game, I'll try to describe the problem concisely and abstractly: I need to find the longest possible path in an undirected cyclic graph.

Tomerikoo
  • 18,379
  • 16
  • 47
  • 61
Jay
  • 510
  • 6
  • 17
  • I hope you're not looking for anything really efficient, longest path is known to be NP-complete! – Jan Gorzny Jul 07 '10 at 02:28
  • I've been doing some checking on this, but I'd look into Adjacency matrices. I would have posted an answer saying this, but I haven't been able to hunt down an algorithm for the longest non-cyclic path. Also, with the number of roads in a Settlers map, it may get somewhat complicated, especially if you have varying map sizes for multiple players. – fire.eagle Jul 07 '10 at 02:29
  • @Jan: I was disappointed when I discovered that longest path is NP-complete, but I figured that the specificity of the problem would yield some optimizations allowing for solving in polynomial or better time. – Jay Jul 07 '10 at 02:34
  • 2
    The complexity is not really all that big since no player will ever have more than 10-15 road segments, thus even an inefficient algorithm will search it in no time at all. I wouldn't worry about the big O here if implementation of the game is your goal. If it's learning about graph algorithms, that's another matter :) – Amadan Jul 07 '10 at 03:24
  • How are your roads represented? As joins of nodes? Also, how are you going to handle roads that circle back on themselves? – Justin L. Jul 07 '10 at 06:51
  • 1
    Don't forget that a road can be disconnected if another players settlement or city is built in the middle of it. – Cameron MacFarland Jul 07 '10 at 07:55
  • If you want to get some ideas you could study http://pio.sourceforge.net/ – Unreason Jul 07 '10 at 08:10
  • specifically https://pio.svn.sourceforge.net/svnroot/pio/trunk/pioneers/common/map_query.c, map_longest_road(), though not sure should you get any credit if you take it all from there – Unreason Jul 07 '10 at 08:45

7 Answers7

8

This should be a rather easy algorithm to implement.

To begin with, separate out the roads into distinct sets, where all the road segments in each set are somehow connected. There's various methods on doing this, but here's one:

  1. Pick a random road segment, add it to a set, and mark it
  2. Branch out from this segment, ie. follow connected segments in both directions that aren't marked (if they're marked, we've already been here)
  3. If found road segment is not already in the set, add it, and mark it
  4. Keep going from new segments until you cannot find any more unmarked segments that are connected to those currently in the set
  5. If there's unmarked segments left, they're part of a new set, pick a random one and start back at 1 with another set

Note: As per the official Catan Rules, a road can be broken if another play builds a settlement on a joint between two segments. You need to detect this and not branch past the settlement.

Note, in the above, and following, steps, only consider the current players segments. You can ignore those other segments as though they weren't even on the map.

This gives you one or more sets, each containing one or more road segments.

Ok, for each set, do the following:

  1. Pick a random road segment in the set that has only one connected road segment out from it (ie. you pick an endpoint)
  2. If you can't do that, then the whole set is looping (one or more), so pick a random segment in this case

Now, from the segment you picked, do a recursive branching out depth-first search, keeping track of the length of the current road you've found so far. Always mark road segments as well, and don't branch into segments already marked. This will allow the algorithm to stop when it "eats its own tail".

Whenever you need to backtrack, because there are no more branches, take a note of the current length, and if it is longer than the "previous maximum", store the new length as the maximum.

Do this for all the sets, and you should have your longest road.

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • I'd modify step 3 to only look for road segments of the same player, branching out from a node that does not have another players piece on it. – Cameron MacFarland Jul 07 '10 at 08:13
  • This algorithm is not complete. It will fail to find the optimal solution if it picks the wrong random road segment in option 1 of part 2. I believe it can also fail in option 2. In order to guarantee completeness, you must run part 2 for every endpoint road in the set (for option 1) or every road in the set (for option 2). I can provide examples of both cases if needed. – Zeimyth Nov 18 '14 at 02:29
3

A simple polynomial-time depth-first search is unlikely to work, since the problem is NP-hard. You will need something that takes exponential time to get an optimal solution. Since the problem is so small, that should be no problem in practice, though.

Possibly the simplest solution would be dynamic programming: keep a table T[v, l] that stores for each node v and each length l the set of paths that have length l and end in v. Clearly T[v, 1] = {[v]} and you can fill out T[v, l] for l > 1 by collecting all paths from T[w, l-1] where w is a neighbor of v that do not already contain v, and then attaching v. This is similar to Justin L.'s solution, but avoids some duplicate work.

Falk Hüffner
  • 4,942
  • 19
  • 25
2

I would suggest a breadth-first search.

For each player:

  • Set a default known maximum of 0
  • Pick a starting node. Skip if it has zero or more than one connected neighbors and find all of the player's paths starting from it in a breadth-first manner. The catch: Once a node has been traversed to, it is "deactivated" for the all searches left in that turn. That is, it may no longer be traversed.

    How can this be implemented? Here's one possible breadth-first algorithm that can be used:

    1. If there are no paths from this initial node, or more than one path, mark it deactivated and skip it.
    2. Keep a queue of paths.
    3. Add a path containing only the initial dead-end node to the queue. Deactivate this node.
    4. Pop the first path out of the queue and "explode" it -- that is, find all valid paths that are the the path + 1 more step in a valid direction. By "valid", the next node must be connected to the last one by a road, and it also must be activated.
    5. Deactivate all nodes stepped to during the last step.
    6. If there are no valid "explosions" of the previous path, then compare that length of that path to the known maximum. If greater than it, it is the new maximum.
    7. Add all exploded paths, if any, back into the queue.
    8. Repeat 4-7 until the queue is empty.
  • After you do this once, move onto the next activated node and start the process all over again. Stop when all nodes are deactivated.

  • The maximum you have now is your longest road length, for the given player.

Note that this is slightly inefficient, but if performance doesn't matter, then this would work :)

IMPORTANT NOTE, thanks to Cameron MacFarland

  • Assume all nodes with cities that do not belong to the current player automatically deactivated always.

Ruby pseudocode (assumes an get_neighbors function for each node)

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
Justin L.
  • 13,510
  • 5
  • 48
  • 83
2

Little late, but still relevant. I implemented it in java, see here. Algorithm goes as follows:

  • Derive a graph from the main graph using all edges from a player, adding the vertices of the main graph connected to the edges
  • Make a list of ends (vertices where degree==1) and splits (vertices where degree==3)
  • Add a route to check (possibleRoute) for every end, and for every two edges + one vertex combination found (3, since degree is 3) at every split
  • Walk every route. Three things can be encountered: an end, intermediate or a split.
  • End: Terminate possibleRoute, add it to the found list
  • Intermediate: check if connection to other edge is possible. If so, add the edge. If not, terminate the route and add it to the found routes list
  • Split: For both edges, do as intermediate. When both routes connect, copy the second route and add it to the list, too.
  • Checking for a connection is done using two methods: see if the new edge already exists in the possibleRoute (no connection), and asking the new edge if it can connect by giving the vertex and the originating vertex as parameters. A road can connect to a road, but only if the vertex does not contain a city/settlement from another player. A road can connect to a ship, but only if the vertex holds a city or road from the player whom route is being checked.
  • Loops may exist. Every encountered edge is added to a list if not already in there. When all possibleRoutes are checked, but the amount of edges in this list is less then the total amount of edges of the player, one or more loops exist. When this is the case, any nonchecked edge is made a new possibleRoute from, and is being checked again for the route.
  • Length of longest route is determined by simply iterating over all routes. The first route encountered having equal or more then 5 edges is returned.

This implementation supports most variants of Catan. The edges can decide for themselves if they want to connect to another, see

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

A road, ship, bridge have SidePiece interface implemented. A road has as implementation

public boolean canConnect(GraphPoint graphPoint, SidePiece otherPiece)
{
    return (player.equals(graphPoint.getPlayer()) || graphPoint.getPlayer() == null)
            && otherPiece.connectsWithRoad();
}
Ruudjah
  • 807
  • 7
  • 27
0

What I'd do:

  1. Pick a vertex with a road
  2. Pick at most two roads to follow
  3. Follow the the road; backtrack here if it branches, and choose that which was longer
  4. If current vertex is in the visited list, backtrack to 3
  5. Add the vertex to the visited list, recurse from 3
  6. If there is no more road at 3, return the length
  7. When you've followed at most 2 roads, add them up, note the length
  8. If there were 3 roads at the initial vertex, backtrack to 2.

Sorry for the prolog-ish terminology :)

Amadan
  • 191,408
  • 23
  • 240
  • 301
  • I'm afraid I'm not catching your thoughts... I don't understand step 3. – Jay Jul 07 '10 at 03:12
  • You come to a crossroads. Remember the visited list. Go down one road, note the length of this leg. Reset the visited list to the remembered value. Go down the other road, note the length of _that_ leg. Maximum length beyond this vertex is the longer of the two. If there's no crossroads, it's just a bit simpler. – Amadan Jul 07 '10 at 03:26
0

Here is my version if anyone needs it. Written in Typescript.

longestRoadLengthForPlayer(player: PlayerColors): number { let 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
}
Esqarrouth
  • 38,543
  • 21
  • 161
  • 168
-1

You could use Dijkstra and just change the conditions to choose the longest path instead.

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

It's efficient, but won't always find the path that in reality is shortest/longest although it will work most of the times.

martiert
  • 1,636
  • 2
  • 18
  • 23
  • 1
    For a board game where victory can hinge on who has the longest road, a chancy algorithm is probably not a good choice. – Borealid Jul 07 '10 at 07:59
  • True, depends if the feature as he calls it will be the factor which decides victory or not. If it's not something that will cause the player to win/fail, I would say that efficiency is more important than if it find the actual shortest path. – martiert Jul 07 '10 at 08:41