(2018) 我如何创建了一个包含所有有趣的《尖峰时刻》配置的数据库
Solving Rush Hour, the Puzzle (2018)

原始链接: https://www.michaelfogleman.com/rush/

## 拥堵时段谜题数据库:深度解析 该项目详细介绍了为滑动方块谜题“拥堵时段”创建完整数据库的过程。作者在最初玩过手机版游戏后,重新回到实体棋盘上,受到启发,开始了一段旅程,不仅要解决问题,还要*生成*和编录标准6x6棋盘上所有“有趣”的谜题配置。 该过程始于一个求解器,然后是一个使用模拟退火的谜题生成器。作者并不满意,旨在生成*所有*可能的谜题,最终得到一个包含超过250万个独特配置的数据库,并根据复杂程度进行筛选,避免琐碎的排列。 “有趣”谜题的关键标准包括防止堵塞的行/列,并确保每个棋子对解决方案至关重要。 该项目利用位棋盘来实现高效的状态表示,并采用规范状态枚举技术来避免冗余计算,最终在一台强大的iMac上耗时3小时,或在配备72个核心的云服务器上耗时17小时。 结果数据库可供下载,其中包含需要最多50步才能解决的谜题,为“拥堵时段”爱好者和研究人员提供了重要的资源。 代码是开源的,并且还有一个可在线游玩的版本。 这项工作展示了计算能力在经典谜题中的一个迷人应用,并推动了人们对“拥堵时段”复杂性的认知边界。

这个Hacker News讨论围绕着Michael Fogleman 2018年发表的关于解决“Rush Hour”拼图的文章。用户分享相关的谜题和见解,强调了这款游戏的惊人复杂性。 值得注意的是,“Subway Shuffle”拼图被提及难度显著更高,有些实例需要589步才能解决。 另一个变体,“Unit Rush Hour”(将汽车缩小到1x1大小)极大地增加了难度,最难的谜题达到732步——远远超过了标准6x6 “Rush Hour”的51步。 评论者还分享了资源:一个指向Simon Tatham的谜题合集(可在Android上使用)的链接,一个“Rush Hour”解决方案的3D可视化,以及一篇详细说明使用PDDL为该谜题构建更清晰规划模型的论文。 几位用户表示喜欢这款谜题,甚至有人从讨论中获得了送礼的灵感。
相关文章

原文

How I created a database of all interesting Rush Hour configurations.

Rush Hour is a 6x6 sliding block puzzle invented by Nob Yoshigahara in the 1970s. It was first sold in the United States in 1996.

I played a clone of this game on my first iPhone several years ago. Recently, I stumbled on the physical incarnation of it and instantly bought it on Amazon for my kids to play. We've been having fun with it, but naturally I was most interested in writing some code to solve the puzzles.

After writing a solver, I wrote a puzzle generator that would create more starting positions for us to try (the game comes with 40 levels printed on playing cards). The generator used simulated annealing to try to maximize the number of moves required to solve the puzzle.

Unsatisfied with the results, I then decided to try generating all possible puzzles. Ultimately I ended up with a complete database of every "interesting" starting position. It was quite challenging (and exciting!) and that's what I want to talk about in this article. My code is open source with a permissive license and the resulting database is available for download.

Rush Hour (Regular Edition)

Table of Contents

Terminology

  • Piece: a vehicle on the puzzle board. Each piece has a position, a size, and an orientation (horizontal or vertical). In Rush Hour there are 2-pieces (cars) and 3-pieces (trucks).
  • Primary Piece: the "red car" in the puzzle, the piece that must reach the exit on the right side of the board.
  • Step: moving a piece by one unit.
  • Move: moving a piece by N steps at once.
  • Goal State: any state where the primary piece is on the target square (all the way to the right). Note that there's no need to model the primary piece actually exiting the board.
  • Unsolved: a state that is further from a goal state than any other reachable state. The puzzle cannot be made any harder by moving the pieces around. Note that this does not mean unsolvable!
  • Minimal: a state where each piece is important. No piece can be removed without altering the solution.
  • Hardest: requiring the most moves to solve.
  • Cluster: a set of puzzle states that are all reachable from one another - a connected graph of all reachable states.
  • Wall: a fixed (unmovable), single-cell obstacle. Not part of the original Rush Hour game, but included in some clones and in my code.

Prior Art

I didn't do a whole lot of research or anything, but did find these papers:

The first one was rather academic and not particularly helpful. It seemed they were more interested in taking an unusual approach rather than just solving the problem. Also it was optimizing number of "steps" (versus "moves" - steps being unit moves) which seemed an unnatural way of thinking about it to me. The second was more interesting and gave me some ideas. But no where did I find any sort of complete database, just a handful of the hardest configurations and a few charts. The second paper mentioned that it took on the order of 100 hours for their code to run. My code takes about 3 hours.

