【问题标题】:Rush Hour - Solving the game高峰时间 - 解决游戏
【发布时间】:2021-04-14 04:13:52
【问题描述】:

高峰时间
如果您不熟悉它,该游戏由一系列大小不一的汽车组成,水平或垂直设置在一个 NxM 网格上,只有一个出口。
只要另一辆车没有挡住它,每辆车都可以按照它设定的方向前进/后退。您可以永远改变汽车的方向。
有一辆专用车,通常是红色的。它设置在出口所在的同一排,游戏的目标是找到一系列动作(一个动作 - 将汽车向后或向前移动 N 步),这将使红色汽车驶出迷宫.

我一直在想如何通过计算来解决这个问题,但我真的想不出任何好的解决方案。
我想出了几个:

  1. 回溯。这很简单——递归和更多的递归,直到你找到答案。但是,每辆车都可以通过几种不同的方式移动,并且在每个游戏状态下,可以移动几辆车,由此产生的游戏树将非常庞大。
  2. 某种约束算法将考虑需要移动的内容,并以某种方式递归工作。这是一个非常粗略的想法,但它是一个想法。
  3. 图表?将游戏状态建模为图形并在着色算法上应用某种变化来解决依赖关系?同样,这是一个非常粗略的想法。
  4. 一位朋友建议使用遗传算法。这是可能的,但并不容易。我想不出制作评估函数的好方法,否则我们将一无所有。

所以问题是 - 如何创建一个程序,该程序采用网格和车辆布局,并输出将红色汽车开出所需的一系列步骤?

子问题:

  1. 寻找一些解决方案。
  2. 寻找最佳解决方案(最少移动次数)
  3. 评估当前状态的好坏

示例:如何在此设置中移动汽车,以便红色汽车可以通过右侧的出口“退出”迷宫?

(来源:@987654322 @)

【问题讨论】:

  • while(!win){selectRandomCar().moveRandomWay()};
  • 我小时候就喜欢这个游戏。我的算法:将汽车推过边界或将挡住板路的汽车扔出。我赢了。
  • 另外一个相关的问题是证明随机生成的汽车配置实际上是可以解决的。
  • 这种问题不适合用Prolog表达吗?我绝不是这方面的专家,所以这只是一个猜测。
  • @Rubys 该 API 使用什么语言,它是开源的吗? :)

标签: algorithm language-agnostic artificial-intelligence


【解决方案1】:

对于经典的 Rush Hour,这个问题很容易通过简单的广度优先搜索来解决。声称的最难的已知初始配置需要 93 次移动来解决,总共只有 24132 个可达配置。即使是一个简单实现的广度优先搜索算法,即使在一台普通的机器上也能在 1 秒内探索整个搜索空间。

参考文献


Java 求解器

这是广度优先搜索穷举求解器的完整源代码,用 C 风格编写。

import java.util.*;

public class RushHour {
    // classic Rush Hour parameters
    static final int N = 6;
    static final int M = 6;
    static final int GOAL_R = 2;
    static final int GOAL_C = 5;

    // the transcription of the 93 moves, total 24132 configurations problem
    // from http://cs.ulb.ac.be/~fservais/rushhour/index.php?window_size=20&offset=0
    static final String INITIAL =   "333BCC" +
                                    "B22BCC" +
                                    "B.XXCC" +
                                    "22B..." +
                                    ".BB.22" +
                                    ".B2222";

    static final String HORZS = "23X";  // horizontal-sliding cars
    static final String VERTS = "BC";   // vertical-sliding cars
    static final String LONGS = "3C";   // length 3 cars
    static final String SHORTS = "2BX"; // length 2 cars
    static final char GOAL_CAR = 'X';
    static final char EMPTY = '.';      // empty space, movable into
    static final char VOID = '@';       // represents everything out of bound

    // breaks a string into lines of length N using regex
    static String prettify(String state) {
        String EVERY_NTH = "(?<=\\G.{N})".replace("N", String.valueOf(N));
        return state.replaceAll(EVERY_NTH, "\n");
    }

    // conventional row major 2D-1D index transformation
    static int rc2i(int r, int c) {
        return r * N + c;
    }

