ラッシュアワー
それに慣れていない場合、ゲームは、単一の出口を持つNxMグリッド上で、水平または垂直に設定されたさまざまなサイズの車のコレクションで構成されます。
別の車が妨げていない限り、各車は設定された方向に前後に移動できます。あなたは決して車の方向を変えることはできません。
特別な車が1台あり、通常は赤い車です。それは出口が入っているのと同じ行に設定されており、ゲームの目的は赤い車が迷路からドライブできるようにする一連の動き(動き-車をNステップ前または後ろに動かす)を見つけることです。
私はこの問題を計算で解決する方法を考えていましたが、本当に良い解決策は考えられません。
いくつか思いつきました:
だから問題は-グリッドと車両レイアウトを取り、赤い車を出すために必要な一連のステップを出力するプログラムをどのように作成するか?
サブイシュー:
例:この設定で車を移動して、赤い車が右側の出口から迷路を「出る」ようにするにはどうすればよいですか?
(ソース: scienceblogs.com )
古典的なラッシュアワーの場合、この問題は単純な幅優先検索で非常に扱いやすくなっています。主張されている最も難しい既知の初期構成では、解決するために93回の移動が必要であり、到達可能な構成は合計で24132のみです。単純に実装された幅優先検索アルゴリズムでさえ、ささやかなマシンでも1秒未満で検索スペース全体を探索できます。
以下は、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");
}
}
_
ソースコードには、注目に値する2行があります。
break;
_ slide
のcurrent = sb.toString();
アルゴリズムは基本的に幅優先検索であり、一般的なようにキューを使用して実装されます。先行マップは維持されているため、どのような状態でも初期状態にトレースできます。キーが再マッピングされることはありません。エントリが幅優先の検索順序で挿入されるため、最短パスが保証されます。
状態はNxM
- length String
として表されます。各char
はボード上のエンティティを表し、行優先順で格納されます。
隣接する状態は、空のスペースから4方向すべてをスキャンし、適切な車のタイプを探して、部屋が収まるようにスライドさせることで見つかります。
ここには多くの冗長な作業があります(たとえば、長い「路地」が複数回スキャンされます)。ただし、前述のように、一般化バージョンはPSPACE完全ですが、古典的なRush Hourバリアントはブルートフォースによって非常に扱いやすくなっています。
これが私の答えです。それはわずか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";
}
}
実際には、MIT特にラッシュアワーを参照する)の論文があります (検索用語「スライドブロックパズル」を使用しました)
実装の記述と実験を終えたところです。古典的なゲーム(6x6ボード)の場合、状態空間は本当に小さいと私はpolygenelubricantsに同意します。しかし、私は賢い検索の実装( A * search )を試しました。単純なBFSと比較して、探索された状態空間の削減について興味を持っていました。
A *アルゴリズムは、BFS検索の一般化と見なすことができます。次に探索するパスの決定は、パスの長さ(つまり、移動数)と残りの移動数の下限の両方を組み合わせたスコアによって決定されます。後者を計算するために選択した方法は、出口から赤い車の距離を取得し、道をクリアするために少なくとも1回移動する必要があるため、道のすべての車に1を加えることです。下限の計算を定数0に置き換えると、通常のBFS動作が得られます。
このリスト から4つのパズルを検査した後、A *検索は通常のBFSよりも平均で16%少ない状態を探索することがわかりました。
再帰する必要があります(「バックトラッキング」ソリューション)。これはおそらく、このようなパズルを解く唯一の方法です。問題はそれをどのように速く行うかです。
お気づきのように、妥当なサイズのボードを使用している場合、検索スペースは大きくなりますが、大きすぎません。たとえば、12台の車が配置された6x6グリッドを描画したとします。それぞれがサイズ2の自動車であるとすると、1台あたり5つのスペースが与えられるため、最大で5 ^ 12 = 244,140,625の潜在的な位置になります。これは32ビット整数にも適合します。したがって、1つの可能性は、潜在的な位置ごとに1つのスロットである巨大な配列を割り当て、メモ化を使用して、位置を繰り返さないようにすることです。
次に注意すべきことは、それらの「潜在的な」ポジションのほとんどは実際には可能ではないということです(それらは車の重なりを含みます)。代わりに、ハッシュテーブルを使用して、アクセスした各位置を追跡します。これにより、エントリごとのメモリオーバーヘッドが小さくなりますが、おそらく「巨大な配列」ソリューションよりもスペース効率が良くなります。ただし、エントリへのアクセスごとに少し時間がかかります。
MITの論文 @ Daniel's answer にあるように、問題はPSPACE完全であり、NP問題はおそらく使用できません。
とはいえ、繰り返し位置の問題に対する上記の2つの解決策のいずれかは、小さめのグリッドで機能するはずです。それはすべて、問題の大きさと、コンピューターに搭載されているメモリの量によって決まります。しかし、表示した例は、通常のデスクトップコンピュータであっても、まったく問題ありません。
すでに行ったことを追跡しない限り、再帰は悪い考えだと思います。車を前後に動かすと、無限に再帰する可能性があります。
多分これは良いスタートです:各ボードの状態を無向グラフとして表し、保存します。次に、考えられる移動ごとに、過去の状態をチェックして、同じ状態に戻るだけではないことを確認します。
ここで、ノードが状態を表し、エッジが車を動かすことによってある状態から別の状態に移動する能力を表す別の無向グラフを作成します。それらの1つが解決策になるまで状態を探索します。次に、エッジを最初に戻って移動パスを見つけます。
数独ソルバーを書いた。詳細はまったく異なりますが、全体的な問題は似ていると思います。 1つには、数独ソルバーでスマートヒューリスティックを実行しようとすると、ブルートフォースソリューションよりもはるかに遅くなります。いくつかの単純なヒューリスティックを使用してすべての移動を試行し、重複はありません。ラッシュアワーに重複するボードの状態をチェックするのは少し難しいですが、それほど多くはありません。
サンプルのボードを見ると、有効な手は4つだけです。いつでも、有効な動きはほんのわずかです。
再帰の各レベルで、ボードの状態をコピーし、ボード上のすべての有効な動きを試します。空の正方形ごとに、その正方形に移動できるすべての車を移動します。新しいボードの状態が履歴リストにない場合は、別のレベルを再帰します。履歴リストとは、おそらく、リンクされたリストで、その状態に至った各ボードへの再帰アクセスの各レベルを与えることです。ハッシュを使用して、等しくない状態をすばやく破棄します。
これの鍵は、簡単にコピーおよび変更できる単純なボード状態を持っていることです。おそらく、正方形ごとに1つの整数を持つ配列で、その正方形をカバーしている車がある場合は、その配列を表します。次に、四角形を繰り返し処理し、法的な動きを理解する必要があります。法的移動とは、テストスクエアとそれに向けられた自動車の間の空のスクエアを意味します。
数独と同様に、最悪の選択肢は遺伝的アルゴリズムです。