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.
... and is a slightly modified puzzle that does have a solution, as you can see here.
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":
Having additional logic for removing doors in the same room as valid choices when the path has just come from that room.
Producing a graph that isn't isomorphic to the input room configuration.
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))
}
}
}