    // checks if an entity is of a given type
    static boolean isType(char entity, String type) {
        return type.indexOf(entity) != -1;
    }

    // finds the length of a car
    static int length(char car) {
        return
            isType(car, LONGS) ? 3 :
            isType(car, SHORTS) ? 2 :
            0/0; // a nasty shortcut for throwing IllegalArgumentException
    }

    // in given state, returns the entity at a given coordinate, possibly out of bound
    static char at(String state, int r, int c) {
        return (inBound(r, M) && inBound(c, N)) ? state.charAt(rc2i(r, c)) : VOID;
    }
    static boolean inBound(int v, int max) {
        return (v >= 0) && (v < max);
    }

    // checks if a given state is a goal state
    static boolean isGoal(String state) {
        return at(state, GOAL_R, GOAL_C) == GOAL_CAR;
    }

    // in a given state, starting from given coordinate, toward the given direction,
    // counts how many empty spaces there are (origin inclusive)
    static int countSpaces(String state, int r, int c, int dr, int dc) {
        int k = 0;
        while (at(state, r + k * dr, c + k * dc) == EMPTY) {
            k++;
        }
        return k;
    }

    // the predecessor map, maps currentState => previousState
    static Map<String,String> pred = new HashMap<String,String>();
    // the breadth first search queue
    static Queue<String> queue = new LinkedList<String>();
    // the breadth first search proposal method: if we haven't reached it yet,
    // (i.e. it has no predecessor), we map the given state and add to queue
    static void propose(String next, String prev) {
        if (!pred.containsKey(next)) {
            pred.put(next, prev);
            queue.add(next);
        }
    }

    // the predecessor tracing method, implemented using recursion for brevity;
    // guaranteed no infinite recursion, but may throw StackOverflowError on
    // really long shortest-path trace (which is infeasible in standard Rush Hour)
    static int trace(String current) {
        String prev = pred.get(current);
        int step = (prev == null) ? 0 : trace(prev) + 1;
        System.out.println(step);
        System.out.println(prettify(current));
        return step;
    }

    // in a given state, from a given origin coordinate, attempts to find a car of a given type
    // at a given distance in a given direction; if found, slide it in the opposite direction
    // one spot at a time, exactly n times, proposing those states to the breadth first search
    //
    // e.g.
    //    direction = -->
    //    __n__
    //   /     \
    //   ..o....c
    //      \___/
    //      distance
    //
    static void slide(String current, int r, int c, String type, int distance, int dr, int dc, int n) {
        r += distance * dr;
        c += distance * dc;
        char car = at(current, r, c);
        if (!isType(car, type)) return;
        final int L = length(car);
        StringBuilder sb = new StringBuilder(current);
        for (int i = 0; i < n; i++) {
            r -= dr;
            c -= dc;
            sb.setCharAt(rc2i(r, c), car);
            sb.setCharAt(rc2i(r + L * dr, c + L * dc), EMPTY);
            propose(sb.toString(), current);
            current = sb.toString(); // comment to combo as one step
        }
    }

    // explores a given state; searches for next level states in the breadth first search
    //
    // Let (r,c) be the intersection point of this cross:
    //
    //     @       nU = 3     '@' is not a car, 'B' and 'X' are of the wrong type;
    //     .       nD = 1     only '2' can slide to the right up to 5 spaces
    //   2.....B   nL = 2
    //     X       nR = 4
    //
    // The n? counts how many spaces are there in a given direction, origin inclusive.
    // Cars matching the type will then slide on these "alleys".
    //
    static void explore(String current) {
        for (int r = 0; r < M; r++) {
            for (int c = 0; c < N; c++) {
                if (at(current, r, c) != EMPTY) continue;
                int nU = countSpaces(current, r, c, -1, 0);
                int nD = countSpaces(current, r, c, +1, 0);
                int nL = countSpaces(current, r, c, 0, -1);
                int nR = countSpaces(current, r, c, 0, +1);
                slide(current, r, c, VERTS, nU, -1, 0, nU + nD - 1);
                slide(current, r, c, VERTS, nD, +1, 0, nU + nD - 1);
                slide(current, r, c, HORZS, nL, 0, -1, nL + nR - 1);
                slide(current, r, c, HORZS, nR, 0, +1, nL + nR - 1);
            }
        }
    }
    public static void main(String[] args) {
        // typical queue-based breadth first search implementation
        propose(INITIAL, null);
        boolean solved = false;
        while (!queue.isEmpty()) {
            String current = queue.remove();
            if (isGoal(current) && !solved) {
                solved = true;
                trace(current);
                //break; // comment to continue exploring entire space
            }
            explore(current);
        }
        System.out.println(pred.size() + " explored");
    }
}

