编程原理:解决迷宫


73

解决迷宫的可能方法有哪些?
我有两个主意,但我认为它们不是很优雅。

基本情况:我们有一个矩阵,该矩阵中的元素以代表迷宫的方式排序,一种进出一种迷宫。

我的第一个想法是让机器人从迷宫中穿过,直到一侧离开迷宫。我认为这是一个非常缓慢的解决方案。

第二个遍历每个标记为1的连续项,检查它可以去向何处(上,右,下,左),选择一种方法,并在该处继续其路径。这甚至比第一个慢。

当然,如果我在两个交叉点使两个机器人成为多线程,速度会更快一些,但这也不是最好的方法。

需要有更好的解决方案来使机器人通过迷宫。

编辑
第一:谢谢你的美好的答案!

我的问题的第二部分是:如果我们有多维图,该怎么办?是否有特殊的做法,或者贾斯汀·L。的答案是否也可以用于此?
我认为这不是解决此问题的最佳方法。

第三个问题:
哪种迷宫求解器算法最快?(完全假设)


7
我通常从END开始,然后往回走。它几乎每次都能工作!
乔·菲利普斯

1
您可以阅读cut-the-knot.org/ctk/Mazes.shtml,这是迷宫的一个不错的介绍
belisarius博士10年

6
计算机不知道起点是什么,终点是什么,从终点开始根本没有帮助。
多米尼克

1
@Dominique只是一个主观的观察:人类迷宫设计者倾向于在迷宫出口处仅绘制一个分支。这就是为什么通常是一个有点快,从终点搜索-不能与贾斯汀L. ASCII艺术的情况下,当然
博士贝利萨留

1
多维图也可以用树表示=)
Justin L.

Answers:


164

您可以将迷宫想象成一棵树。

     一种
    / \
   / \
  公元前
 ///
DEFG
   / \ \
  HIJ
 / \
LM
   / \
  ** O

(可能代表)

        开始
        + + --- + --- +
        | ACG |
    + --- + + + +
    | DB | F | J |
+ --- + --- + + --- + --- +
| LHEI |
+ --- + + --- + --- +
    | MO |
    + + --- +
    完

(忽略树上的左右顺序)

每个节点都是路径的交汇点。D,I,J,L和O是死胡同,**是目标。当然,在您的实际树中,每个节点都有可能拥有多达三个子节点。

现在,您的目标只是找到要遍历的节点以找到终点。任何其他的树搜索算法都可以。

查看树,只需从树的最深处的**中“追溯”即可很容易地找到正确的解决方案:

A B E H M **

请注意,当您的迷宫中有“圈”时,这种方法只会变得稍微复杂一点(即,如果可能的话,没有回溯,您可以重新输入已经遍历的段落)。检查评论,找到一个不错的解决方案。

现在,让我们看看您提到的应用于该树的第一个解决方案。

您的第一个解决方案基本上是“深度优先”搜索,这的确不错。实际上,这是一个很好的递归搜索。基本上,它说:“始终首先使用最右边的方法。如果没有任何东西,请回溯到可以直行或左行的位置,然后重复。

深度优先搜索将按以下顺序搜索上述树:

A B D (backtrack) E H L (backtrack) M ** (backtrack) O (backtrack thrice) I
(backtrack thrice) C F (backtrack) G J

请注意,一旦找到**,您就可以停止。

但是,当您实际编写深度优先搜索的代码时,使用递归编程可以使一切变得容易得多。即使是迭代方法也可以使用,并且您不必显式编程如何回溯。查看链接的文章以获取实现。

搜索树的另一种方法是“广度优先”解决方案,该解决方案按深度搜索树。它将按上述顺序搜索上面的树:

A (next level) B C (next level) D E F G (next level)
H I J (next level) L M (next level) ** O

请注意,由于迷宫的性质,广度优先的平均节点数量要多得多。广度优先是很容易实现的,它具有要搜索的路径队列,并且每次迭代都将路径从队列中弹出,通过在一步之后获取所有可以变为的路径,然后放置这些新路径来“爆炸”它。在队列末尾。没有明确的“下一级”命令可以编码,而这些命令只是为了帮助理解。

实际上,有很多搜索树的方法。我刚刚提到了两种最简单,最直接的方法。

如果您的迷宫非常非常长且很深,并且有环回和疯狂的声音并且很复杂,我建议您使用A *算法,这是行业标准的寻路算法,将广度优先搜索与启发式技术相结合...类似“智能广度优先搜索”。

