Маршрутизация путей через прямоугольный массив

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

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

Я использую структуру массива формы:

board[n,m] = byte
Each bit of the byte represents a direction 0..7 and exactly 2 bits are always set
Directions are numbered clockwise
0 1 2 
7 . 3
6 5 4 
Board[0,0] must have some combination of bits 3,4,5 set

Мой текущий подход к построению случайного пути:

 Start at a random position
 While (Squares remain)
    if no directions from this square are valid, step backwards
    Pick random direction from those remaining in bitfield for this square       
    Erase the direction to this square from those not picked
    Move to the next square

Этот алгоритм занимает слишком много времени на некоторых сетках среднего размера, потому что более ранние варианты исключают области из рассмотрения.

Я хотел бы иметь функцию, которая принимает индекс для каждого возможного пути и возвращает массив, заполненный для этого пути. Это позволило бы мне указать «начальное» значение, чтобы вернуться к этой конкретной доске.

Приветствуются другие предложения..


person Community    schedule 25.08.2009    source источник
comment
Если я правильно понимаю, вы хотите построить путь в сетке так, чтобы каждый элемент посещался ровно один раз?   -  person Charlie Salts    schedule 26.08.2009
comment
да. Каждый элемент посетил ровно один раз, и пробелов нет.   -  person    schedule 26.08.2009
comment
Я подозреваю, что вы наткнулись на сложную проблему. Единственный совет, который я могу дать, — попытаться обнаружить и избежать ситуаций, когда вы блокируете регионы. Если бы вы делали это вручную, вы бы инстинктивно «приклеивались» к существующим заполненным областям, чтобы избежать создания таких заблокированных областей.   -  person Charlie Salts    schedule 26.08.2009


Ответы (4)


По сути, вы хотите построить гамильтоновский путь: путь, который точно посещает каждый узел графа. однажды.

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

запись в Википедии о задаче о гамильтоновом пути содержит рандомизированный алгоритм поиска заявленного гамильтонова пути. быть «быстрым на большинстве графиков». Он отличается от вашего алгоритма тем, что он меняет местами всю ветвь пути вместо возврата только на один узел. Эта более «агрессивная» стратегия может быть быстрее — попробуйте и убедитесь.

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

person Martin B    schedule 25.08.2009
comment
Да, но я хочу либо построить один, либо изменить существующий, чтобы увеличить разнообразие. Существуют как тривиальные способы обхода двумерного массива, так и некоторые способы «заполнения пространства». Разве гамильтонова сложность не связана с ограничениями соединения? - person ; 26.08.2009
comment
Я думаю, что неправильно понял вашу проблему. Я думал, что ваша структура массива была входными данными, определяющими допустимые направления, в которых вы можете оставить квадрат, то есть квадрат не обязательно связан со всеми своими соседями. Но теперь я понимаю, что это ваш выход, т.е. сгенерированный путь, верно? Это упрощает задачу, потому что граф известен и имеет регулярную структуру — и, как вы говорите, существование гамильтоновых путей очевидно. Тем не менее, я думаю, что перечисление всех гамильтоновых путей в этом графе, вероятно, все еще является сложной задачей. - person Martin B; 26.08.2009
comment
Отредактировал ответ теперь, когда я понял, что вы пытаетесь сделать. - person Martin B; 26.08.2009

Первоначально это был комментарий к сообщению Мартина Б., но он немного вырос, поэтому я публикую его как «ответ».

Во-первых, проблема перечисления всех возможных гамильтоновых путей очень похожа на класс сложности #P — ужасающий старший брат NP. Википедия, как всегда, может объяснить. Именно по этой причине я надеюсь, что вам действительно нужно знать не каждый путь, а только большинство. Если нет, то все еще есть надежда, что вы сможете использовать структуру ваших графиков.

(Вот забавный способ взглянуть на то, почему перечисление всех путей действительно сложно: скажем, у вас есть 10 путей для графа, и вы думаете, что закончили. И злой старый я подходит и говорит: «Ну, докажи мне, что есть не является 11-м путем". Ответ, который мог бы убедить меня и не требовал времени для демонстрации --- это было бы довольно впечатляюще! Доказательство того, что не существует какого-либо пути, -НП, и это тоже довольно сложно. Доказать, что существует только определенное число, это звучит очень сложно. Уже поздно, поэтому я немного запутался, но пока я думаю, что все, что я сказал, правильно. )

В любом случае, мой гугл-фу кажется особенно слабым по этому вопросу, что удивительно, поэтому у меня нет для вас холодного ответа, но у меня есть некоторые намеки?

  • Во-первых, если каждый узел имеет не более 2 исходящих ребер, то количество ребер линейно зависит от количества вершин, и я почти уверен, что это означает, что это «разреженный» граф, с которым обычно удобнее иметь дело. Но гамильтонов путь экспоненциален по количеству ребер, так что простого выхода нет. :)
  • Во-вторых, если структура вашего графика позволяет это сделать, можно разбить задачу на более мелкие фрагменты. Min-cut и сильно-связанные-компоненты. Основная идея, в идеале, заключалась бы в том, чтобы обнаружить, что ваш большой граф на самом деле состоит из 4 меньших графов с несколько соединяющими ребрами между меньшими графами. Запуск вашего алгоритма HamPath на этих меньших фрагментах был бы значительно быстрее, и можно было бы надеяться, что будет несколько легко собрать воедино пути подграфа в пути всего графа. Кстати, это отличные скороговорки. Недостатком является то, что эти алгоритмы немного сложны в реализации и требуют много размышлений и осторожности для правильного использования (при объединении HamPath). Кроме того, если ваши графы на самом деле достаточно хорошо связаны, весь этот абзац был напрасным! :)