源码中有两行值得注意:

  • 找到解决方案时的break;
    • 现在对此进行了注释,以便广度优先搜索探索整个搜索空间,以确认上面链接网站中给出的数字
  • slide 中的current = sb.toString();
    • 基本上这将任何汽车的每次移动都计为一次移动。如果一辆车向左移动 3 格,那就是 3 次移动。要将其组合为一个动作(因为它涉及同一辆车向同一方向移动),只需注释此行。链接的网站不承认组合,因此该行现在未注释以匹配给定的最小移动数。使用连击数,93 步问题只需要 49 步。也就是说,如果停车场里有一个停车服务员来移动这些汽车,他只需要进出汽车 49 次。

算法概述

该算法本质上是广度优先搜索,通常使用队列实现。维护前驱图,以便任何状态都可以追溯到初始状态。一个键永远不会被重新映射,并且由于条目是按广度优先搜索顺序插入的,因此可以保证最短路径。

状态表示为NxM-length String。每个char 代表板上的一个实体,以行优先顺序存储。

通过从空白空间扫描所有 4 个方向来找到相邻状态,寻找合适的汽车类型,并在房间容纳时滑动它。

这里有很多多余的工作(例如多次扫描长“小巷”),但如前所述,虽然通用版本是 PSPACE 完整的,但经典的 Rush Hour 变体很容易通过蛮力处理。

维基百科参考

【讨论】:

  • @Earlz:当然,它的泛化是 PSPACE 完备的。此解决方案适用于经典的 Rush Hour 变体,是迄今为止最常见的变体。
  • 这很好,很好,但是 C 风格的写作有点限制,你能解释一下这里发生了什么吗?
  • nU = 3 上的错字应为nU = 2。将修复下一个版本,以及其他人建议的任何其他添加/更正。
  • @polygenelubricants:你的回答写得真好!
  • @Moron:我想我会在下一个版本中更多地解释组合的事情,但谢谢你这么说。
【解决方案2】:

这是我的答案。它在不到 6 秒的时间内解决了大师级难题。

它使用广度优先搜索 (BFS)。诀窍是寻找您在之前的搜索中看到的电路板布局并中止该序列。由于 BFS,如果您在到达那里之前已经看到了该布局,那么请让该序列继续尝试解决它而不是这个更长的方法。

#!perl

# Program by Rodos rodos at haywood dot org

use Storable qw(dclone);
use Data::Dumper;

print "Lets play Rush Hour! \n";


# Lets define our current game state as a grid where each car is a different letter.
# Our special car is a marked with the specific letter T
# The boarder is a * and the gloal point on the edge is an @.
# The grid must be the same witdh and height 
# You can use a . to mark an empty space

# Grand Master
@startingGrid = (
 ['*','*','*','*','*','*','*','*'],
 ['*','.','.','A','O','O','O','*'],
 ['*','.','.','A','.','B','.','*'],
 ['*','.','T','T','C','B','.','@'],
 ['*','D','D','E','C','.','P','*'],
 ['*','.','F','E','G','G','P','*'],
 ['*','.','F','Q','Q','Q','P','*'],
 ['*','*','*','*','*','*','*','*']
);

# Now lets print out our grid board so we can see what it looks like.
# We will go through each row and then each column.
# As we do this we will record the list of cars (letters) we see into a hash

print "Here is your board.\n";

&printGrid(\@startingGrid);

# Lets find the cars on the board and the direction they are sitting