Interesting Puzzles

So, what do I mean by "interesting"? I established a set of rules to filter out a lot of nonsense puzzles. Interesting puzzles follow these rules:

  • No row may be completely filled with horizontal pieces and no column may be completely filled with vertical pieces. These just form walls of pieces that can never move. Not very interesting.
  • No horizontal piece may appear on the primary row except for the primary piece itself. That would allow other cars to exit the board too. However, walls may be present to the left of the primary piece.
  • There should only be one database entry per cluster - no two entries in the database should be reachable from one another.
  • Database entries should be fully "unsolved" - they cannot be made any harder by moving pieces around.
  • Database entries should be "minimal" - each piece in the puzzle is important. Removing any piece will alter the solution. (This rule drastically reduces the size of the database but adds some processing time.)
  • The database should be complete - any cluster that satisfies the above rules should be included.

Combinatorial Explosion

Rush Hour is a perfect example of combinatorial explosion.

Board Size

Time to Solve*

Interesting Puzzles

Number of States

Number of States (One Wall)

4x4

0.01s

32

9,303

43,927

5x5

6.2s

1,730

6,795,363

51,444,144

6x6

3h10m

476,118

27,103,652,325

287,592,039,683

7x7

???

???

???

???

So, writing efficient code and using clever algorithms will be important but will only get us so far. I think solving the 7x7 case may be doable but would require extensive computing resources that I can't afford! (It would probably require decades of CPU time.) Even adding walls increases complexity significantly, so I have only solved the case of 0, 1, or 2 walls. Note that while there are tens of billions of possible states, there are only about half a million puzzles that meet our criteria of interesting-ness.

* times are from an iMac with a 3.4 GHz Intel Core i5 (4 cores)

Bitboards

My code uses bitboards heavily. These are commonly used in chess engines and for other board games. In the case of Rush Hour, the bitboards represent which squares are occupied by pieces. The code also maintains a bitboard for just horizontal pieces and a bitboard for just vertical pieces. There are 6x6 = 36 squares, so a 64-bit unsigned integer is used for the bitboards.

The following diagram shows which bits map to which squares.

00 01 02 03 04 05
06 07 08 09 10 11
12 13 14 15 16 17
18 19 20 21 22 23
24 25 26 27 28 29
30 31 32 33 34 35

The following diagram shows the bitboards for a sample board.

All Horz Vert GBB.L. 111010 011000 100010 GHI.LM 111011 000000 111011 GHIAAM => 111111 = 000110 | 111001 CCCK.M 111101 111000 000101 ..JKDD 001111 000011 001100 EEJFF. 111110 110110 001000

Solving a 51-move Puzzle

Enumerating States

To examine all possible puzzles, we need an efficient way to enumerate all possible piece placements based on our rules. Let's break it down by looking at individual rows and columns first. For the 6x6 puzzle with 2-pieces and 3-pieces, the following table shows all of the possible ways that horizontal pieces may appear on a row (or vertical pieces on a column).

[]

[2]

[2, 2]

[2, 3]

[3]

[3, 2]

......

AA....
.AA...
..AA..
...AA.
....AA

AABB..
AA.BB.
AA..BB
.AABB.
.AA.BB
..AABB

AABBB.
AA.BBB
.AABBB

AAA...
.AAA..
..AAA.
...AAA

AAABB.
AAA.BB
.AAABB

The primary row (the row with the red car) is special and is limited to the [2] set.

We can choose any of these placements for every row. But then when we begin choosing placements for the columns, we must check that we aren't making vertical pieces overlap the horizontal pieces. In my implementation, I use bitboards that are progressively updated as piece assignments are made to check for these collisions efficiently. As soon as a collision is identified, that subtree no longer needs to be considered.

Lexicographical Ordering

The enumeration scheme above will produce billions of states that we can then evaluate. But we only need to evaluate each distinct cluster (connected graph of mutually reachable states) once. We really only need the enumerator to give us one state per cluster, and then we can find the other reachable states during further evaluation.

In theory, we could maintain a giant set of seen positions to avoid duplicating work. But given the size of the search space I considered this a bad approach.

So I created the notion of having a single "canonical" state for each cluster. The canonical state is the one that compares less than every other reachable state. Less than, you may ask? Recall that the Board class maintains three bitboards, respectively indicating the presence of 1) all pieces, 2) horizontal pieces, and 3) vertical pieces. With these, defining the less-than operator is simple:

bool operator
    