它基本上是这样的:

  1. 将一条路径放在队列中(您仅一步步进入迷宫的路径)。路径具有“权重”,该权重由其当前长度+距端点的直线距离(可以通过数学计算)得出
  2. 从队列中弹出权重最低的路径。
  3. 一步之后,将路径“分解”为所有路径。(即,如果您的路径为“左右”,则您的分解路径为RLLRR和RLLRL,不包括穿过墙壁的非法路径)
  4. 如果这些路径之一有目标,那就胜利!除此以外:
  5. 计算爆炸路径的权重,并将所有权重放回队列(不包括原始路径)
  6. 按重量对队列进行排序,从最低到最低。然后从步骤2开始重复

这就是A *,我特别介绍了它,因为它或多或少是针对寻路的所有应用程序的行业标准寻路算法,包括从地图的一个边缘移至另一边缘,同时避开越野道路或山脉等。很好,因为它使用了最短的距离启发式算法,这使其具有“智能”。A *用途广泛,因为如果有任何问题,如果您有尽可能短的距离启发式可用方法(我们很容易-直线),则可以应用它。

但是请注意,A *不是您唯一的选择,这非常有价值。

实际上,树遍历算法维基百科类别仅列出了97个!(最好的仍然会在此页面的前面链接)

抱歉,长度= P(我倾向于漫步)


3
嘿,增加了ascii迷宫的乐趣。希望它有助于理解如何从迷宫中获取一棵树。
贾斯汀·L.2010年

5
@贾斯汀:好答案。如果迷宫有回圈,那么它将成为一个图形。如果您在迭代之前使用迭代的结构并使用单独的堆栈结构并检查节点的堆栈以防止循环,则仍可以像树一样遍历它。
安德烈·阿特斯

5
是的,这么长的时间没人会读我的答案嘘hoo
willoller 2010年

2
FWIW,自动程序死锁检查器实际上等效于图的深度优先搜索,其中分支表示不确定性。只是图的编码(程序)是非常微不足道的。
Donal Fellows 2010年

1
@JAB优先级队列绝对是一个不错的实现,因为抽象将负责排序和选择下一个节点。我所描述的基本上是一个用作优先级队列的队列。
贾斯汀·L.2010年


12

至少我发现很有趣的一种有趣的方法是使用细胞自动机。简而言之,被3个“墙”单元包围的“空间”单元变为“墙”单元。最后,仅剩下的空间单元是到达出口的通道。

如果您看一下贾斯汀在他的回答中插入的树,那么您会看到叶节点具有3面墙。修剪树直到有路径。


我更喜欢这种优雅的解决方案,它使我想起了Willoller发表的“死角填充算法”
Justin L. 2010年

5

这是我最喜欢的算法之一。

1) Move forward
2) Are you at a wall?
2a) If yes, turn left
3) Are you at the finish?
3a) If no, go to 1
3b) If yes, solved

8
如果整个迷宫都是向右转的走廊,那么该算法会发生什么?:)
Mike Sherov 2010年

啊!你将永远被困!
willoller 2010年

@迈克·S:“在广场上走来走去!”
Andre Artus 2010年

2
我认为作者正在尝试描述“墙跟随”,即您基本上只跟随左侧(或右侧)的墙。
加布

@Gabe,是的,CS课入门,我花了4个小时自行提出解决方案。围墙跟随并非没有警告或失败的地方……
Nate Noonen

4

如何从矩阵中构建图并使用广度优先搜索,深度优先搜索或Dijkstras算法?


3

这是在C ++中模拟迷宫的非常简单的表示形式:)

#ifndef vAlgorithms_Interview_graph_maze_better_h
#define vAlgorithms_Interview_graph_maze_better_h

static const int kMaxRows = 100;
static const int kMaxColumns = 100;