for $row (0 .. $#startingGrid) {
    for $col (0 .. $#{$startingGrid[$row]} ) {

        # Make spot the value of the bit on the grid we are looking at
        $spot = $startingGrid[$row][$col];

        # Lets record any cars we see into a "hash" of valid cars.
        # If the splot is a non-character we will ignore it cars are only characters
        unless ($spot =~ /\W/) {

            # We will record the direction of the car as the value of the hash key.
            # If the location above or below our spot is the same then the car must be vertical.
            # If its not vertical we mark as it as horizonal as it can't be anything else!

            if ($startingGrid[$row-1][$col] eq $spot || $startingGrid[$row+1] eq $spot) {
                $cars{$spot} = '|';
            } else {
                $cars{$spot} = '-';
            }
        }
    }
}

# Okay we should have printed our grid and worked out the unique cars
# Lets print out our list of cars in order

print "\nI have determined that you have used the following cars on your grid board.\n";
foreach $car (sort keys %cars) {
    print " $car$cars{$car}";
}
print "\n\n";

end;

&tryMoves();

end;

# Here are our subroutines for things that we want to do over and over again or things we might do once but for 
# clatiry we want to keep the main line of logic clear

sub tryMoves {

    # Okay, this is the hard work. Take the grid we have been given. For each car see what moves are possible
    # and try each in turn on a new grid. We will do a shallow breadth first search (BFS) rather than depth first. 
    # The BFS is achieved by throwing new sequences onto the end of a stack. You then keep pulling sequnces
    # from the front of the stack. Each time you get a new item of the stack you have to rebuild the grid to what
    # it looks like at that point based on the previous moves, this takes more CPU but does not consume as much
    # memory as saving all of the grid representations.

    my (@moveQueue);
    my (@thisMove);
    push @moveQueue, \@thisMove;

    # Whlst there are moves on the queue process them                
    while ($sequence = shift @moveQueue) { 

        # We have to make a current view of the grid based on the moves that got us here

        $currentGrid = dclone(\@startingGrid);
        foreach $step (@{ $sequence }) {
            $step =~ /(\w)-(\w)(\d)/;
            $car = $1; $dir = $2; $repeat = $3;

            foreach (1 .. $repeat) {
                &moveCarRight($car, $currentGrid) if $dir eq 'R';
                &moveCarLeft($car,  $currentGrid) if $dir eq 'L';
                &moveCarUp($car,    $currentGrid) if $dir eq 'U';
                &moveCarDown($car,  $currentGrid) if $dir eq 'D';
            }
        }

        # Lets see what are the moves that we can do from here.

        my (@moves);

        foreach $car (sort keys %cars) {
            if ($cars{$car} eq "-") {
                $l = &canGoLeft($car,$currentGrid);
                push @moves, "$car-L$l" if ($l);
                $l = &canGoRight($car,$currentGrid);
                push @moves, "$car-R$l" if ($l);
            } else {
                $l = &canGoUp($car,$currentGrid);
                push @moves, "$car-U$l" if ($l);
                $l = &canGoDown($car,$currentGrid);
                push @moves, "$car-D$l" if ($l);
            }
        }

        # Try each of the moves, if it solves the puzzle we are done. Otherwise take the new 
        # list of moves and throw it on the stack

        foreach $step (@moves) {

            $step =~ /(\w)-(\w)(\d)/;
            $car = $1; $dir = $2; $repeat = $3;

            my $newGrid = dclone($currentGrid);

            foreach (1 .. $repeat) {
                &moveCarRight($car, $newGrid) if $dir eq 'R';
                &moveCarLeft($car, $newGrid) if $dir eq 'L';
                &moveCarUp($car, $newGrid) if $dir eq 'U';
                &moveCarDown($car, $newGrid) if $dir eq 'D';
            }

            if (&isItSolved($newGrid)) {
                print sprintf("Solution in %d moves :\n", (scalar @{ $sequence }) + 1);
                print join ",", @{ $sequence };
                print ",$car-$dir$repeat\n";
                return;
            } else {

                # That did not create a solution, before we push this for further sequencing we want to see if this
                # pattern has been encountered before. If it has there is no point trying more variations as we already
                # have a sequence that gets here and it might have been shorter, thanks to our BFS

                if (!&seen($newGrid)) {
                    # Um, looks like it was not solved, lets throw this grid on the queue for another attempt
                    my (@thisSteps) = @{ $sequence };
                    push @thisSteps, "$car-$dir$repeat";
                    push @moveQueue, \@thisSteps;
                }
            }            
        }
    }
}    

