用n球解决迷宫的通用算法

时间:2016-06-15 16:08:39

标签: algorithm conceptual

前几天我被问到" 概述了用n球解决迷宫的一般算法,其目标是将所有球放到迷宫中的给定位置(迷宫没有出口)&#34 ;.唯一的规则是算法必须有效(优于随机移动球)并且所有发出的命令都将影响所有球,因此一个球向北移动,如果它们没有被阻挡,所有其他球也将移动。

为此,我做了一些假设,即

  • 迷宫是标准的/完美的
  • 球不能相互阻挡
  • 球可以到达要求的位置
  • 命令会让球滚动直至击中墙壁
  • 命令只能是N / S / E / W
  • 迷宫是随机构造的,每次重置时球随机分布
  • 迷宫操作员
  • 可以立即观察到所有的迷宫

并且,使我的算法工作

  • 球不相同(例如,球上有数字或其他东西)
  • 迷宫操作员有照片记忆

鉴于此,我认为最好的想法是

  1. 随机(或以聪明的方式)移动球直到两个球到达目标位置
  2. 将路径从起始位置保存到最终位置。
  3. 确定那些球以及它们来自哪里,并为每一个球做1.
  4. "休息"在这个递归算法中,当所有的球都有办法到达给定目标时(我认为是O(log(n))递归?)

    这有用吗?有没有其他人有更好的算法呢?

    我有另一个想法,包括将所有球移动到相同的随机位置,然后将它们全部移动为一个球,但这似乎是一个更糟糕的算法。

    另一个想法是生成一个图形(图形理论),其中球的所有稳定点都是一个节点,移动将是一个边缘,但我不能看到它是如何做的&# 39;需要做很多蛮力。

2 个答案:

答案 0 :(得分:9)

我建议使用以下算法:

  1. 为迷宫创建数据结构,其中每个空闲单元格(正方形)已知以下内容:

    一个。坐标(行,列)
    湾4个移动的目标细胞(北,东,南,西)
    C。 b的反面:大理石可以来到这个细胞的细胞(如果有的话)。

  2. 从目标单元格开始执行BFS,使用一个假想的大理石执行反向移动,为每个访问的单元格指定从该单元格到达目标单元格所需的最少移动次数。请注意,某些单元格可能不会以这种方式访问​​,这意味着如果将大理石放置在那里,则无法通过执行合法移动将其移至目标单元格。这些单元格将获得归因于它们的无限距离(初始值)。

  3. 创建一个评估函数,该函数将为特定的大理石配置分配成本。建议的评估函数将总结由至少一个大理石占据的每个单元的距离的平方。通过取正方形,较高的距离将带来相对较高的成本,因此该算法将有利于改善具有最差位置的大理石位置的移动。此功能不会计算由多个大理石占据的单元格的两倍。这样,大理石共享一个单元格的配置受到青睐。

  4. 从起始位置,以评估成本生成4个可能的移动。按成本递增排序,按顺序执行DFS,递归重复此步骤。当成本变为零时,找到解决方案,并且在立即回溯递归期间,"路径"的动作返回。当成本无限时,搜索就会停止,然后尝试下一步......等等。

  5. 在搜索过程中,保留一份访问过的位置列表。当再次访问某个位置时,评估函数会给它一个无穷大的值,以便在发生这种情况时搜索会回溯。

  6. 以下是上述算法的JavaScript实现:

    
    
    "use strict";
    function createMaze(mazeStr) {
        var maze, lines, cell, row, ch, id, r, c, n, m;
        
        maze = {
            nodesRowCol: [],
            nodes: [],
            target: null,
            marbles: []
        };
        id = 0;
        lines = mazeStr.split("\n");
        for (r = 0; r < lines.length; r++) {
            maze.nodesRowCol[r] = row = [];
            for (c = 0; c < lines[r].length; c++) {
                ch = lines[r].charAt(c);
                if (ch !== '#') {
                    maze.nodes[id] = row[c] = cell = {
                        row: r,
                        col: c,
                        id: id++,
                        comeFrom: [],
                    };
                    // Keep track of target and marbles
                    if (ch === '*') maze.target = cell;
                    if (ch === '.') maze.marbles.push(cell);
                }
            }
        }
        // Add neighbours
        for (n = 0; n < maze.nodes.length; n++) {
            cell = maze.nodes[n];
            cell.neighbours = [
                maze.nodesRowCol[cell.row-1][cell.col], /* north */
                maze.nodesRowCol[cell.row][cell.col+1], /* east  */
                maze.nodesRowCol[cell.row+1][cell.col], /* south */
                maze.nodesRowCol[cell.row][cell.col-1]  /* west  */
            ];
        }
        // Add marble moves in two steps
        for (n = 0; n < maze.nodes.length; n++) {
            cell = maze.nodes[n];
            cell.moves = [ 
                cell.neighbours[0] ? cell.neighbours[0].moves[0] : cell, /* north */
                null,
                null,
                cell.neighbours[3] ? cell.neighbours[3].moves[3] : cell, /* west  */
            ];
        }
        for (n = maze.nodes.length - 1; n >= 0; n--) {
            cell = maze.nodes[n];
            cell.moves[1] = cell.neighbours[1] ? cell.neighbours[1].moves[1] : cell; /* west */
            cell.moves[2] = cell.neighbours[2] ? cell.neighbours[2].moves[2] : cell; /* south */
        }
        // add reverse-move ("marble can come from") data
        for (n = maze.nodes.length - 1; n >= 0; n--) {
            cell = maze.nodes[n];
            for (m = 0; m < 4; m++) {
                if (cell.moves[m] !== cell) cell.moves[m].comeFrom.push(cell);
            }
        }
        return maze;
    }
    
    function setDistances(maze) {
        var n, cell, distance, stack, newStack, i;
        // clear distance information
        for (n = 0; n < maze.nodes.length; n++) {
            maze.nodes[n].distance = Number.POSITIVE_INFINITY;
        }
        // set initial distance
        cell = maze.target;
        cell.distance = distance = 0;
        // BSF loop to set the distance for each cell that can be reached
        stack = cell.comeFrom.slice();
        while (stack.length) {
            distance++;
            newStack = [];
            for (i = 0; i < stack.length; i++) {
                cell = stack[i];
                if (distance < cell.distance) {
                    cell.distance = distance;
                    newStack = newStack.concat(cell.comeFrom);
                }
            }
            stack = newStack;
        }
    }
    
    function evaluatedPosition(position, visited) {
        // Assign heurstic cost to position
        var m, ids;
        
        position.cost = 0;
        ids = []; // keep track of marble positions
        for (m = 0; m < position.marbles.length; m++) {
            // If mulitple marbles are at same cell, only account for that cell once.
            // This will favour such positions: 
            if (ids[position.marbles[m].id] === undefined) { 
               // Make higher distances cost a lot, so that insentive
               // is to improve the position of the worst placed marble 
               position.cost += Math.pow(position.marbles[m].distance, 2);
               ids[position.marbles[m].id] = position.marbles[m].id;
            }
        }
        // Assign some unique string, identifying the marble configuration
        position.id = ids.join(','); 
        // If position was already visited before, give it the maximum cost
        if (visited[position.id]) position.cost = Number.POSITIVE_INFINITY;
        // Mark position as visited
        visited[position.id] = 1;
        return position;
    }
    
    function createMove(dir, marbles, visited) {
        var m, movedMarbles;
        
        movedMarbles = [];
        for (m = 0; m < marbles.length; m++) {
            movedMarbles[m] = marbles[m].moves[dir];
        }
        return evaluatedPosition({
            dir: dir,
            marbles: movedMarbles,
        }, visited);
    }
    
    function solve(maze) {
        var visited = {}; // nothing visited yet
        
        function recurse (position) {
            var ids, m, moves, i, path;
            if (position.cost == 0) return []; // marbles are all on target spot.
            if (!isFinite(position.cost)) return false; // no solution
            // get move list
            moves = [];
            for (i = 0; i < 4; i++) {
                moves[i] = createMove(i, position.marbles, visited);
            }
            // apply heuristic: sort the 4 moves by ascending cost
            moves.sort(function (a,b) { return a.cost - b.cost });
            for (i = 0; i < 4; i++) {
                //console.log('=== move === ' +  moves[i].dir);
                path = recurse(moves[i]);
                if (path !== false) return [moves[i].dir].concat(path);
            }
            return false; // no solution found
        }
        // Enrich initial position with cost, and start recursive search 
        return recurse(evaluatedPosition({
            marbles: maze.marbles
        }, visited));
    }
    
    
    // # = wall
    // * = target
    // . = marble
    
    var mazeStr = `
    ###########
    #   #   #*#
    # # #.#  .#
    # #.  #.# #
    # # # ### #
    #   #     #
    ###########
    `.trim();
    
    var maze = createMaze(mazeStr);
    setDistances(maze);
    console.log('#=wall, .=marble, *=target\n\n' + mazeStr);
    
    var moves = solve(maze);
    console.log('moves (0=north,1=east,2=south,3=west): ' + moves);
    &#13;
    &#13;
    &#13;

    找到的解决方案不一定是最佳的。它执行深度为1的评估。为了获得更好的解决方案,该算法可以在更深的位置进行评估。