Так что это были всего лишь несколько идей. Если есть какой-либо другой аспект вашего графика (требуется конкретная начальная точка, или конкретная конечная точка, или дополнительные правила о том, какой узел может соединяться с каким), это может быть весьма полезным! Граница между NP-полными задачами и P (быстрыми) алгоритмами часто довольно тонка.

Надеюсь, это поможет, -Агор.

person agorenst    schedule 26.08.2009

Я бы начал с тривиального пути через сетку, затем случайным образом выбрал места на доске (используя входное начальное число для заполнения генератора случайных чисел) и выполнил «переключения» на пути, где это возможно. например.:

------  to:    --\/--
------         --/\--

С достаточным количеством переключателей вы должны получить случайный путь.

person jcd    schedule 13.09.2009

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

Мартин Б.: Правильно определил, что это проблема гамильтоновых путей. Однако, если граф представляет собой обычную сетку (как вы двое обсуждали в комментариях), то гамильтонов путь может быть найден тривиально (например, извилистый порядок строк).

agnorest: Правильно сказано о том, что проблема гамильтоновых путей относится к классу сложных задач. agnorest также упомянул о возможном использовании структуры графа, что, как ни странно, тривиально в случае регулярной сетки.

Теперь я продолжу обсуждение, уточнив, что, по моему думаю, вы пытаетесь достичь. Как вы упомянули в комментариях, тривиально найти определенные классы «заполняющих пространство» непересекающихся «прогулок» на регулярной решетке/сетке. Однако, похоже, вы не удовлетворены только этим и хотели бы найти алгоритм, который находит «интересные» прогулки, которые случайным образом покрывают вашу сетку. Но прежде чем я исследую эту возможность, я хотел бы указать, что свойство «непересекаемости» этих блужданий чрезвычайно важно и вызывает трудности при их перечислении.

На самом деле, вещь, которую я изучал во время летней стажировки, называется самоизбегающая прогулка. . Удивительно, но изучение SAW (самоизбегающих прогулок) очень важно для нескольких подобластей моделирования в физике (и было ключевым компонентом Манхэттенский проект!) Алгоритм, который вы дали в своем вопросе, на самом деле является вариантом алгоритма, известного как алгоритм "роста" или алгоритм Розенблюта (названный не иначе как Маршал Розенблют). Более подробную информацию как об общей версии этого алгоритма (используемой для оценки статистики ПАВ), так и об их отношении к физике можно легко найти в литературе, подобной этой тезис.

Общеизвестно, что ПАВ в двух измерениях трудно изучать. Известно очень мало теоретических результатов о ПАВ в 2 измерениях. Как ни странно, в измерениях выше 3 можно сказать, что большинство свойств ПАВ известны теоретически, например, их постоянная роста. Достаточно сказать, что ПАВ в 2-х измерениях — это очень интересно!

Доминируя в этом обсуждении, чтобы поговорить о вашей проблеме, вы, вероятно, обнаружите, что ваша реализация алгоритма роста довольно часто «обрезается» и не может расширяться, чтобы заполнить всю вашу решетку. В этом случае более уместно рассматривать вашу проблему как проблему гамильтонового пути. Мой подход к поиску интересных гамильтоновых путей состоял бы в том, чтобы сформулировать задачу как целочисленную линейную программу и добавить фиксированные случайные ребра для использования в ILP. Фиксация случайных ребер придаст случайность процессу генерации, а часть ILP будет эффективно вычислять, допустимы ли определенные конфигурации, и если да, то возвращать решение.

Код

Следующий код реализует гамильтонову задачу о пути или цикле для произвольных начальных условий. Он реализует его на обычной двумерной решетке с 4-связностью. Формулировка следует стандартному лагранжиану ILP с исключением субтуров. Вложенные туры удаляются лениво, что означает, что потенциально может потребоваться много итераций.

Вы можете увеличить это, чтобы удовлетворить случайные или иные начальные условия, которые вы считаете «интересными» для вашей проблемы. Если начальные условия невыполнимы, он завершается досрочно и печатает это.

Этот код зависит от NetworkX и PuLP.

"""
Hamiltonian path formulated as ILP, solved using PuLP, adapted from 

https://projects.coin-or.org/PuLP/browser/trunk/examples/Sudoku1.py

Authors: ldog
"""

# Import PuLP modeler functions
from pulp import *

# Solve for Hamiltonian path or cycle
solve_type = 'cycle'

# Define grid size
N = 10