sub isItSolved {

    my ($grid) = shift;

    my ($row, $col);
    my $stringVersion;

    foreach $row (@$grid) {
        $stringVersion .= join "",@$row;
    }

    # We know we have solve the grid lock when the T is next to the @, because that means the taxi is at the door
    if ($stringVersion =~ /\T\@/) {
        return 1;
    }
    return 0;
}    

sub seen {

    my ($grid) = shift;

    my ($row, $col);
    my $stringVersion;

    foreach $row (@$grid) {
        $stringVersion .= join "",@$row;
    }

    # Have we seen this before?
    if ($seen{$stringVersion}) {
        return 1;
    }
    $seen{$stringVersion} = 1;
    return 0;
}    

sub canGoDown {

    my ($car) = shift;

    return 0 if $cars{$car} eq "-";

    my ($grid) = shift;

    my ($row, $col);


    for ($row = $#{$grid}; $row >= 0; --$row) {
        for $col (0 .. $#{$grid->[$row]} ) {
            if ($grid->[$row][$col] eq $car) {
                # See how many we can move
                $l = 0;
                while ($grid->[++$row][$col] eq ".") {
                    ++$l;
                }
                return $l;
            }
        }
    }
    return 0;
}

sub canGoUp {

    my ($car) = shift;

    return 0 if $cars{$car} eq "-";

    my ($grid) = shift;

    my ($row, $col);

    for $row (0 .. $#{$grid}) {
        for $col (0 .. $#{$grid->[$row]} ) {
            if ($grid->[$row][$col] eq $car) {
                # See how many we can move
                $l = 0;
                while ($grid->[--$row][$col] eq ".") {
                    ++$l;
                } 
                return $l;
            }
        }
    }
    return 0;
}

sub canGoRight {

    my ($car) = shift;

    return 0 if $cars{$car} eq "|";

    my ($grid) = shift;

    my ($row, $col);

    for $row (0 .. $#{$grid}) {
        for ($col = $#{$grid->[$row]}; $col >= 0; --$col ) {
            if ($grid->[$row][$col] eq $car) {
                # See how many we can move
                $l = 0;
                while ($grid->[$row][++$col] eq ".") {
                    ++$l;
                } 
                return $l;
            }
        }
    }
    return 0;
}

sub canGoLeft {

    my ($car) = shift;

    return 0 if $cars{$car} eq "|";

    my ($grid) = shift;

    my ($row, $col);

    for $row (0 .. $#{$grid}) {
        for $col (0 .. $#{$grid->[$row]} ) {
            if ($grid->[$row][$col] eq $car) {
                # See how many we can move
                $l = 0;
                while ($grid->[$row][--$col] eq ".") {
                    ++$l;
                } 
                return $l;
            }
        }
    }
    return 0;
}

sub moveCarLeft {

    # Move the named car to the left of the passed grid. Care must be taken with the algoritm
    # to not move part of the car and then come across it again on the same pass and move it again 
    # so moving left requires sweeping left to right.

    # We need to know which car you want to move and the reference to the grid you want to move it on
    my ($car) = shift;
    my ($grid) = shift;

    # Only horizontal cards can move left
    die "Opps, tried to move a vertical car $car left" if $cars{$car} eq "|";

    my ($row, $col);

    for $row (0 .. $#{$grid}) {
        for $col (0 .. $#{$grid->[$row]} ) {
            if ($grid->[$row][$col] eq $car) {
                die "Tried to move car $car left into an occupied spot\n" if $grid->[$row][$col-1] ne ".";
                $grid->[$row][$col-1] = $car;
                $grid->[$row][$col] = ".";
            }
        }
    }
}

