通过矩形阵列路由“路径”

时间:2009-08-25 20:31:21

标签: algorithm language-agnostic graph puzzle

我正在尝试创建自己的益智游戏实现。

要创建我的游戏板,我需要遍历我的阵列中的每个方块一次且仅一次 遍历需要链接到相邻的邻居(水平,垂直或对角线)。

我正在使用以下形式的数组结构:

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

这种算法在一些中等大小的网格上花费的时间太长,因为早期的选择会删除不考虑的区域。

我想要的是一个函数,它将索引引入每个可能的路径,并返回填充该路径的数组。这将让我提供一个“种子”值来回到这个特定的董事会。

欢迎提出其他建议..

4 个答案:

答案 0 :(得分:1)

基本上,您希望构建一个Hamiltonian path:一个只访问图表每个节点一次的路径。

通常,即使您只想测试图形是否包含哈密尔顿路径,这已经是NP完全的。在这种情况下,显然图形包含至少一个哈密顿路径,并且图形具有良好的规则结构 - 但枚举所有哈密尔顿路径似乎仍然是一个难题。

汉密尔顿路径问题上的Wikipedia entry有一个随机算法,用于找到声称在大多数图上“快速”的哈密顿路径。它与您的算法的不同之处在于它围绕路径的整个分支进行交换,而不是仅通过一个节点进行回溯。这种更具“侵略性”的策略可能会更快 - 尝试并看看。

您可以让您的用户输入随机数生成器的种子来重建某个路径。你仍然不会列举所有可能的路径,但我想这对你的应用程序来说可能没有必要。

答案 1 :(得分:0)

这最初是对Martin B的帖子的评论,但它有所增长,所以我将其作为“答案”发布。

首先,列举所有可能的汉密尔顿路径的问题与#P复杂性类非常相似--- NP的可怕的哥哥。 Wikipedia一如既往地可以解释。出于这个原因,我希望你真的不需要知道每条路径,但最重要的是。如果没有,那么,如果您可以利用图表的结构,仍然有希望。

(这是一个有趣的方式,看看为什么枚举所有路径真的很难:让我们说你有10条路径的图表,你认为你已经完成。而邪恶的老我过来说“好吧,证明我没有第11条道路“。有一个可以说服我并且不需要花一点时间来证明的答案 - 那将是相当令人印象深刻的!证明不存在任何路径是co-NP,这也很难。证明只存在一定数量,这听起来很难。它在这里很晚,所以我有点模糊,但到目前为止我认为我所说的一切是对的。)

无论如何,我的google-fu在这个问题上似乎特别弱,这是令人惊讶的,所以我对你没有冷酷的答案,但我有一些提示?

  • 首先,如果每个节点最多有2个输出边,那么边数的数量是线性的,我很确定这意味着它是一个“稀疏”图形,通常更快乐地处理用。但汉密尔顿路径的边数是指数,所以没有真正简单的出路。 :)
  • 其次,如果图表的结构适用于此,则可以将问题分解为更小的块。我会想到Min-cutstrongly-connected-components。理想情况下,基本思路是发现你的大图实际上是4个较小的图,只有较小图之间的很少连接边。在那些较小的块上运行HamPath算法会快得多,并且希望将子图路径拼凑到整个图形路径中会有些容易。巧合的是,这些都是很好的绕口令。缺点是这些算法有点难以实现,需要大量思考和谨慎才能正确使用(在HamPaths的组合中)。此外,如果您的图表实际上是完全连接的,那么整个段落都是徒劳的! :)

所以这些只是一些想法。如果您的图表有任何其他方面(需要特定的起点,或特定的终点,或关于哪个节点可以连接到哪个的其他规则),它可能会非常有用! NP完全问题和P(快速)算法之间的界限通常非常薄。

希望这有帮助,-Agor。

答案 2 :(得分:0)

我将从一个通过网格的简单路径开始,然后在板上随机选择位置(使用输入种子来播种随机数生成器)并在可能的路径上执行“切换”。 e.g:

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

如果有足够的开关,你应该得到一个随机的路径。

答案 3 :(得分:0)

奇怪的是,我在我的数学学位上度过了一个本科生的暑期,研究这个问题以及提出解决它的算法。首先,我将评论其他答案。

Martin B:正确地将此确定为汉密尔顿路径问题。但是,如果图形是一个规则网格(正如你在评论中讨论的那样),那么可以轻易地找到哈密顿路径(例如,以行 - 主要顺序为例)。

agnorest:正确地谈到哈密尔顿路径问题属于一类难题。 agnorest还提到可能利用图形结构,这个结构非常有趣,在常规网格的情况下是微不足道的。

我现在将通过详细阐述您想要实现的想法来继续讨论。正如你在评论中提到的那样,找到某些类型的空间填充物是非常简单的。不相交"散步"在规则的格子/网格上。然而,似乎你不满足于这些,并希望找到一个发现"有趣"走路,随意覆盖你的网格。但在我探索这种可能性之前,我想指出一下“非相交”#34;这些散步的属性非常重要,是什么导致了枚举它们的困难。

事实上,我在暑期实习中学到的东西叫做Self Avoiding Walk。令人惊讶的是,对SAWs(自我避免行走)的研究对于物理建模的一些子域非常重要(并且是Manhattan project的关键成分!)您在问题中给出的算法实际上是一个变体在一个称为"增长"算法或Rosenbluth算法(以Marshal Rosenbluth以外的其他名称命名)。关于这种算法的一般版本(用于估计SAW的统计数据)以及它们与物理学的关系的更多细节可以在像thesis这样的文献中找到。

众所周知,二维SAW难以研究。关于2维SAW的理论结果很少。奇怪的是,在高于3维的情况下,您可以说SAW的大多数属性在理论上是已知的,例如它们的生长常数。 我只想说,二维SAW是非常有趣的事情!

在讨论中谈论你手头的问题,你可能会发现你的增长算法的实现得到了切断"切断了#34;非常频繁,无法扩展以填满整个格子。在这种情况下,将问题视为哈密尔顿路径问题更为合适。我找到有趣的哈密尔顿路径的方法是将问题表示为整数线性程序,并添加要在ILP中使用的固定随机边。随机边缘的固定将使生成过程具有随机性,并且ILP部分将有效地计算某些配置是否可行以及它们是否将返回解决方案。

守则

以下代码针对任意初始条件实现哈密顿路径或循环问题。它在具有4连接的常规2D晶格上实现。该公式遵循标准的子游览消除ILP拉格朗日。子游览被懒惰地消除,这意味着可能需要多次迭代。

你可以增加这个来满足你认为有趣的随机或其他初始条件"为你的问题。如果初始条件不可行,它会提前终止并打印出来。

此代码取决于NetworkXPuLP

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