# If solving for path a start and end must be specified
if solve_type == 'path':
    start_vertex = (0,0)
    end_vertex = (5,5)

# Assuming 4-connectivity (up, down, left, right)
Edges = ["up", "down", "left", "right"]
Sequence = [ i for i in range(N) ]

# The Rows and Cols sequences follow this form, Vals will be which edge is used
Vals = Edges
Rows = Sequence
Cols = Sequence

# The prob variable is created to contain the problem data        
prob = LpProblem("Hamiltonian Path Problem",LpMinimize)

# The problem variables are created
choices = LpVariable.dicts("Choice",(Vals,Rows,Cols),0,1,LpInteger)

# The arbitrary objective function is added
prob += 0, "Arbitrary Objective Function"

# A constraint ensuring that exactly two edges per node are used 
# (a requirement for the solution to be a walk or path.)
for r in Rows:
    for c in Cols:
        if solve_type == 'cycle':
            prob += lpSum([choices[v][r][c] for v in Vals ]) == 2, ""
        elif solve_type == 'path':
            if (r,c) == end_vertex or (r,c) == start_vertex:
                prob += lpSum([choices[v][r][c] for v in Vals]) == 1, ""
            else:
                prob += lpSum([choices[v][r][c] for v in Vals]) == 2, ""

# A constraint ensuring that edges between adjacent nodes agree
for r in Rows:
    for c in Cols:
        #The up direction
        if r > 0:
            prob += choices["up"][r][c] == choices["down"][r-1][c],""
        #The down direction
        if r < N-1:
            prob += choices["down"][r][c] == choices["up"][r+1][c],""
        #The left direction
        if c > 0:
            prob += choices["left"][r][c] == choices["right"][r][c-1],""
        #The right direction
        if c < N-1:
            prob += choices["right"][r][c] == choices["left"][r][c+1],""

# Ensure boundaries are not used
for c in Cols:
    prob += choices["up"][0][c] == 0,""
    prob += choices["down"][N-1][c] == 0,""
for r in Rows:
    prob += choices["left"][r][0] == 0,""
    prob += choices["right"][r][N-1] == 0,""

# Random conditions can be specified to give "interesting" paths or cycles 
# that have this condition.
# In the simplest case, just specify one node with fixed edges used.
prob += choices["down"][2][2] == 1,""
prob += choices["up"][2][2] == 1,""

# Keep solving while the smallest cycle is not the whole thing
while True:
    # The problem is solved using PuLP's choice of Solver
    prob.solve()

    # The status of the solution is printed to the screen
    status = LpStatus[prob.status]
    print "Status:", status
    if status == 'Infeasible':
        print 'The set of conditions imposed are impossible to solve for. Change the conditions.'
        break

    import networkx as nx
    g = nx.Graph()
    g.add_nodes_from([i for i in range(N*N)])

    for r in Rows:
        for c in Cols:
            if value(choices['up'][r][c])  == 1:
                nr = r - 1
                nc = c
                g.add_edge(r*N+c,nr*N+nc)
            if value(choices['down'][r][c])  == 1:
                nr = r + 1
                nc = c
                g.add_edge(r*N+c,nr*N+nc)
            if value(choices['left'][r][c])  == 1:
                nr = r
                nc = c - 1
                g.add_edge(r*N+c,nr*N+nc)
            if value(choices['right'][r][c])  == 1:
                nr = r
                nc = c + 1
                g.add_edge(r*N+c,nr*N+nc)

    # Get connected components sorted by length
    cc = sorted(nx.connected_components(g), key = len)

    # For the shortest, add the remove cycle condition
    def ngb(idx,v):
        r = idx/N
        c = idx%N
        if v == 'up':
            nr = r - 1
            nc = c
        if v == 'down':
            nr = r + 1
            nc = c
        if v == 'left':
            nr = r 
            nc = c - 1
        if v == 'right':
            nr = r 
            nc = c + 1
        return nr*N+c

    prob += lpSum([choices[v][idx/N][idx%N] for v in Vals for idx in cc[0] if ngb(idx,v) in cc[0] ]) \
            <= 2*(len(cc[0]))-1,  ""

    # Pretty print the solution
    if len(cc[0]) == N*N:
        print ''
        print '***************************'
        print ' This is the final solution'
        print '***************************'
    for r in Rows:
        s = ""
        for c in Cols:
            if value(choices['up'][r][c])  == 1:
                s += " | "
            else:
                s += "   "
        print s
        s = ""
        for c in Cols:
            if value(choices['left'][r][c])  == 1:
                s += "-"
            else:
                s += " "
            s += "X"
            if value(choices['right'][r][c])  == 1:
                s += "-"
            else:
                s += " "
        print s
        s = ""
        for c in Cols:
            if value(choices['down'][r][c])  == 1:
                s += " | "
            else:
                s += "   "
        print s

    if len(cc[0]) != N*N:
        print 'Press key to continue to next iteration (which eliminates a suboptimal subtour) ...'
    elif len(cc[0]) == N*N:
        print 'Press key to terminate'
    raw_input()

    if len(cc[0]) == N*N:
        break
person ldog    schedule 15.11.2017