sub moveCarRight {

    # Move the named car to the right of the passed grid. Care must be taken with the algoritm
    # to not move part of the car and then come across it again on the same pass and move it again 
    # so moving right requires sweeping right to left (backwards).

    # We need to know which car you want to move and the reference to the grid you want to move it on
    my ($car) = shift;
    my ($grid) = shift;

    # Only horizontal cards can move right
    die "Opps, tried to move a vertical car $car right" if $cars{$car} eq "|";

    my ($row, $col);

    for $row (0 .. $#{$grid}) {
        for ($col = $#{$grid->[$row]}; $col >= 0; --$col ) {
            if ($grid->[$row][$col] eq $car) {
                die "Tried to move car $car right into an occupied spot\n" if $grid->[$row][$col+1] ne ".";
                $grid->[$row][$col+1] = $car;
                $grid->[$row][$col] = ".";
            }
        }
    }
}


sub moveCarUp {

    # Move the named car up in the passed grid. Care must be taken with the algoritm
    # to not move part of the car and then come across it again on the same pass and move it again 
    # so moving right requires sweeping top down.

    # We need to know which car you want to move and the reference to the grid you want to move it on
    my ($car) = shift;
    my ($grid) = shift;

    # Only vertical cards can move up
    die "Opps, tried to move a horizontal car $car up" if $cars{$car} eq "-";

    my ($row, $col);

    for $row (0 .. $#{$grid}) {
        for $col (0 .. $#{$grid->[$row]} ) {
            if ($grid->[$row][$col] eq $car) {
                die "Tried to move car $car up into an occupied spot\n" if $grid->[$row-1][$col] ne ".";
                $grid->[$row-1][$col] = $car;
                $grid->[$row][$col] = ".";
            }
        }
    }
}

sub moveCarDown {

    # Move the named car down in the passed grid. Care must be taken with the algoritm
    # to not move part of the car and then come across it again on the same pass and move it again 
    # so moving right requires sweeping upwards from the bottom.

    # We need to know which car you want to move and the reference to the grid you want to move it on
    my ($car) = shift;
    my ($grid) = shift;

    # Only vertical cards can move up
    die "Opps, tried to move a horizontal car $car down" if $cars{$car} eq "-";

    my ($row, $col);    

    for ($row = $#{$grid}; $row >=0; --$row) {
        for $col (0 .. $#{$grid->[$row]} ) {
            if ($grid->[$row][$col] eq $car) {
                die "Tried to move car $car down into an occupied spot\n" if $grid->[$row+1][$col] ne ".";
                $grid->[$row+1][$col] = $car;
                $grid->[$row][$col] = ".";
            }
        }
    }
}

sub printGrid {

    # Print out a representation of a grid

    my ($grid) = shift; # This is a reference to an array of arrays whch is passed as the argument

    my ($row, $col);

    for $row (0 .. $#{$grid}) {
        for $col (0 .. $#{$grid->[$row]} ) {
                print $grid->[$row][$col], " ";
        }
        print "\n";
    }
}