class MazeSolver
    {
private:
    char m_matrix[kMaxRows][kMaxColumns]; //matrix representation of graph
    int rows, cols; //actual rows and columns

    bool m_exit_found;
    int m_exit_row, m_exit_col;
    int m_entrance_row, m_entrance_col;

    struct square //abstraction for data stored in every verex
        {
        pair<int, int> m_coord; //x and y co-ordinates of the matrix
        square* m_parent; //to trace the path backwards

        square() : m_parent(0) {}
        };

    queue<square*> Q;

public:
    MazeSolver(const char* filename)
        : m_exit_found(false)
        , m_exit_row(0)
        , m_exit_col(0)
        , m_entrance_row(0)
        , m_entrance_col(0)
        {
        ifstream file;
        file.open(filename);

        if(!file)
            {
            cout << "could not open the file" << endl << flush;
            // in real world, put this in second phase constructor
            }
        init_matrix(file);
        }
    ~MazeSolver()
        {
        }
    void solve_maze()
        {
        //we will basically use BFS: keep pushing squares on q, visit all 4 neighbors and see
        //which way can we proceed depending on obstacle(wall)

        square* s = new square();
        s->m_coord = make_pair(m_entrance_row, m_entrance_col);

        Q.push(s);

        while(!m_exit_found && !Q.empty())
            {
            s = Q.front();
            Q.pop();

            int x = s->m_coord.first;
            int y = s->m_coord.second;
            //check if this square is an exit cell
            if(x == m_exit_row && y == m_exit_col)
                {
                m_matrix[x][y] = '>'; // end of the path
                m_exit_found = true;
                //todo: try breaking? no= queue wont empty
                }
            else
                {
                //try walking all 4 neighbors and select best path
                //NOTE: Since we check all 4 neighbors simultaneously,
                //      the path will be the shortest path
                walk_path(x-1, y, s);
                walk_path(x+1, y, s);
                walk_path(x, y-1, s);
                walk_path(x, y+1, s);
                }
            } /* end while */

        clear_maze(); //unset all previously marked visited shit

        //put the traversed path in maze for printing
        while(s->m_parent)
            {
            m_matrix[s->m_coord.first][s->m_coord.second] = '-';
            s = s->m_parent;
            } /* end while */
        }

    void print()
        {
        for(int i=0; i<rows; i++)
            {
            for(int j=0; j<cols; j++)
                cout << m_matrix[i][j];
            cout << endl << flush;
            }
        }

private:
    void init_matrix(ifstream& file)
        {
        //read the contents line-wise
        string line;
        int row=0;
        while(!file.eof())
            {
            std::getline(file, line);
            for(int i=0; i<line.size(); i++)
                {
                m_matrix[row][i] = line[i];
                }
            row++;
            if(line.size() > 0)
                {
                cols = line.size();
                }
            } /* end while */
        rows = row - 1;

        find_exit_and_entry();
        m_exit_found = false;
        }

    //find and mark ramp and exit points
    void find_exit_and_entry()
        {
        for(int i=0; i<rows; i++)
            {
            if(m_matrix[i][cols-1] == ' ')
                {
                m_exit_row = i;
                m_exit_col = cols - 1;
                }
            if(m_matrix[i][0] == ' ')
                {
                m_entrance_row = i;
                m_entrance_col = 0;
                }
            } /* end for */
        //mark entry and exit for testing
        m_matrix[m_entrance_row][m_entrance_col] = 's';
        m_matrix[m_exit_row][m_exit_col] = 'e';
        }

    void clear_maze()
        {
        for(int x=0; x<rows; x++)
            for(int y=0; y<cols; y++)
                if(m_matrix[x][y] == '-')
                    m_matrix[x][y] = ' ';
        }
        // Take a square, see if it's the exit. If not, 
        // push it onto the queue so its (possible) pathways
        // are checked.
    void walk_path(int x, int y, square* parent)
        {
        if(m_exit_found) return;
        if(x==m_exit_row && y==m_exit_col)
            {
            m_matrix[x][y] = '>';
            m_exit_found = true;
            }
        else
            {
            if(can_walk_at(x, y))
                {
                //tag this cell as visited
                m_matrix[x][y] = '-';

                cout << "can walk = " << x << ", " << y << endl << flush;

                //add to queue
                square* s = new square();
                s->m_parent = parent;
                s->m_coord = make_pair(x, y);
                Q.push(s);
                }
            }
        }

    bool can_walk_at(int x, int y)
        {
        bool oob = is_out_of_bounds(x, y);
        bool visited = m_matrix[x][y] == '-';
        bool walled = m_matrix[x][y] == '#';

        return ( !oob && !visited && !walled);
        }
    bool is_out_of_bounds(int x, int y)
        {
        if(x<0 || x > rows || y<0 || y>cols)
            return true;
        return false;
        }
    };


void run_test_graph_maze_better()
        {
        MazeSolver m("/Users/vshakya/Dropbox/private/graph/maze.txt");
        m.print();
        m.solve_maze();
        m.print();
        }


#endif

这个文件的格式是什么/Users/vshakya/Dropbox/private/graph/maze.txt
gaurus

2

只是一个主意。为什么不以蒙特卡洛的方式在其中扔一些机器人呢?让我们称第一代机器人gen0。我们只让gen0的机器人以这种方式保持连续行驶:
-从起点到某个点,
或者-从某个点到终点

我们在新的随机点中运行新的bot机器人gen1,然后尝试将gen1机器人与gen0机器人的道路连接起来,看看从头到尾是否有连续的道路。

因此,对于genn,我们尝试以gen0,gen1,...,genn-1形式连接机器人。

当然,一代只会持续一段有限的时间。

我不知道该算法的复杂性是否会证明对小数据集可行。
该算法还假设我们知道起点和终点。


一些好的想法网站:
http : //citeseerx.ist.psu.edu/
http://arxiv.org/