答案 1 :(得分:0)

可以将迷宫和允许的移动建模为四个符号的字母上的deterministic finite automaton(DFA)。迷宫中的每个单元格都是DFA状态,每当发出命令s时,单元格x上的球将移动到单元格y时,单元格x就会在符号s上过渡到单元格y。

该算法分为三个阶段:

  1. 构造一个DFA,该DFA仅由迷宫中的任何球通过某些命令序列可以到达的状态组成。
  2. 找到DFA的任何synchronizing word。同步字或“复位字”是所有初始状态都以相同状态结尾的任何符号序列。请注意,我们实际上只需要一个单词即可同步所有球的初始状态,而不是DFA中的每个状态。
  3. 找到从复位字的结束状态到迷宫中目标位置的最短移动顺序。这可以使用任何最短路径算法来完成,例如breadth-first search(BFS)。

这需要一些解释。

首先,不是每个DFA都有一个复位字,但是,如果在步骤1中构造的DFA没有复位字,那么根据定义,没有命令序列可以将所有球都带到同一目标单元。因此,该算法将解决问题的所有可解决实例。

第二,找到最小长度的复位字是一个困难的问题,在最坏的情况下要花费指数时间。但是问题只说“算法必须有效(比随机移动球要好)”,所以任何复位字都可以。

构造复位字的最简单方法可能是对DFA的笛卡尔积及其自身使用广度优先搜索。对于具有n个状态的DFA,找到一个同步两个状态的字需要O(n²)时间。必须重复此操作最多k-1次,以同步球的k个初始状态,给出运行时间O(kn²)和长度为O(kn²)的复位字。


用通俗易懂的语言表示,此算法的最简单形式是使用BFS将两个球放入相同的位置,然后再次使用BFS将第三个球放入与两个相同的位置,依此类推,直到所有球在同一个地方。然后,它使用BFS一致地将它们全部带到目标。但是可以通过插入更好的重置字查找算法来改进该算法。通常,即使在最坏的情况下(也可以相信,但未经证实),也应该存在比n²符号短的复位字,这比kn²更好。