7

There are a number of puzzles that are variant of the classic "7 Bridges of Konigsberg" puzzle where you must find a route through a set of rooms without ever using a door twice.

Here is an example of one without a solution. Test

... and is a slightly modified puzzle that does have a solution, as you can see here. Fake

I'm interested in a programmatic approach to solving these sort of problems and while there are a number of ways to determine that a particular configuration of rooms and doors has no solution, I am interested in computing the lists of doors to visit to solve the puzzle. One way to view the problem is to transform it's configuration into a graph and solve for the Hamiltonian. This sort of problem however requires tacking on inelegant logic because of the constraint that "U-Turns" are prohibited.

I hacked up a solution in a few minutes to show the problem. It is a brute-force solution that puts the "rooms" into groups, with the added invariant that you cannot move from one "door" to another "door" in the same room (as that would entail doing a U-Turn).

I feel that there must be a better abstraction for representing this problem that does not resort to the following "tricks":

  1. Having additional logic for removing doors in the same room as valid choices when the path has just come from that room.

  2. Producing a graph that isn't isomorphic to the input room configuration.

  3. Filtering all configurations that don't satisfy the U-turn constraint. (Variant of #1)

Is there an existing body of literature tackling these sort of problems, and if so, what are their conclusions? Are room problems fundamentally at odds with the methods employed by the most well-known graph algorithms such that it requires this special logic? If there is a better solution that isn't a transformation to a graph, I'd love to hear about that as well.

Here's the existing code that does work, the groups represent the first problem, the groups that are commented out represent the latter problem.:

// I renamed "groups" to rooms to make the code more clear.
var rooms = {
    1: ['A','B','C','D'],
    //1: ['A','B','C','D','P'],
    2: ['E', 'D', 'F', 'G'],
    3: ['F','I','J','H'],
    //3: ['F','I','P','J', 'H'],
    4: ['I', 'M', 'N', 'O'],
    5: ['C','J','M','L','K'],
    OUTER: ['A', 'B', 'E', 'G', 'H', 'O', 'N', 'L', 'K']
}

class Graph {
    constructor(rooms) {
        // This is a map of a door letter to the rooms (rooms) that it belongs to.
        this.roomKey = {};
        // The total number of doors
        this.totalNodes = 0;
        this.rooms = rooms;
        // This is only used to produce the number of rooms, but remains in case
        // I need to adapt the algorithm for the classical approach.
        this.vertices = {};
        for (var key in rooms) {
            this.addRoom(key, rooms[key]);
        }
    }

    addRoom(roomName, elements) {
        for (var from of elements) {
            if (!this.roomKey[from]) {
                // initialize
                this.roomKey[from] = [roomName]
            } else {
                this.roomKey[from].push(roomName)
            }
            for (var to of elements) {
                // it doesn't make sense to add a vertex to yourself
                if (from === to) continue
                // otherwise add the vertex
                this.addDoor(from, to)
            }
        }
    }

    addDoor(name, edge) {
        // initialize if empty
        if (!this.vertices[name]) {
            this.vertices[name] = []
            this.totalNodes++
        }

        if (this.vertices[name].indexOf(edge) != -1) {
            console.log(`${name} already has this edge: ${edge}`)
        } else {
            this.vertices[name] = this.vertices[name].concat(edge)
        }
    }

    hamiltonian(current, prevRoom, used) {
        // Find the rooms that this connects to
        var kpossible = this.roomKey[current]

        // Find the rooms that connect to this door, but filter those that are
        // in the room we just came from, this is the hacky part.
        var possibleRoom = kpossible.find((room) => room !== prevRoom)
        // Produce all possible rooms, but if we've already been to a room, remove it.
        var possibleDoors = this.rooms[possibleRoom].filter((elt) => used.indexOf(elt) == -1)

        if (used.length == this.totalNodes) {
            console.log("success!", used)
            return;
        }

        // No more possible rooms, this path is no good.
        if (!possibleDoors || possibleDoors.length === 0)
            return;

        for(var door of possibleDoors) {
            this.hamiltonian(door, possibleRoom, used.concat(door))
        }
    }
}

The doors are labeled as follows: Labeled Doors

