Канонический алгоритм для задач "посетить каждую дверь один раз"

есть ряд головоломок, которые являются вариантом классической головоломки" 7 мостов Кенигсберга", где вы должны найти маршрут через набор комнат, никогда не используя дверь дважды.

вот пример одного без решения. Test

... и это слегка измененная головоломка, что тут есть решение, как вы можете видеть здесь. Fake

меня интересует программный подход к решению такого рода проблемы, и хотя есть несколько способов определить, что конкретная конфигурация комнат и дверей не имеет решения, я заинтересован в вычислении списков дверей для посещения, чтобы решить головоломку. Один из способов просмотра проблемы-преобразовать ее конфигурацию в граф и решить для гамильтониана. Однако такого рода проблема требует лавирования на неэлегантной логике из-за ограничения, что "развороты" запрещены.

я взломал решение через несколько минуты, чтобы показать проблему. Это решение грубой силы, которое ставит " комнаты "в группы, с добавленным инвариантом, что вы не можете двигаться от одной" двери "к другой" двери " в той же комнате (поскольку это повлечет за собой поворот).

Я чувствую, что должна быть лучшая абстракция для представления этой проблемы, которая не прибегает к следующим "трюкам":

  1. иметь дополнительную логику для извлекать двери в такой же комнате как действительные выборы когда путь только что из этой комнаты.

  2. создание графика, который не изоморфен конфигурации входной комнаты.

  3. фильтрация всех конфигураций,которые не удовлетворяют ограничению разворота. (Вариант #1)

требует это специальная логика? Если есть лучшее решение, это не переход на график, я хотел бы услышать об этом.

вот существующий код, который работает, группы представляют первую проблему, группы, которые комментируются, представляют последнюю проблему.:

// 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))
        }
    }
}

двери обозначены следующим образом: Labeled Doors

1 ответов


как вы заявили, дверь может использоваться только один раз.

Я бы представил данные как список смежности со следующими свойствами:

  • каждая комната является вершиной
  • Outside - это вершина
  • каждая дверь двунаправленный край
  • любая комната может иметь несколько дверей, зайдя в любую другую комнату или на улицу

тогда вы будете следовать каждому краю только один раз.

для преобразования ваших данных структура в список смежности я бы сделал следующее:

  • собрать все метки для каждой двери в массив
  • для каждой дверной этикетки найдите две смежные комнаты
  • добавить две комнаты в качестве записи в списке смежности

что-то вроде этого построит список смежности из структуры данных, которая у вас уже есть:

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
  }
}

Теперь adjacency_list список ребер, которые можно использовать для прохода между комнатами. Будет один край на дверь, соединяющую две комнаты. Если вы пересекаете край (проходите через дверь), то он должен быть удален (или отмечен), чтобы вы не пересекали его (проходите через дверь) снова.