1
蒙特卡洛模拟最适合于在给定时间限制的情况下无法计算最佳解决方案但可接受部分解决方案的问题。迷宫搜索在有限的时间内可以解决,因此除非迷宫为1,000,000 x 1,000,000平方,否则我不会为这个问题推荐这种解决方案。
IceArdor 2014年

2

我的大学计算机之一也遇到了类似的问题。科学 培训班。我们想出的解决方案是沿左手墙移动(右手墙也将正常工作)。这是一些伪代码

While Not At End
    If Square To Left is open,
        Rotate Left
        Go Forward
    Else
        Rotate Right
    End If
Wend

基本上就是这样。复杂的部分是跟踪您面对的方向,并根据该方向找出您左侧的网格位置。它适用于我反对的任何测试用例。有趣的是,教授的解决方案大致如下:

While Not At End
    If Can Go North
        Go North
    ElseIf Can Go East
        Go East
    ElseIf Can Go South
        Go South
    ElseIf Can Go West 
        Go West
    EndIf
Wend

这对于大多数简单的迷宫效果很好,但在如下所示的迷宫中却无法使用:

SXXXXXXXXXXXXX
   X         X
   X         X
   X         X
 XXX         X
 X X         X
 X XXXXXXXXXXX     XXXE
 X                 X
 XXXXXXXXXXXXXXXXXXX

以S和E为起点和终点。

遇到任何不符合要求的事情,您最终都必须保存自己去过的地方的清单,以便在陷入困境时可以在必要时回溯,以免被抓住在一个循环中。如果您沿着墙走,则无需跟踪您去过的地方。尽管您找不到通过迷宫的最佳路径,但是您将始终通过它。


2
我怀疑您没有给出完整的算法,因为这一算法在入口处旋转/摆动。如果您尝试使用Wall Follower,请记住,如果出口在一个小岛内(被路径包围),它只会循环。
安德烈·阿特斯

2

如果机器人可以跟踪其位置,那么它就知道它是否曾经去过某个位置,那么深度优先搜索是显而易见的算法。您可以通过对抗性论证表明,最坏情况下的性能不可能比深度优先搜索更好。

如果您拥有机器人无法实现的技术,那么广度优先搜索可能会更好地解决许多迷宫问题,就像Dijkstra用于在图形中找到最短路径的算法一样。



1

有很多算法,并且有许多不同的设置指定哪种算法最好。这只是一个有趣的设置的想法:

假设您具有以下属性...

  • 您移动机器人,但要使其移动最小化,而不是使其CPU使用率最小化
  • 该机器人只能检查其相邻的单元格,也可以沿着走廊看是否看到交叉路口。
  • 它有GPS
  • 它知道目的地的坐标。

然后您可以设计一个AI ...

  • 绘制地图-每当它收到有关迷宫的新信息时。
  • 计算所有未观察到的位置(及其自身与目的地)之间的最小已知路径长度。
  • 可以根据周围结构优先安排未观察的位置进行检查。(如果仍然无法从那里到达目的地...)
  • 可以根据方向和到目的地的距离确定未观察位置的优先顺序以进行检查。
  • 可以根据收集信息的经验优先安排未观察的位置进行检查。(平均可以看到多远,它必须走多远?)
  • 可以优先处理未观察到的职位,以找到可能的捷径。(经验:有很多循环吗?)

0

此azkaban算法可能也对您有帮助,http: //journals.analysisofalgorithms.com/2011/08/efficiency-maze-solving-approach-with.html


0

解决迷宫的最佳方法是使用连接算法,例如Union-find,它是假设完成路径压缩的准线性时间算法。

Union-Find是一种数据结构,它告诉您集合中的两个元素是否可传递地连接。

为了使用联合查找数据结构来解决迷宫,首先使用邻居连接数据来构建联合查找数据结构。然后,联合查找被压缩。为了确定迷宫是否可解决,将入口和出口值进行比较。如果它们具有相同的值,则说明它们已连接且迷宫可解。最后,要找到解决方案,请从入口开始,然后检查与每个邻居相关联的根。一旦找到与当前单元格具有相同根的先前未曾访问过的邻居,便可以访问该单元格并重复该过程。

这种方法的主要缺点是,如果有多个路径,它不会告诉您通过迷宫的最短路径。


0

并非专门针对您的情况,但我遇到了几个编程竞赛问题,在这些问题中, Lee的算法非常容易快速编写代码。它并不是所有情况下最有效的,但是很容易解决。这里有一个我砍死了一个比赛。


请提供一些示例代码,而不仅仅是链接。谢谢!
JoelC '16

实际上,示例代码在我发布的github链接上:)
Jubin Chheda

谢谢朱宾。Stack Overflow的一般想法是链接可能变得无效,因此,为后代提供更完整/自包含的答案是一件好事!:)无论哪种方式,欢迎您!
JoelC
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.