Simply comparing the All masks would not work, because two different states in the same cluster could have the same value.

So now, when evaluating a new cluster as a potential database entry, we can immediately exit early if we find that the initial state was not less than every other reachable state in the cluster. We trade in a small bit of processing time for a huge relief in memory consumption and inter-thread communication.

But there's more! We can make the enumerator smarter such that it usually produces canonical states that won't fail this check. If any piece can move up or to the left, then we know that some reachable state is less than the current state. Bitboards are put to good use here. As the enumerator is building up candidate states, it maintains a bitboard indicating squares that must be occupied to prevent any pieces from moving up or left. These are the squares immediately above vertical pieces and immediately to the left of horizontal pieces. Here's an example to demonstrate how just a few bitboard operations can identify if any pieces can move up or left. The standalone formula is: (((Horz >> 1) & ~RightColumn) | (Vert >> 6)) & ~All

H>>1&~RC V>>6 ~All GBB.L. 110000 111011 000101 000001 GHI.LM 000000 111001 000100 000000 GHIAAM => 001100 | 000101 & 000000 = 000000 CCCK.M 110000 001100 000010 000000 ..JKDD 000110 001000 110000 000000 EEJFF. 101100 000000 000001 000000

Shifting the horizontal mask by 1 emulates moving horizontal pieces to the left, but we must clear out the rightmost column due to the bits wrapping around. Shifting the vertical mask by the board width (6) emulates moving vertical pieces up. In this case we don't need to worry about wrapping. Finally, the & ~All operation removes bits that will be prevented from moving due to obstacles.