zetavolt
  • 2,989
  • 1
  • 23
  • 33
  • It sounds like `addVertex` should be named `addEdge` – Bergi May 18 '16 at 02:45
  • I don't get what these "groups" are good for. Shouldn't each room be a node (vertex) and each door be an edge of your graph? – Bergi May 18 '16 at 02:46
  • Are you adding the doors as the vertices? – br3nt May 18 '16 at 02:59
  • @Bergi - Groups are "rooms", and yes, it would be more aptly named `addEdge`. It was done in > 15 minutes and the concept for solving the problem changed once within that timeframe. – zetavolt May 18 '16 at 03:07
  • @br3nt - Yes, the doors are vertices. – zetavolt May 18 '16 at 03:07
  • @ZephyrPellerin: Hm, I still don't get what groups are (what `addGroup` does and how this `groupKey` datastructure works). Also, if every node is one room and every edge is one door, wouldn't you try to find an eulerian graph not a hamiltonian one? – Bergi May 18 '16 at 03:12
  • 1
    @Bergi, the groups represent the rooms. Each door is labelled. Two rooms that have the same labelled door are connected. – br3nt May 18 '16 at 03:20
  • @Bergi: I've edited the post with labeled vertices (doors) if it helps, the groups are numbered counter-clockwise. – zetavolt May 18 '16 at 03:27
  • Oh wait, I begin to understand now. That `groupKey` is a list of your (door) edges, and your `vertices` are actually edges of the [line graph](https://en.wikipedia.org/wiki/Line_graph) (that connects the doors) in which you are going to find a hamiltonian path. – Bergi May 18 '16 at 03:30
  • 1
    Sorry if I'm being a turd by asking this, but did you check this out yet? http://www.cim.mcgill.ca/~sveta/COMP102/Lecture17-4.pdf – kayleeFrye_onDeck May 18 '16 at 04:03
  • 1
    @kayleeFrye_onDeck Just from a quick perusal, none of those answer the question – zetavolt May 18 '16 at 04:22
  • My immediate thought - if there are more than two rooms with an odd number of doors (including the exterior "room") then there cannot be a path (since you can start/end in rooms with odd numbers of doors but if there are any in the intermediate steps of the path then you will reach a point where you enter a room through the final odd door and there is no available exit). – MT0 May 18 '16 at 09:02
  • @MT0: There are a number of ways to show the (non)existence of a solution. The method you just described is very close to Fleury's algorithm as Bergi has pointed out. However, I'm interested in *finding* the path. br3nt has given a good answer but I'd be interested in more thoughts from the community. – zetavolt May 18 '16 at 20:29

1 Answers1

5

As you stated a door can only be used once.

I would represent the data as an adjacency list with the following properties:

  • Each room is a vertex
  • Outside is a vertex
  • Each door is a bi-directional edge
  • Any room can have multiple doors going to any other room or to the Outside

Then you would follow each edge only once.

In order to convert your data structure into an adjacency list I would do the following:

  • Collect all the labels for each door into an array
  • For each door label, find the two connecting rooms
  • Add the two rooms as an entry in the adjacency list

Something like this will build the adjacency list from the data structure that you already have:

var groups = {
    1: ['A','B','C','D','P'],
    2: ['E', 'D', 'F', 'G'],
    3: ['F','I','P','J', 'H'],
    4: ['I', 'M', 'N', 'O'],
    5: ['C','J','M','L','K'],
    OUTER: ['A', 'B', 'E', 'G', 'H', 'O', 'N', 'L', 'K']
}

var edges = [];
var adjacency_list = [];

// collect all the doors
for (var room in groups) {
  doors = groups[room];
  for (var door of doors) {
    if (edges.indexOf(door) < 0) {
      edges.push(door); // mark off this door
    }
  }
}

// find the connections between the rooms (build the adjacency matrix)
for (var door of edges) {
  rooms = [];

  // find the two rooms that this door connects
  for (var room in groups) {
    doors = groups[room];
    if (doors.indexOf(door) > 0) {
      rooms.push(room);
    }
  }

  // add these as an edge in our adjacency list
  if (rooms.length == 2) {
    adjacency_list.push(rooms);
  }
  else {
    //TODO: raise an error as the rooms aren't connected properly
  }
}

Now, adjacency_list is a list of edges that you can use to traverse between rooms. There will be one edge per door connecting two rooms. If you traverse an edge (go through a door), then it must be removed (or marked) so that you do not traverse it (go through the door) again.

br3nt
  • 9,017
  • 3
  • 42
  • 63
  • 2
    See also https://en.wikipedia.org/wiki/Eulerian_path#Constructing_Eulerian_trails_and_circuits :-) – Bergi May 18 '16 at 04:46
  • I think this is a more concise and intuitive variant of the algorithm, I'll accept this answer unless someone shows up with something totally unexpected. – zetavolt May 18 '16 at 20:33