【讨论】:

    【解决方案3】:

    【讨论】:

    • “在这里,我将概述如何用滑块拼图构建计算机” - 一定会喜欢 MIT。
    • 如果可以轻松验证解决方案,那么 Rush Hour PSPACE 的完整性如何?还是我只是偶然证明了 NP = PSPACE?
    • 只有pspace-complete才能尝试找到最优解吗?
    【解决方案4】:

    您应该递归(您的“回溯”解决方案)。这可能是解决此类难题的唯一方法;问题是如何快速完成。

    正如您所指出的,搜索空间会很大 - 但不会太大,如果您有一个合理大小的板。例如,您绘制了一个 6x6 网格,上面有 12 辆汽车。假设每辆是一辆 2 号车,每辆汽车有 5 个车位,所以最多 5^12 = 244,140,​​625 个潜在位置。这甚至适合 32 位整数。所以一种可能性是分配一个巨大的数组,每个潜在位置一个槽,并使用记忆来确保你不会重复一个位置。

    接下来要注意的是,大多数“潜在”位置实际上是不可能的(它们会涉及汽车重叠)。因此,请改为使用哈希表来跟踪您访问过的每个位置。每个条目的内存开销很小,但它可能比“巨大的数组”解决方案更节省空间。但是,每次访问都需要更长的时间。

    正如@Daniel's answer 中的麻省理工学院论文所说,这个问题是 PSPACE 完备的,这意味着许多用于降低 NP 问题复杂性的技巧可能无法使用。

    也就是说,对于重复位置问题,上述两种解决方案中的任何一种都应该适用于较小的网格。这一切都取决于问题有多大,以及您的计算机有多少内存;但是您展示的示例应该完全没有问题,即使对于普通的台式计算机也是如此。

    【讨论】:

    • 那么……如果网格稍微大一些/汽车多一些,人类解决问题可能比计算机更快?是不是很有趣。
    • @Mark 真正的问题是人类使用什么算法来解决它:)
    • @Mark,这很奇怪。围棋等游戏也是如此,直到最近十年,国际象棋也是如此。虽然不知道有没有“尖峰时刻宗师”:)
    • 完全正确。人类不会通过蛮力解决它(在大多数情况下)。那我们怎么解决!?我真的很想“观看”人们玩 Rush Hour,并分析他们如何解决它以及他们采取了哪些行动。
    【解决方案5】:

    刚刚写完我的实现并进行了试验。我同意 polygenelubricants 的观点,即状态空间对于经典游戏(6x6 棋盘)来说真的很小。然而,我尝试了一个聪明的搜索实现(A* search)。与简单的 BFS 相比,我很好奇探索状态空间的减少。

    A* 算法可以看作是 BFS 搜索的推广。下一步要探索哪条路径的决定取决于结合路径长度(即移动次数)和剩余移动计数下限的分数。 我选择计算后者的方法是获取红色汽车到出口的距离,然后为路上的每一辆车加 1,因为它必须至少移动一次才能让路。当我用常数 0 替换下限计算时,我得到了常规的 BFS 行为。

    在检查了来自 this list 的四个谜题后,我发现 A* 搜索比常规 BFS 探索的状态平均少 16%

    【讨论】:

      【解决方案6】:

      我认为递归是一个坏主意,除非你跟踪你已经访问过的内容;通过来回移动汽车,您最终可能会无限递归。

      也许这是一个好的开始:将每个棋盘状态表示并存储为无向图。然后对于每一个可能的移动,检查过去的状态,以确保你不只是再次达到相同的状态。

      现在制作另一个无向图,其中节点表示状态,边表示通过移动汽车从一个状态到另一个状态的能力。探索状态,直到其中一个成为解决方案。然后沿着边缘回到起点找出移动路径。

      【讨论】:

      • 我实际上想到了这个问题,但这很容易解决,只需将当前的移动集合放在一个静态变量中,然后检查你要进行的移动是否已经在集合中。您可以制作您正在谈论的图表,我想了想,扫描它以找到获胜的场景,然后使用 Dijkstra 的算法找到与它们的最短距离。但是,枚举所有状态在空间上是指数级的(我认为),这超出了个人计算机的处理能力。
      【解决方案7】:

      我写了一个数独求解器。虽然细节完全不同,但我认为整体问题是相似的。一方面,尝试在数独求解器中进行智能启发式算法比蛮力解决方案要慢得多。尝试每一步,使用一些简单的启发式方法并且没有重复是要走的路。在高峰时间检查重复的板状态稍微困难一些,但并不难。

      如果您查看示例中的棋盘,则只有 4 个有效动作。在任何给定时间,都只会有几个有效的移动。

      在每个递归级别,复制棋盘状态并尝试棋盘上的每一个有效动作。对于每个空方格,将每辆车可以移动到该方格上。如果新的棋盘状态不在历史列表中,则递归另一个级别。通过历史列表,我的意思是给导致该状态的每个板的每个级别的递归访问,可能在一个链接列表中。使用哈希快速丢弃不相等的状态。

      实现这一点的关键是拥有一个可以轻松复制和修改的简单棋盘状态。可能是一个每平方一个整数的数组,说明什么车覆盖了那个正方形,如果有的话。然后你只需要遍历方块并找出合法的动作。合法移动是指测试方格和朝向它的汽车之间的空白方格。

      与数独一样,最糟糕的选择是遗传算法。

      【讨论】:

        猜你喜欢
        • 2015-07-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-01
        • 2021-03-30
        • 2020-10-15
        相关资源
        最近更新 更多