The result in this example is nonzero, so some piece is able to move up or left. (We can see that the M piece can move up.) This board is not canonical and can be skipped. (We'll visit this cluster when the enumerator produces the canonical form.)

Thanks to this technique, the code uses very little memory and doesn't need to maintain a huge lookup table of already-seen positions. In the 6x6 case, the enumerator produces perfectly "canonical" states 33% of the time. Without this trick it would be less than 1%.

Visualizing Clusters

Clusters are just directed graphs where each node is a puzzle state and each edge is a valid move. (Actually, all moves are reversible, so it could be considered an undirected graph as well.) So we can render them using Graphviz. Here are some examples (click to enlarge).

In these graphs, unsolved states are red, goal states are green, and an optimal shortest path is shown in blue. All other nodes are yellow. We can see that different clusters have different "shapes" and there are often bottleneck points. Perhaps this could somehow be used to estimate the difficulty of the puzzles?

Cluster Analysis

Go vs. C++

All of my code was initially written in Go. But when I started trying to "solve" the entire game I decided to use C++ to get the most performance possible.

I made a lot of improvements along the way in porting my code to C++, so I don't have a direct apples-to-apples comparison between Go and C++ for the full program. But I did run benchmarks on several sub-problems, such as repeatedly solving the same puzzle. In most of these benchmarks C++ came out about 2x faster than Go.

Special thanks to Matt Godbolt (of Compiler Explorer fame) for giving me some C++ performance tips!

The Cloud

Solving the 6x6 case with no walls took 3h10m on my iMac. But to compute puzzles with up to two walls, I used an EC2 instance with 72 cores (3.0 GHz Intel Xeon Scalable) to crank through it faster. It took 16h50m to complete and cost me about $50. But it sure was fun! I estimate it would have taken 170 hours (about one week) on my iMac.

htop on EC2 with 72 cores chugging away

Hardest Puzzles

Quantifying the difficulty of Rush Hour puzzles is... difficult... so normally when I say "hardest" I mean "requiring the most moves." I now present to you the hardest 6x6 puzzles with 0, 1, and 2 walls.

Analyzing the Results

It is interesting to look at the cluster statistics in aggregate. The most common number of moves in the database is 11 and hard puzzles are rare. We see a very nice normal distribution when plotting log(cluster size). I didn't dive in too deeply here, so there might be more worth exploring.

Database Format

The database is a simple text file with just a few columns. There is one row for every valid (solvable, minimal) cluster. The columns are: # of moves, board description, and cluster size (# of reachable states).

60 IBBxooIooLDDJAALooJoKEEMFFKooMGGHHHM 2332
58 BBoKMxDDDKMoIAALooIoJLEEooJFFNoGGoxN 9192
55 ooBBMxDDDKMoAAJKoNooJEENIFFLooIGGLox 9712
55 ooBBMxDDDKMoAAJKoNooJEENIFFLooIGGLHH 8257
54 oxCCMoDDDKMoAAJKooooJLEEIFFLoNIGGoxN 6355
54 oooJLxCCCJLoHAAKooHoIKDDooIEEMoFFoxM 3461
54 oooJxoCCCJLoHAAKLoHoIKDDooIEEMoFFoxM 2948
54 BBBKCCDDoKoLIAAKoLIoJEEMFFJooMooxoHH 1845
53 BBBCCNDDoxMNJAAoMOJoKFFOGGKLooxIILoo 4705
53 ooBBoxDDDKooAAJKoMooJEEMIFFLooIGGLHH 4228
53 ooBBoxDDDKooAAJKoMooJEEMIFFLooIGGLox 4102
52 oxCCMNDDDKMNAAJKooooJEEoIFFLooIGGLox 7178
52 oxCCMNDDDKMNAAJKooooJLEEIFFLooIGGHHo 5717
51 xCCoLMooJoLMAAJoLNHIDDDNHIoKEExooKGG 9360
51 GBBoLoGHIoLMGHIAAMCCCKoMooJKDDEEJFFo 4780
50 BBIooMGHIoLMGHAALNGCCKoNooJKDDooJEEx 14385
50 ooIBBBooIKooAAJKoLCCJDDLGHEEoLGHFFoo 4643
50 ooooxoCCCJLoAAIJLMooIDDMHEEKooHFFKox 3874
50 ooooLxCCCJLoAAIJoMooIDDMHEEKooHFFKox 3676
50 ooooLxCCCJLoAAIJoMooIDDMHEEKooHFFKGG 3169
50 ooooxoCCCJLoAAIJLMooIDDMHEEKooHFFKGG 2966
50 ooxCCLDDoKoLIAAKooIoJKEEFFJooMGGGHHM 1845
50 BBBJCCHooJoKHAAJoKooIDDLEEIooLooxoGG 1066

The board description is a 36-character string representing the state of the unsolved board. It is a 6x6 2D array in row-major order. The characters in the description follow these simple rules:

I used lowercase o instead of periods . for the empty cells in the database so that the entire board description can be selected with a double-click. My code can parse either format.

Database Download

The database contains every "interesting" 6x6 Rush Hour puzzle with up to two walls. There are 2,577,412 puzzles in the database, covering 9,698,093,879 reachable states. Walls are lowercase x so you can easily grep them in or out as desired.

Utilities

My code includes several utility binaries written in Go for solving puzzles, rendering puzzles, etc.

To render a board:

$ go run cmd/render/main.go GBB.L.GHI.LMGHIAAMCCCK.M..JKDDEEJFF. out.png

To create a .dot file for Graphviz and then render it:

$ go run cmd/graph/main.go ..xCCLDD.K.LIAAK..I.JKEEFFJ..MGGGHHM > graph.dot
$ dot -Tpng graph.dot > graph.png

To solve a puzzle:

$ go run cmd/solve/main.go GBB.L.GHI.LMGHIAAMCCCK.M..JKDDEEJFF.
{true [F+1 K+1 M-1 C+3 H+2 J-1 E+1 G+3 B-1 I-1 A-3 I+1 L+1 B+3 I-1 A+2 G-3 E-1
H-3 A-1 J+1 C-3 M+1 B+1 K-4 A+1 C+2 D-1 F-1 H+3 A-1 K+1 B-1 M-1 C+1 J-1 E+1 G+3
A-1 I+1 B-3 I-1 A+1 G-1 E-1 J+1 C-1 K-1 L-1 M+3 A+3] 51 81 51 2756 229984}

Negative moves are up or left, positive moves are down or right.

Online Play

I wrote an online version that you can play. I used p5.js to create it. Click "Random" to play a random puzzle from my database.

Try It Yourself

The code is on GitHub at https://github.com/fogleman/rush.

Top 23

The top 23 puzzles in the database require 50 or more moves to solve. Here they are.

Click on one to play it in your browser!

60 moves, 2332 states

58 moves, 9192 states

55 moves, 9712 states

55 moves, 8257 states

54 moves, 6355 states

54 moves, 3461 states

54 moves, 2948 states

54 moves, 1845 states

53 moves, 4705 states

53 moves, 4228 states

53 moves, 4102 states

52 moves, 7178 states

52 moves, 5717 states

51 moves, 9360 states

51 moves, 4780 states

50 moves, 14385 states

50 moves, 4643 states

50 moves, 3874 states

50 moves, 3676 states

50 moves, 3169 states

50 moves, 2966 states

50 moves, 1845 states

50 moves, 1066 states

Conclusion

I believe that the techniques described in this article advance the current "state of the art" for solving the game of Rush Hour. Prove me wrong and I'll be thrilled!

Let me know if you do anything interesting with the code or the database!

联系我们 contact @ memedata.com