Fast automated solver for Flow Free puzzles written in C.  用C语言编写的连线游戏的快速自动求解器。

GIF of the final program in action (see below if you’re unfamiliar with Flow Free):

最终程序的GIF效果(如果您不熟悉Flow Free,请参见下文):

Standard admonishments apply: feel free to skip ahead to the end; also, don’t hesitate to try out the code, which is up on github as always.

适用标准建议:随时跳过最后一步; 另外,请不要犹豫地尝试代码,该代码一如既往地在github上发布。

Overview  总览

I like puzzles. Well, more precisely, I enjoy problem solving, and puzzles present nice, neat, self-contained problems to solve from time to time. But one thing I like even better than solving a puzzle is writing programs to automatically solve a whole lot of puzzles, fast.

我喜欢拼图。 好吧,更准确地说,我喜欢解决问题,猜谜游戏会不时呈现出精美,整洁,独立的问题,需要解决。 但是我比解决猜谜更喜欢的一件事是编写程序来快速自动解决大量猜谜游戏。

Here’s a couple of screenshots from the mobile game Flow Free, in case you’re unfamiliar:

如果您不熟悉,这是移动游戏Flow Free的一些屏幕截图:

The game takes place on a grid which starts out empty, except for a number of pairs of colored dots (as shown on the left). The objective is to draw paths or flows such that all pairs are connected and all of the empty cells are filled (as shown on the right). Puzzles range from 5x5 all the way up to 14x14, and they may have anywhere from 5 to 16 colors of flows.

游戏在开始时是空的网格上进行,除了许多成对的彩色点(如左图所示)外。 目的是绘制路径或流,以使所有线对都连接并且填充所有空单元格(如右图所示)。 拼图的范围从5x5一直到14x14,它们的流动范围可能从5到16种颜色。

I’ve had Flow Free on my iPhone for years, and it’s saved me from boredom on many a subway or airplane. From time to time I’ve wondered how hard it would be to write a good automated solver for it, but I never took on the project until this summer, when I found myself with a few dozen hours of flights on which to occupy myself.

我在iPhone上安装过Flow Free已有多年了,它使我免于在许多地铁或飞机上感到无聊。 我不时地想知道为它编写一个好的自动求解器有多难,但是直到今年夏天我才开始从事这个项目,那时我发现自己要花几十个小时的时间来占据自己的位置。

I developed the bulk of my solver over about a week of travel through Indonesia, Singapore, and Malaysia; when I got back home, I added about an order of magnitude’s worth of speed optimizations and some ANSI cursor commands to produce animated solutions like the one shown above. The program also outputs SVGimages, which became the basis for many of the figures in this post.我在印度尼西亚,新加坡和马来西亚旅行了大约一周的时间,开发了大部分求解器。 当我回到家时,我添加了大约一个数量级的速度优化和一些ANSI光标命令,以产生类似于上面所示的动画解决方案。 该程序还输出SVG图像,这些图像成为本文中许多图形的基础。

By the way, I did dig up some prior work on the subject, but I only started looking for it once I had finished my solver. If you’re curious, there’s a list of related efforts at the very end of this post. As far as I can tell, my solver is the only one I’m aware of that can tackle large puzzles (e.g. 12x12 and up) in a reasonable amount of time.顺便说一句,我确实做了一些有关该主题的先前工作,但只有在完成求解器后才开始寻找它。 如果您感到好奇,请在本文结尾处列出相关的工作清单。 据我所知,我唯一知道的求解器可以在合理的时间内解决大型难题(例如12x12及以上)。

Framing the problem 解决问题

From an AI perspective, there are at least two substantially different ways to frame the Flow Free puzzle: the first is as a constraint satisfaction problem (CSP), and the second is as a shortest path problem. The former is the domain of SAT solvers and Knuth’s DLX, whereas the latter is the domain of Dijkstra’s algorithm and A* search.

从AI的角度来看,至少有两种实质上不同的方法来构筑连线游戏:第一种方法是约束满足问题(CSP),第二种方法是最短路径问题。 前者是SAT求解器和Knuth的DLX的领域,而后者是Dijkstra的算法和A *搜索的领域。

At first blush, considering Flow Free as a CSP seems like a simpler formulation, because the constraints are so simple to state:

乍一看,将Flow Free视为CSP似乎是一个更简单的表述,因为约束很容易陈述:

  • Each free space must be filled in by a color.每个可用空间必须用一种颜色填充。
  • Each filled-in cell must be adjacent to exactly two cells of the same color.每个填充的单元必须与两个相同颜色的单元完全相邻。
  • Each dot must be adjacent to exactly one filled-in cell of the same color.每个点必须与同一颜色的一个完全填充的单元格相邻。

In contrast, shortest path algorithms like Dijkstra’s and A* seem ill-suited for this problem because the zig-zaggy flows found in puzzle solutions rarely resemble short, direct paths:相比之下,Dijkstra和A *等最短路径算法似乎不适合该问题,因为在拼图解决方案中发现的之字形流很少类似于短而直接的路径:

Despite this apparently poor fit, I decided to take the “A*-like” approach for two main reasons:

尽管这种贴合性很差,但我还是决定采用“类似A *”的方法,主要有两个原因:

  1. I spent years in grad school coding up search-based planners with A*. 我在研究生院度过了多年,用A *为基于搜索的计划者编码。
  2. When you have a hammer, the whole world looks like a nail.当您拿着锤子时,整个世界看起来就像钉子。

Searching puzzle trees搜索拼图树

Although finding shortest paths is a general graph search problem, my solver operates on a tree rooted at the initial puzzle state. Each node in the tree not only stores a puzzle state, but also the current position of each flow. We can generate a successor for a state – i.e. its “child” in the tree – by extending one of the flows into an empty cell adjacent to its current position.尽管找到最短路径是一个一般的图搜索问题,但我的求解器在植根于初始拼图状态的树上运行。 树中的每个节点不仅存储拼图状态,而且还存储每个流的当前位置。 通过将其中一个流扩展到与其当前位置相邻的空单元格中,我们可以生成状态的后继者,即树中的“子级”。

It’s vitally important to reduce the branching factor – the average number of children per node – as much as possible, because a larger branching factor means searching exponentially more nodes. For example, a tree of branching factor 2 fully built out to depth of 10 has a modest 1,023 nodes, but doubling the branching factor to 4 would increase the tree size to 349,525!尽可能降低分支因子(每个节点的平均子节点数)至关重要,因为更大的分支因子意味着搜索成倍数量的节点。 例如,完全扩展到10深度的分支因子2的树具有适度的1,023个节点,但是将分支因子增加一倍至4将使树的大小增加到349,525!

So to keep our branching factor small, for any state we will designate a single active color which is permitted to move. Say we are solving a puzzle with 6 colors. If we allow moves from any flow’s current position, a node might have as many as 24 children (6 colors times 4 directions of motion per color); however, imposing an active color reduces the maximum number of children to 4.

因此,为了使分支因子保持较小,对于任何状态,我们都将指定一种允许移动的活动颜色。 假设我们正在用6种颜色解决难题。 如果我们允许从任何流的当前位置移动,则一个节点可能有多达24个子代(6种颜色乘以每种颜色4个运动方向); 但是,设置活动颜色会将子级的最大数量减少到4个。

Which color should we choose to be active? Probably the most constrained one – that is, the color with the fewest possible moves. For example, consider the puzzle below, with the colored cell backgrounds indicating the initial position for each flow. Both blue and cyan have only one valid move available, but green has four:我们应该选择哪种颜色激活? 可能是最受限制的一种-也就是说,移动最少的颜色。 例如,考虑下面的难题,彩色单元格背景指示每个流程的初始位置。 蓝色和青色都只有一个有效的移动可用,但是绿色有四个有效移动:

Hence, we should choose blue or cyan as the active color now, and postpone moving green until later, in hopes that the board has gotten more crowded by then. In the board above, we would say that the cyan and blue flows have forced moves because there is only a single free space next to the current position of each one.

因此,我们现在应该选择蓝色或青色作为活动颜色,并将绿色推迟到以后,以希望届时董事会变得更加拥挤。 在上面的面板中,我们可以说青色和蓝色的流动已被迫移动,因为每个位置的当前位置旁边只有一个可用空间。

Restricting moves to a deterministically-chosen active color has another important side benefit: it prevents duplicate states from arising in the tree. Otherwise, we could imagine arriving at the same state via two different “routes” which differ only in the order the flows are created (say moving red-then-green vs. moving green-then-red).限制移动到确定性选择的活动颜色还有另一个重要的附带好处:它可以防止在树中出现重复状态。 否则,我们可以想象通过两条不同的“路线”到达同一状态,两条路线仅在创建流的顺序上有所不同(例如,先移动先红后绿,再先移动先绿后红)。

Heuristic search启发式搜索

Although we could use breadth-first search to solve the puzzle, I found that it’s significantly faster to go withbest-first search instead. The method I ended up using is kind of a poor man’s A* search in that it computes two quantities for each state xx:尽管我们可以使用广度优先搜索来解决这个难题,但我发现使用最佳优先搜索会明显更快。 我最终使用的方法有点像穷人的A *搜索,因为它为每个州xx计算两个数量:

  • a cost-to-come g(x)g(x) that considers all of the moves made to get to this state; and即将到来的成本g(x)g(x)考虑了为达到该状态而进行的所有移动; 和
  • a cost-to-go estimate h(x)h(x) that estimates the remaining “distance” to a goal state.估计到目标状态的剩余“距离”的成本估计值h(x)h(x)。

My program defines the cost-to-come g(x)g(x) by counting up the “action cost” of each move. Each move incurs one unit of cost, with two zero-cost exceptions: completing a flow, and forced moves.

我的程序通过计算每次移动的“动作成本”来定义成本g(x)g(x)。 每个移动产生一个单位成本,但有两个零成本例外:完成流程和强制移动。

The heuristic cost-to-go h(x)h(x) is simply the number of empty spaces remaining to be filled. In terms of A* search, this is an inadmissible heuristic for my cost function because it disregards the possibility of future zero-cost moves; nevertheless, it works pretty well in practice.启发式待销成本h(x)h(x)只是剩余要填充的空白空间的数量。 就A *搜索而言,这对于我的成本函数是不可取的启发,因为它忽略了未来零成本变动的可能性; 但是,它在实践中效果很好。

For best-first search, I put the nodes in a heap-based priority queue which is keyed on the total cost f(x)=g(x)+h(x)f(x)=g(x)+h(x) for each node xx. As new moves are generated, they are placed onto the queue; the next one to be dequeued will always be the one with the minimum f(x)f(x) value.对于最佳优先搜索,我将节点放在基于堆的优先级队列中,该队列以总成本f(x)= g(x)+ h(x)f(x)= g(x)+ h( x)每个节点xx。 随着新动作的产生,它们被放到队列中。 下一个要出队的将始终是具有最小f(x)f(x)值的那个。

As a concrete illustration, consider the tiny puzzle below:

作为一个具体的例子,请考虑以下小难题:

Red has made two moves, but the first was forced, so the total cost-to-come g(x)g(x) is 1. There are 8 empty squares remaining, so the cost-to-go h(x)h(x) is 8. Hence we see the total cost f(x)=8+1=9f(x)=8+1=9.

Red采取了两次行动,但第一个行动是被强制执行的,因此总获得成本g(x)g(x)为1。剩余8个空方块,因此,持续成本h(x)h (x)是8。因此,我们看到总成本f(x)= 8 + 1 = 9f(x)= 8 + 1 = 9。

Other forced moves 其他强制移动

Consider this puzzle state:考虑以下难题状态:

Given the moves made by blue, the only way that the free space to the right of the orange flow could get occupied is if orange moves into it. From this example, we can now say a move is forced:考虑到蓝色的移动,橙色流右边的自由空间可以被占用的唯一方法是将橙色移动到其中。 从这个例子中,我们现在可以说是强制移动了:

  • …if a flow has only one possible free cell to move into (the original definition); or,……如果一个流只有一个可能的自由单元进入(原始定义); 要么,

  • …if an empty space is adjacent to a single flow’s current position, and it has only one free neighbor.…如果单个流程的当前位置附近有一个空白空间,并且只有一个空闲邻居。

Recognizing the second situation is a bit more code-intensive than the first, but it’s worth it, because we are always looking for ways to reduce the branching factor. Without considering this type of forced move, it might seem like orange has three possible moves, but two of them are guaranteed to result in an awkwardly leftover empty square.

认识到第二种情况比第一种情况要花费更多的代码,但这是值得的,因为我们一直在寻找减少分支因子的方法。 如果不考虑这种强制移动,橙色似乎有3种可能的移动,但是其中有2种可以保证产生一个笨拙的剩余空方格。

Dead-end checks 死角检查

Sometimes, advancing a flow may leave behind a square that has a way into it, but no way out – for instance, the one marked with an “X” below:有时,前进的流程可能会留下一个可以进入其中但没有出路的正方形,例如,下面带有“ X”标记的正方形:

After any move – e.g. completing the red flow above – we can recognize nearby “dead-end” cells by looking for a free cell that is surrounded by three completed path segments or walls (the current position or goal position of an in-progress flow don’t count). Any state containing such a dead-end is unsolvable, and we need not continue searching any of its successors.任何举动之后-例如 完成上面的红色流程–通过查找被三个完整的路径段或墙包围的空闲单元,我们可以识别附近的“死角”单元(正在进行的流程的当前位置或目标位置不计算在内) 。 包含这种死胡同的任何状态都是无法解决的,我们无需继续搜索其任何后继状态。

Detecting unsolvable states early can prevent a lot of unnecessary work. The search might still eventually find a solution if we didn’t recognize and discard these, it might just take a lot more time and memory, exploring all possible successors of an unsolvable state.尽早发现无法解决的状态可以避免很多不必要的工作。 如果我们不识别并丢弃这些搜索,搜索可能最终仍会找到解决方案,这可能会花费更多的时间和内存,探索所有无法解决的状态的继任者。

Stranded colors and stranded regions 搁浅的颜色和搁浅的区域

It’s possible that extending one flow can totally block off another one, preventing its completion. This can be as simple as surrounding a flow’s current position so it has nowhere to go, like the poor yellow flow in this state:扩展一个流程可能会完全阻止另一流程,从而阻止其完成。 这可以像围绕流的当前位置一样简单,所以它无处可去,就像这种状态下的黄色流不畅一样:

In a more subtle case, one flow can divide up the free space so that it is impossible to connect another flow’s current position to its eventual goal, like blue depriving red in the image below:在更微妙的情况下,一个流可以划分出可用空间,从而不可能将另一个流的当前位置与其最终目标联系起来,例如下图中的蓝色剥夺了红色:

Finally, a move may create a bubble of freespace that is impossible to fill later, like the top-left 4x2 free area here:

最后,此举可能会产生一个气泡,此气泡以后将无法填充,例如此处左上方的4x2空闲区域:

Although red, cyan, and orange can all enter the 4x2 area, it would be useless for them to do so because they must terminate outside of it, which would be impossible.尽管红色,青色和橙色都可以进入4x2区域,但是这样做对他们来说是没有用的,因为它们必须在其外部终止,这是不可能的。

The former two illustrations are examples of stranded colors and the latter is an example of a stranded region. We can detect both using connected component labeling. We start by assigning each continuous region of free space a unique identifier, which can be done efficiently via a disjoint-set data structure. Here are the results for the two states above (color hatching corresponds to region labels):前两个插图是多色的示例,而后者是多色区域的示例。 我们可以使用连接的组件标签来检测两者。 我们首先为自由空间的每个连续区域分配一个唯一的标识符,这可以通过不相交的数据结构高效地完成。 以下是上述两种状态的结果(阴影线对应于区域标签):

To see which colors and regions might be stranded, we keep track of which current positions and which goal dots are horizontally or vertically adjacent to each region. Then the logic is:为了查看可能会滞留的颜色和区域,我们跟踪每个区域在水平或垂直方向上相邻的当前位置和目标点。 那么逻辑是:

  • For each non-completed color, there must exist a region which touches both its current position and goal position, otherwise the color is stranded.对于每种未完成的颜色,必须存在一个既触及其当前位置又触及目标位置的区域,否则该颜色会滞留。

  • For each distinct region, there must exist some color whose current position and goal position touch it, otherwise the region is stranded.对于每个不同的区域,必须存在某种颜色,其当前位置和目标位置会与之接触,否则该区域会滞留。

It’s important to consider the space/time tradeoff presented by validity checks like these. Even though connected component analysis is pretty fast, it’s many orders of magnitude slower than just checking if a move is permitted; hence, adding a validity check will only provide an overall program speedup if performing it is faster on average than expanding all successors of a given node. On the other hand, these types of validity checks pretty much always reduce the space needed for search, because successors of states found to be unsolvable will never be allocated.重要的是要考虑此类有效性检查所提供的时空权衡。 尽管关联组件分析非常快,但比仅检查是否允许移动要慢很多数量级; 因此,如果添加有效性检查的平均速度要比扩展给定节点的所有后继速度快,那么添加有效性检查将仅提供总体程序速度。 另一方面,这些类型的有效性检查几乎总是会减少搜索所需的空间,因为永远无法分配被发现无法解决的状态的后继者。

Chokepoint detection阻塞点检测

Let us define a bottleneck as a narrow region of freespace of a given width WW. If closing the bottleneck renders more than WW colors unsolvable (by separating their current and goal positions into distinct regions of freespace), then the bottleneck has become a chokepoint, and the puzzle can not be solved. Here’s a example to help explain:

让我们将瓶颈定义为给定宽度WW的自由空间的狭窄区域。 如果关闭瓶颈导致无法解决WW颜色(通过将其当前位置和目标位置划分为不同的自由空间区域),则瓶颈已成为瓶颈,并且难题无法解决。 这里有一个例子可以帮助解释:

The cells cross-hatched in gray constitute a bottleneck separating the current and goal positions of red, blue, yellow, cyan, and magenta. No matter where any of those five flows go, they will have to pass through the shaded cells in order to reach their goals; however, since the bottleneck is just three cells wide, we have a chokepoint and the puzzle is unsolvable from this state.灰色阴影线的单元格构成了一个瓶颈,将红色,蓝色,黄色,青色和洋红色的当前位置和目标位置分开。 无论这五个流中的任何一个流向何处,它们都必须穿过阴影单元才能达到目标。 但是,由于瓶颈只有3个单元宽,因此我们有一个瓶颈,从这个状态开始难题是无法解决的。

Fast-forwarding  快进

For this project, I chose to use a stack-based allocator to create nodes. Basically, a stack allocator is lovely when you want to be able to “undo” one or more memory allocations, and it dovetails perfectly with this search problem when it comes to handling forced moves.对于这个项目,我选择使用基于堆栈的分配器来创建节点。 基本上,当您希望能够“撤消”一个或多个内存分配时,堆栈分配器很不错,并且在处理强制移动时,它与该搜索问题完全吻合。

Let’s say we discover, upon entering a particular state, that there is a forced move. Suppose the state resulting from the forced move passes all validity checks. If we enqueue this successor, we know it will be dequeued immediately. Why? Since forced moves cost zero, the cost-to-come has not increased, but the cost-to-go has decreased by one. Why go through all of the trouble to enqueue and dequeue it? Why not just skip the queue entirely?假设我们进入特定状态后发现有强制移动。 假设强制移动产生的状态通过了所有有效性检查。 如果我们将后继者加入队列,我们知道它将立即出队。 为什么? 由于强制移动成本为零,因此成本不会增加,但成本却下降了一个。 为什么要经历所有麻烦才能入队和出队? 为什么不完全跳过队列?

Conversely, imagine that making the forced move leads to an unsolvable state. In this case, we might wish to “undo” the allocation of the successor node – after all, memory is limited, and we might want to use that slot of memory for a potentially-valid node later on!

相反,想象一下强制移动会导致无法解决的状态。 在这种情况下,我们可能希望“撤消”后继节点的分配–毕竟,内存是有限的,并且以后我们可能希望将该内存插槽用于可能有效的节点!

But this approach doesn’t just work for a single forced move. What if one forced move leads to another and another, starting a chain of forced moves? Once again, we should skip the queue for all of the intermediate states, only enqueuing the final result state which has no forced moves, and which passes all of the validity checks. On the other hand, if at any point we fail a validity check after a forced move, then we can de-allocate the entire chain, saving lots of memory. Here’s an example of such a chain:但是,这种方法不仅适用于一次强制移动。 如果一个强制移动导致另一个启动另一个强制移动怎么办? 再一次,我们应该跳过所有中间状态的队列,只排队没有强制移动且通过所有有效性检查的最终结果状态。 另一方面,如果在强制移动之后任何时候我们都没有通过有效性检查,那么我们可以取消分配整个链,节省大量内存。 这是此类链条的一个示例:

On the left, orange has just moved into the square adjacent to the cyan flow. Cyan must now follow the single-cell-wide passage until the flow is completed, creating a 2x1 stranded region that invalidates the entire chain; hence, we can de-allocate these two states along with all of the intermediate ones as well. For the purposes of memory usage, its as if none of these nodes ever existed.在左侧,橙色刚刚移到与青色流相邻的正方形中。 现在,Cyan必须遵循单细胞范围的通道,直到流程完成为止,并创建一个2x1的搁浅区域,使整个链无效。 因此,我们也可以取消分配这两个状态以及所有中间状态。 出于内存使用的目的,就好像这些节点都不存在一样。

Other whistles and bells 其他哨兵

If a node is invalid, we would like it to fail a validity check as quickly as possible. Tests costs time, and we will often see a program speedup if we run the cheap ones first. Accordingly, my solver orders validity checks from fastest to slowest, starting with dead ends, then stranded regions/colors, and finally bottlenecks.如果节点无效,我们希望它尽快通过有效性检查。 测试会花费时间,并且如果我们先运行便宜的程序,通常会看到程序加速。 因此,我的求解程序将有效性检查的顺序从最快到最慢,从死角开始,然后是滞留的区域/颜色,最后是瓶颈。

Viewed in this light, we can consider the dead-end check not just a way of verifying a node’s potential validity, but also as a test of whether we should allow the considerably more expensive connected component analysis code to execute at all. As a wise programmer observed, the fastest code is the code that never runs. Or by analogy to medicine, don’t schedule an MRI until after you’ve completed the physical examination!从这个角度来看,我们可以考虑进行死角检查,不仅是一种验证节点潜在有效性的方法,而且还可以作为一种测试,是否可以允许我们执行昂贵得多的连接组件分析代码。 正如一个明智的程序员所观察到的,最快的代码是永不运行的代码。 或类似于医学,在完成身体检查之前不要安排MRI!

There were a few other features which sped up puzzle solution, but which I won’t discuss in depth here. There is code to choose which dot of a pair should be a flow’s initial position and which one its goal; and other code to decide when to switch the active color. These little details can have a big impact on both runtime and memory use.还有其他一些功能可以加快拼图解决方案的速度,但是在这里我将不进行详细讨论。 有代码选择一对对中的哪个点应该是流的初始位置,以及它的目标是哪个。 和其他代码来决定何时切换活动颜色。 这些小细节会对运行时和内存使用产生很大影响。

There’s also a way to load hints into the solver, which helped during development when I came across a puzzle that was taking too much space and time to solve. The jumbo_14x14_19.txt puzzle was the bane of my existence for a few days, but finally became tractable after I added a few more features. This site was helpful to check my work, too.

还有一种将提示加载到求解器中的方法,这在开发过程中遇到了一个难题,该难题占用了太多的时间和空间来解决。 jumbo_14x14_19.txt难题是我存在几天的祸根,但是在我添加了更多功能之后,终于变得易于处理了。 这个站点也有助于检查我的工作。

As I discovered in grad school, a good way to devise validity checks is to visualize intermediate states after a search has failed – if I can figure out how express in code the reasons why some of them are in fact invalid, I can prevent them from ever clogging up the tree in the first place.正如我在研究生院发现的那样,进行有效性检查的一种好方法是可视化搜索失败后的中间状态-如果我能弄清楚如何以代码表达某些原因实际上是无效的原因,则可以防止它们 一开始就堵塞了树。

Wrapping up 包起来

In the end, the program I wrote turned out a bit long and a bit complicated.1 The good news is it that exposes just about every one of the features described above as a command-line switch – just run flow_solver --help to see them all.

最后,我编写的程序有点冗长和复杂。1好消息是它几乎将上述每一个功能作为命令行开关公开了–只需运行flow_solver --help即可查看 商场。

Also, as I was preparing to write up this post, this happened on Facebook:另外,当我准备撰写这篇文章时,这发生在Facebook上:

I want to say I was half-correct in my reply there – since I posted that last comment, I’ve gotten plenty of “replay value” out of trying to get the program to solve puzzles faster and better. So as a comparison, here’s a “scoreboard” of sorts showing the difference between the performance of the final program, and performance on August 21 – the day after this Facebook exchange – for all of the puzzles in the repository (lower is better in the “% of init” columns):

我想说的是我的回答是半正确的-自从我发布了最后一条评论以来,由于试图使程序更快更好地解决难题,我已经获得了很多“重播价值”。 因此,作为比较,以下是一个“记分板”,显示了最终程序的性能与8月21日(在Facebook交换后的第二天)的性能之间的差异,以反映存储库中的所有难题(在 “初始化的百分比”列):

Puzzle Init time Final time % of init Init nodes Final nodes % of init
regular_5x5_01.txt 0.001 0.001 100% 16 16 100%
regular_6x6_01.txt 0.001 0.001 100% 26 25 96%
regular_7x7_01.txt 0.001 0.001 100% 42 40 95%
regular_8x8_01.txt 0.001 0.001 100% 64 55 86%
regular_9x9_01.txt 0.001 0.001 100% 206 166 81%
extreme_8x8_01.txt 0.001 0.001 100% 289 76 26%
extreme_9x9_01.txt 0.001 0.001 100% 178 111 62%
extreme_9x9_30.txt 0.002 0.001 100% 565 146 26%
extreme_10x10_01.txt 0.037 0.034 92% 7,003 4,625 66%
extreme_10x10_30.txt 0.024 0.008 33% 3,659 1,069 29%
extreme_11x11_07.txt 1.290 0.021 2% 197,173 2,645 1%
extreme_11x11_15.txt 0.103 0.004 4% 13,289 502 4%
extreme_11x11_20.txt 0.731 0.001 < 1% 102,535 227 < 1%
extreme_11x11_30.txt 0.167 0.003 2% 21,792 442 2%
extreme_12x12_01.txt 2.664 0.211 8% 315,600 20,440 6%
extreme_12x12_02.txt 0.052 0.013 25% 6,106 1,408 23%
extreme_12x12_28.txt 2.142 0.823 38% 283,589 84,276 30%
extreme_12x12_29.txt 0.657 0.107 16% 85,906 12,417 14%
extreme_12x12_30.txt 8.977 0.002 < 1% 1,116,520 330 < 1%
jumbo_10x10_01.txt 0.001 0.001 100% 235 167 71%
jumbo_11x11_01.txt 0.014 0.001 7% 1,627 210 13%
jumbo_12x12_30.txt 0.052 0.002 4% 6,646 345 5%
jumbo_13x13_26.txt 0.385 0.149 39% 38,924 16,897 43%
jumbo_14x14_01.txt 0.015 0.003 20% 1,399 389 28%
jumbo_14x14_02.txt 254.903 0.886 < 1% 20,146,761 61,444 < 1%
jumbo_14x14_19.txt 300.415 1.238 < 1% 29,826,161 97,066 < 1%
jumbo_14x14_21.txt 16.184 0.018 < 1% 1,431,364 1,380 < 1%
jumbo_14x14_30.txt 50.818 1.558 3% 4,577,817 130,734 3%

Note that I allowed the initial version of the program to use up to 8 GB of RAM, and still ran out of memory solving jumbo_14x14_19.txt (as mentioned above, it was a thorny one). Other fun observations:

请注意,我允许程序的初始版本使用最多8 GB的RAM,但仍然无法解决jumbo_14x14_19.txt的内存不足(如上所述,这很棘手)。 其他有趣的观察结果:

  • The final version is always at least as efficient as the initial, in terms of both time and space.就时间和空间而言,最终版本始终至少与初始版本一样有效。

  • The average improvement (speedup/reduction in storage) from initial to final was over 10x!从最初到最终的平均改进(存储的加速/减少)超过10倍!

  • The final version solves each puzzle in under 1.6 seconds, using less than 140K nodes.最终版本使用不到140K的节点在1.6秒内解决了每个难题。

  • Am I happy with the final results? Definitely. Did I waste waaay too much time on this project? For sure. Did my fiancée start binge-watching Stranger Things on Netflix without me while I was obsessing over Flow Free? Sadly, yes.我对最终结果感到满意吗? 绝对是 我在这个项目上浪费了很多时间吗? 当然。 当我迷恋Flow Free时,我的未婚夫是否在没有我的情况下开始在Netflix上疯狂观看《怪异事物》? 可悲的是。

  • As usual, download the code yourself from github and give it a spin!和往常一样,您可以从github上自己下载代码,然后旋转一下!

Appendix: Prior work 附录:以前的工作

It turns out that I am unwittingly following in a long line of would-be Flow Free automators. Since I was in the air above southeast Asia when I started this project, I didn’t Google any of this, but now that I’m in documentation mode, here’s a chronological listing of what I could dig up:事实证明,我不知不觉地跟随着一连串可能成为Flow Free的自动化器。 由于我在开始这个项目时在东南亚上空飞行,因此我没有使用Google进行任何搜索,但是由于我处于文档编制模式,因此按时间顺序列出了我可以挖掘的内容:

  • Jun 2011: just a video of a solver proceeding at a leisurely pace, couldn’t find source.
  • Nov 2011: solver in C++ for a related puzzle, looks like a standard recursive backtracker.
  • Aug 2012: solver in Perl, mothballed, doesn’t work, but README has a link to an interesting paper.
  • Oct 2012: solver in C#, appears not to work based upon final commit message.
  • Feb 2013: chatting about designing solvers in a C++ forum.
  • Jul 2013: solver in R, framed as integer programming.2
  • Sep 2013: two solvers from the same programmer, both in C#, both use DLX; author notes long solve times for large grids.
  • Feb 2014: solver as coding assignment in a C++ class, yikes.
  • Mar 2014: StackOverflow chat on designing a solver, top answer suggests reducing to SAT.3
  • Feb 2016: solver in MATLAB using backtracking; very slow on puzzles >10x10 but it does live image capture and there’s a cool YouTube demo.

Without running the solvers listed here, it’s difficult to compare my performance to theirs, but I’d be willing to hazard an educated guess that mine is pretty speedy compared to the others, especially on large puzzles.如果不运行此处列出的求解器,很难将我的性能与他们的性能进行比较,但是我愿意冒险警告说,我的性能比其他方法快得多,尤其是在大型难题上。

代码地址:https://github.com/mzucker/flow_solver

#include <stdio.h>
#include <stdint.h>
#include <locale.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>
#include <math.h>
#include <time.h>#ifndef _WIN32
#include <unistd.h>
#include <sys/time.h>
#else
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif// Positions are 8-bit integers with 4 bits each for y, x.
enum {// Number to represent "not found"INVALID_POS = 0xff,// Maximum # of colors in a puzzleMAX_COLORS = 16,// Maximum valid size of a puzzleMAX_SIZE = 15,// Maximum # cells in a valid puzzle -- since we just use bit// shifting to do x/y, need to allocate space for 1 unused column.MAX_CELLS = (MAX_SIZE+1)*MAX_SIZE-1,// One million(ish) bytesMEGABYTE = 1024*1024,};// Various cell types, all but freespace have a color
enum {TYPE_FREE = 0, // Free spaceTYPE_PATH = 1, // Path between init & goalTYPE_INIT = 2, // Starting pointTYPE_GOAL = 3  // Goal position
};// Enumerate cardinal directions so we can loop over them
// RIGHT is increasing x, DOWN is increasing y.
enum {DIR_LEFT  = 0,DIR_RIGHT = 1,DIR_UP    = 2,DIR_DOWN  = 3
};// Search termination results
enum {SEARCH_SUCCESS = 0,SEARCH_UNREACHABLE = 1,SEARCH_FULL = 2,SEARCH_IN_PROGRESS = 3,
};// Represent the contents of a cell on the game board
typedef uint8_t cell_t;// Represent a position within the game board
typedef uint8_t pos_t;// Match color characters to ANSI color codes
typedef struct color_lookup_struct {char input_char;   // Color characterchar display_char; // Punctuation a la nethackconst char* ansi_code;    // ANSI color codeconst char* fg_rgb;const char* bg_rgb;
} color_lookup_t;// Options for this program
typedef struct options_struct {int    display_quiet;int    display_diagnostics;int    display_animate;int    display_color;int    display_fast;int    display_save_svg;int    node_check_touch;int    node_check_stranded;int    node_check_deadends;int    node_bottleneck_limit;int    node_penalize_exploration;int    order_autosort_colors;int    order_most_constrained;int    order_forced_first;int    order_random;int    search_best_first;int    search_outside_in;size_t search_max_nodes;double search_max_mb;int    search_fast_forward;} options_t;// Static information about a puzzle layout -- anything that does not
// change as the puzzle is solved is stored here.
typedef struct game_info_struct {// Index in color_dict table of codesint    color_ids[MAX_COLORS];// Color orderint    color_order[MAX_COLORS];// Initial and goal positionspos_t  init_pos[MAX_COLORS];pos_t  goal_pos[MAX_COLORS];// Length/width of game boardsize_t size;// Number of colors presentsize_t num_colors;// Color table for looking up color IDuint8_t color_tbl[128];// Was user order specified?int user_order;} game_info_t;// Incremental game state structure for solving -- this is what gets
// written as the search progresses, one state per search node
typedef struct game_state_struct {// State of each cell in the world; a little wasteful to duplicate,// since only one changes on each move, but necessary for BFS or A*// (would not be needed for depth-first search).cell_t   cells[MAX_CELLS];// Head positionpos_t    pos[MAX_COLORS];// How many free cells?uint8_t  num_free;// Which was the last color / endpointuint8_t  last_color;// Bitflag indicating whether each color has been completed or not// (cur_pos is adjacent to goal_pos).uint16_t completed;} game_state_t;// Used for auto-sorting colors
typedef struct color_features_struct {int index;int user_index;int wall_dist[2];int min_dist;
} color_features_t;// Disjoint-set data structure for connected component analysis of free
// space (see region_ functions).
typedef struct region_struct {// Parent index (or INVALID_POS) if no non-free spacepos_t parent;// Rank (see wikipedia article).uint8_t rank;
} region_t;// Search node for A* / BFS.
typedef struct tree_node_struct {game_state_t state;              // Current game statedouble cost_to_come;             // Cost to come (ignored for BFS)double cost_to_go;               // Heuristic cost (ignored for BFS)struct tree_node_struct* parent; // Parent of this node (may be NULL)
} tree_node_t;// Strategy is to pre-allocate a big block of nodes in advance, and
// hand them out in order.
typedef struct node_storage_struct {tree_node_t* start; // Allocated blocksize_t capacity;    // How many nodes did we allocate?size_t count;       // How many did we give out?
} node_storage_t;// Data structure for heap based priority queue
typedef struct heapq_struct {tree_node_t** start; // Array of node pointerssize_t capacity;     // Maximum allowable queue sizesize_t count;        // Number enqueued
} heapq_t;// First in, first-out queue implemented as an array of pointers.
typedef struct fifo_struct {tree_node_t** start; // Array of node pointerssize_t capacity;     // Maximum number of things to enqueue eversize_t count;        // Total enqueued (next one will go into start[count])size_t next;         // Next index to dequeue
} fifo_t;// Union struct for passing around queues.
typedef union queue_union {heapq_t heapq;fifo_t  fifo;
} queue_t;// Function pointers for either type of queue
queue_t (*queue_create)(size_t) = 0;
void (*queue_enqueue)(queue_t*, tree_node_t*) = 0;
tree_node_t* (*queue_deque)(queue_t*) = 0;
void (*queue_destroy)(queue_t*) = 0;
int (*queue_empty)(const queue_t*) = 0;
const tree_node_t* (*queue_peek)(const queue_t*) = 0;//// For succinct printing of search results
const char SEARCH_RESULT_CHARS[4] = "suf?";// For verbose printing of search results
const char* SEARCH_RESULT_STRINGS[4] = {"successful","unsolvable","out of memory","in progress"
};// Was gonna try some unicode magic but meh
const char* BLOCK_CHAR = "#";// For visualizing cardinal directions ordered by the enum above.
const char DIR_CHARS[4] = "<>^v";// x, y, pos coordinates for each direction
const int DIR_DELTA[4][3] = {{ -1, 0, -1 },{  1, 0,  1 },{  0, -1, -16 },{  0, 1, 16 }
};// Look-up table mapping characters in puzzle definitions to
// output char, ANSI color, foreground/background RGB
const color_lookup_t color_dict[MAX_COLORS] = {{ 'R', 'o', "101", "ff0000", "723939" }, // red{ 'B', '+', "104", "0000ff", "393972" }, // blue{ 'Y', '@', "103", "eeee00", "6e6e39" }, // yellow{ 'G', '*',  "42", "008100", "395539" }, // green{ 'O', 'x',  "43", "ff8000", "725539" }, // orange{ 'C', '%', "106", "00ffff", "397272" }, // cyan{ 'M', '?', "105", "ff00ff", "723972" }, // magenta{ 'm', 'v',  "41", "a52a2a", "5f4242" }, // maroon{ 'P', '^',  "45", "800080", "553955" }, // purple{ 'A', '=', "100", "a6a6a6", "5f5e5f" }, // gray{ 'W', '~', "107", "ffffff", "727272" }, // white{ 'g', '-', "102", "00ff00", "397239" }, // bright green{ 'T', '$',  "47", "bdb76b", "646251" }, // tan{ 'b', '"',  "44", "00008b", "393958" }, // dark blue{ 'c', '&',  "46", "008180", "395555" }, // dark cyan{ 'p', '.',  "35", "ff1493", "72415a" }, // pink?
};// Global options struct gets setup during main
options_t g_options;//
// Return the current time as a double. Don't actually care what zero
// is cause we will just offset.double now() {#ifdef _WIN32union {LONG_LONG ns100; /*time since 1 Jan 1601 in 100ns units */FILETIME ft;} now;GetSystemTimeAsFileTime (&now.ft);return (double)now.ns100 * 1e-7; // 100 nanoseconds = 0.1 microsecond
#elsestruct timeval tv;gettimeofday(&tv, 0);return (double)tv.tv_sec + (double)tv.tv_usec * 1e-6;
#endif}//
// Peform lookup in color_dict aboveint get_color_id(char c) {for (int i=0; i<MAX_COLORS; ++i) {if (color_dict[i].input_char == c) {return i;}}return -1;
}//
// Detect whether terminal supports color & cursor commandsint terminal_has_color() {#ifdef _WIN32return 0;#elseif (!isatty(STDOUT_FILENO)) {return 0;} char* term = getenv("TERM");if (!term) { return 0; }return strstr(term, "xterm") == term || strstr(term, "rxvt") == term;#endif}//
// Emit color string for index into color_dict table aboveconst char* color_char(const char* ansi_code, char color_out, char mono_out) {static char buf[256];if (g_options.display_color) {snprintf(buf, 256, "\033[30;%sm%c\033[0m",ansi_code, color_out);} else {snprintf(buf, 256, "%c", mono_out);}return buf;}//
// Clear screen and set cursor pos to 0,0const char* unprint_board(const game_info_t* info) {if (g_options.display_color) {static char buf[256];snprintf(buf, 256, "\033[%zuA\033[%zuD",info->size+2, info->size+2);return buf;} else {return "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";}
}//
// Create a delayvoid delay_seconds(double s) {if (g_options.display_fast) { s /= 4.0; }
#ifdef _WIN32// TODO: find win32 equivalent of usleep?
#elseusleep((size_t)(s * 1e6));
#endif
}//
// Create a 8-bit position from 2 4-bit x,y coordinatespos_t pos_from_coords(pos_t x, pos_t y) {return ((y & 0xf) << 4) | (x & 0xf);
}//
// Split 8-bit position into 4-bit x & y coordsvoid pos_get_coords(pos_t p, int* x, int* y) {*x = p & 0xf;*y = (p >> 4) & 0xf;
}//
// Are the coords on the map?int coords_valid(const game_info_t* info,int x, int y) {return (x >= 0 && x < (int)info->size &&y >= 0 && y < (int)info->size);}//
// Compote an offset as a position and return whether valid or notpos_t offset_pos(const game_info_t* info,int x, int y, int dir) {int offset_x = x + DIR_DELTA[dir][0];int offset_y = y + DIR_DELTA[dir][1];return coords_valid(info, offset_x, offset_y) ?pos_from_coords(offset_x, offset_y) : INVALID_POS;}//
// Compote an offset as a position and return whether valid or notpos_t pos_offset_pos(const game_info_t* info,pos_t pos, int dir) {int x, y;pos_get_coords(pos, &x, &y);return offset_pos(info, x, y, dir);}//
// Get the distance from the wall for x, y coordsint get_wall_dist(const game_info_t* info,int x, int y) {int p[2] = { x, y };int d[2];for (int i=0; i<2; ++i) {int d0 = p[i];int d1 = info->size - 1 - p[i];d[i] = d0 < d1 ? d0 : d1;}return d[0] < d[1] ? d[0] : d[1];}//
// Get the distance from the wall for 8-bit positionint pos_get_wall_dist(const game_info_t* info,pos_t pos) {int x, y;pos_get_coords(pos, &x, &y);return get_wall_dist(info, x, y);}//
// Create a cell from a 2-bit type, a 4-bit color, and a 2-bit
// direction.cell_t cell_create(uint8_t type, uint8_t color, uint8_t dir) {return ((color & 0xf) << 4) | ((dir & 0x3) << 2) | (type & 0x3);
}//
// Get the type from a cell valueuint8_t cell_get_type(cell_t c) {return c & 0x3;
}//
// Get the direction from a cell valueuint8_t cell_get_direction(cell_t c) {return (c >> 2) & 0x3;
}//
// Get the color from a cell valueuint8_t cell_get_color(cell_t c) {return (c >> 4) & 0xf;
}//
// For displaying a color nicelyconst char* color_name_str(const game_info_t* info,int color) {const color_lookup_t* l = &color_dict[info->color_ids[color]];return color_char(l->ansi_code, l->input_char, l->display_char);}//
// For displaying a cell nicelyconst char* color_cell_str(const game_info_t* info,cell_t cell) {int type = cell_get_type(cell);int color = cell_get_color(cell);int dir = cell_get_direction(cell);const color_lookup_t* l = &color_dict[info->color_ids[color]];switch (type) {case TYPE_FREE:return " ";break;case TYPE_PATH:return color_char(l->ansi_code,DIR_CHARS[dir],l->display_char);break;default:return color_char(l->ansi_code,(type == TYPE_INIT ? 'o' : 'O'),l->display_char);}}//
// Consider whether the given move is valid.int game_can_move(const game_info_t* info,const game_state_t* state,int color, int dir) {// Make sure color is validassert(color < info->num_colors);assert(!(state->completed & (1 << color)));// Get cur pos x, yint cur_x, cur_y;pos_get_coords(state->pos[color], &cur_x, &cur_y);// Get new x, yint new_x = cur_x + DIR_DELTA[dir][0];int new_y = cur_y + DIR_DELTA[dir][1];// If outside bounds, not legalif (new_x < 0 || new_x >= info->size ||new_y < 0 || new_y >= info->size) {return 0;}// Create a new positionpos_t new_pos = pos_from_coords(new_x, new_y);assert( new_pos < MAX_CELLS );if (!g_options.node_check_touch &&new_pos == info->goal_pos[color]) {return 1;}// Must be empty (TYPE_FREE)if (state->cells[new_pos]) {return 0;}if (g_options.node_check_touch) {// All puzzles are designed so that a new path segment is adjacent// to at most one path segment of the same color -- the predecessor// to the new segment. We check this by iterating over the// neighbors.for (int dir=0; dir<4; ++dir) {// Assemble positionpos_t neighbor_pos = offset_pos(info, new_x, new_y, dir);// If valid non-empty cell and not cur_pos and not goal_pos and// has our color, then failif (neighbor_pos != INVALID_POS && state->cells[neighbor_pos] &&neighbor_pos != state->pos[color] && neighbor_pos != info->goal_pos[color] && cell_get_color(state->cells[neighbor_pos]) == color) {return 0;}}}// It's validreturn 1;}//
// Print out game board as SVGvoid game_print_svg(FILE* fp,const game_info_t* info,const game_state_t* state) {size_t display_size = 256;size_t m = 1;size_t cell_size = (display_size - m * (info->size + 1)) / info->size;int xy_skip = cell_size + m;double dot_radius = cell_size * 0.35;double path_radius = cell_size * 0.35;display_size = xy_skip * info->size + m;fprintf(fp, "<svg xmlns=\"http://www.w3.org/2000/svg\" ""width=\"%zu\" height=\"%zu\">\n",display_size, display_size);fprintf(fp, "  <rect width=\"%zu\" height=\"%zu\" ""style=\"fill: #7b7c41;\" />\n",display_size, display_size);for (size_t y=0; y<info->size; ++y) {size_t display_y = m+xy_skip*y;for (size_t x=0; x<info->size; ++x) {size_t display_x = m+xy_skip*x;pos_t pos = pos_from_coords(x,y);cell_t cell = state->cells[pos];int color = cell_get_color(cell);int type  = cell_get_type(cell);const char* cell_bg = "000000";if (cell) {if (type == TYPE_PATH ||(type == TYPE_INIT) ||(type == TYPE_GOAL && (state->completed & (1 << color)))) {cell_bg = color_dict[info->color_ids[color]].bg_rgb;} }fprintf(fp, "  <rect x=\"%zu\" y=\"%zu\" ""width=\"%zu\" height=\"%zu\" ""style=\"fill: #%s;\" />\n",display_x, display_y, cell_size, cell_size, cell_bg);if (type == TYPE_INIT || type == TYPE_GOAL) {double center_x = display_x + 0.5*cell_size;double center_y = display_y + 0.5*cell_size;fprintf(fp, "  <circle cx=\"%g\" cy=\"%g\" ""r=\"%g\" style=\"fill: #%s;\" />\n",center_x, center_y, dot_radius,color_dict[info->color_ids[color]].fg_rgb);}}}for (int color=0; color<info->num_colors; ++color) {pos_t pos = (state->completed & (1 << color)) ?info->goal_pos[color] : state->pos[color];if (pos == info->init_pos[color]) { continue; }int x, y;pos_get_coords(pos, &x, &y);double px = m + xy_skip*x + 0.5*cell_size;double py = m + xy_skip*y + 0.5*cell_size;fprintf(fp, "  <path d=\"M %g,%g ", px, py);while (1) {cell_t cell = state->cells[pos];assert( cell_get_color(cell) == color );int dir = cell_get_direction(cell);dir ^= 1; // flip directionif (dir == DIR_LEFT || dir == DIR_RIGHT) {fprintf(fp, "h %d ", dir == DIR_LEFT ? -xy_skip : xy_skip);} else {fprintf(fp, "v %d ", dir == DIR_UP ? -xy_skip : xy_skip);}int npos = pos_offset_pos(info, pos, dir);if (npos == INVALID_POS) { break; }pos = npos;if (pos == info->init_pos[color]) {break;}}fprintf(fp, " \" style=\"stroke: #%s; stroke-width: %g; ""fill: none; stroke-linecap: round\" />\n",color_dict[info->color_ids[color]].fg_rgb,path_radius);}fprintf(fp, "</svg>\n");}//
// Thin wrapper on above.void game_save_svg(const char* filename,const game_info_t* info,const game_state_t* state) {FILE* fp = fopen(filename, "w");if (fp) {game_print_svg(fp, info, state);fclose(fp);}}//
// Print out game boardvoid game_print(const game_info_t* info,const game_state_t* state) {printf("%s", BLOCK_CHAR);for (size_t x=0; x<info->size; ++x) {printf("%s", BLOCK_CHAR);}printf("%s\n", BLOCK_CHAR);for (size_t y=0; y<info->size; ++y) {printf("%s", BLOCK_CHAR);for (size_t x=0; x<info->size; ++x) {cell_t cell = state->cells[pos_from_coords(x, y)];printf("%s", color_cell_str(info, cell));}printf("%s\n", BLOCK_CHAR);}printf("%s", BLOCK_CHAR);for (size_t x=0; x<info->size; ++x) {printf("%s", BLOCK_CHAR);}printf("%s\n", BLOCK_CHAR);}//
// Return the number of free spaces around an x, y positionint game_num_free_coords(const game_info_t* info,const game_state_t* state,int x, int y) {int num_free = 0;for (int dir=0; dir<4; ++dir) {pos_t neighbor_pos = offset_pos(info, x, y, dir);if (neighbor_pos != INVALID_POS &&state->cells[neighbor_pos] == 0) {++num_free;}}return num_free;}//
// Return the number of free spaces around an 8-bit positionint game_num_free_pos(const game_info_t* info,const game_state_t* state,pos_t pos) {int x, y;pos_get_coords(pos, &x, &y);return game_num_free_coords(info, state, x, y);}//
// Update the game state to make the given move.double game_make_move(const game_info_t* info,game_state_t* state, int color, int dir, int forced) {// Make sure valid colorassert(color < info->num_colors);// Update the cell with the new cell valuecell_t move = cell_create(TYPE_PATH, color, dir);// Get current x, yint cur_x, cur_y;pos_get_coords(state->pos[color], &cur_x, &cur_y);// Assemble new x, yint new_x = cur_x + DIR_DELTA[dir][0];int new_y = cur_y + DIR_DELTA[dir][1];// Make sure validassert( new_x >= 0 && new_x < (int)info->size &&new_y >= 0 && new_y < (int)info->size );// Make positionpos_t new_pos = pos_from_coords(new_x, new_y);assert( new_pos < MAX_CELLS );if (!g_options.node_check_touch && new_pos == info->goal_pos[color]) {state->cells[info->goal_pos[color]] = cell_create(TYPE_GOAL, color, dir);state->completed |= 1 << color;return 0;}// Make sure it's emptyassert( state->cells[new_pos] == 0 );// Update cells and new posstate->cells[new_pos] = move;state->pos[color] = new_pos;--state->num_free;state->last_color = color;double action_cost = 1;int goal_dir = -1;if (g_options.node_check_touch) {for (int dir=0; dir<4; ++dir) {if (offset_pos(info, new_x, new_y, dir) == info->goal_pos[color]) {goal_dir = dir;break;}}}if (goal_dir >= 0) {state->cells[info->goal_pos[color]] = cell_create(TYPE_GOAL, color, goal_dir);state->completed |= (1 << color);action_cost = 0;} else {int num_free = game_num_free_coords(info, state,new_x, new_y);if (g_options.node_penalize_exploration && num_free == 2) {action_cost = 2;}}if (forced) {action_cost = 0;}return action_cost;}//
// Helper function for below.int detect_format(FILE* fp) {int max_letter = 'A';int c;while ((c = fgetc(fp)) != EOF) {if (isalpha(c) && c > max_letter) {max_letter = c;}}rewind(fp);return (max_letter - 'A') < MAX_COLORS;}//
// Read game board from text fileint game_read(const char* filename,game_info_t* info,game_state_t* state) {FILE* fp = fopen(filename, "r");if (!fp) {fprintf(stderr, "error opening %s\n", filename);return 0;}int is_alternate_format = detect_format(fp);memset(info, 0, sizeof(game_info_t));memset(state, 0, sizeof(game_state_t));memset(state->pos, 0xff, sizeof(state->pos));state->last_color = MAX_COLORS;size_t y=0;char buf[MAX_SIZE+2];memset(info->color_tbl, 0xff, sizeof(info->color_tbl));memset(info->init_pos, 0xff, sizeof(info->init_pos));memset(info->goal_pos, 0xff, sizeof(info->goal_pos));while (info->size == 0 || y < info->size) {char* s = fgets(buf, MAX_SIZE+1, fp);size_t l = s ? strlen(s) : 0;if (!s) {fprintf(stderr, "%s:%zu: unexpected EOF\n", filename, y+1);fclose(fp);return 0;} else if (s[l-1] != '\n') {fprintf(stderr, "%s:%zu line too long\n", filename, y+1);fclose(fp);return 0;}if (l >= 2 && s[l-2] == '\r') { // DOS line endings--l;}if (info->size == 0) {if (l < 3) {fprintf(stderr, "%s:1: expected at least 3 characters before newline\n",filename);fclose(fp);return 0;} else if (l-1 > MAX_SIZE) {fprintf(stderr, "%s:1: size too big!\n", filename);fclose(fp);return 0;}info->size = l-1;} else if (l != info->size + 1) {fprintf(stderr, "%s:%zu: wrong number of characters before newline ""(expected %zu, but got %zu)\n",filename, y+1,info->size, l-1);fclose(fp);return 0;}for (size_t x=0; x<info->size; ++x) {uint8_t c = s[x];if (isalpha(c)) {pos_t pos = pos_from_coords(x, y);assert(pos < MAX_CELLS);int color = info->color_tbl[c];if (color >= info->num_colors) {color = info->num_colors;if (info->num_colors == MAX_COLORS) {fprintf(stderr, "%s:%zu: can't use color %c""- too many colors!\n",filename, y+1, c);fclose(fp);return 0;}int id = is_alternate_format ? (c - 'A') : get_color_id(c);if (id < 0 || id >= MAX_COLORS) {fprintf(stderr, "%s:%zu: unrecognized color %c\n",filename, y+1, c);fclose(fp);return 0;}info->color_ids[color] = id;info->color_order[color] = color;++info->num_colors;info->color_tbl[c] = color;info->init_pos[color] = state->pos[color] = pos;state->cells[pos] = cell_create(TYPE_INIT, color, 0);} else {if (info->goal_pos[color] != INVALID_POS) {fprintf(stderr, "%s:%zu too many %c already!\n",filename, y+1, c);fclose(fp);return 0;}info->goal_pos[color] = pos;state->cells[pos] = cell_create(TYPE_GOAL, color, 0);}} else {++state->num_free;}}++y;}fclose(fp);if (!info->num_colors) {fprintf(stderr, "empty map!\n");return 0;}for (size_t color=0; color<info->num_colors; ++color) {if (info->goal_pos[color] == INVALID_POS) {game_print(info, state);fprintf(stderr, "\n\n%s: color %s has start but no end\n",filename,color_name_str(info, color));return 0;}if (g_options.search_outside_in) {int init_dist = pos_get_wall_dist(info, info->init_pos[color]);int goal_dist = pos_get_wall_dist(info, info->goal_pos[color]);if (goal_dist < init_dist) {pos_t tmp_pos = info->init_pos[color];info->init_pos[color] = info->goal_pos[color];info->goal_pos[color] = tmp_pos;state->cells[info->init_pos[color]] = cell_create(TYPE_INIT, color, 0);state->cells[info->goal_pos[color]] = cell_create(TYPE_GOAL, color, 0);state->pos[color] = info->init_pos[color];}}}return 1;}//
// Read hint fileint game_read_hint(const game_info_t* info,const game_state_t* state,const char* filename,uint8_t hint[MAX_CELLS]) {memset(hint, 0xff, MAX_CELLS);FILE* fp = fopen(filename, "r");if (!fp) {fprintf(stderr, "error opening %s\n", filename);return 0;}char buf[MAX_SIZE+2];for (size_t y=0; y<info->size; ++y) {char* s = fgets(buf, info->size+2, fp);if (!s) { break; }size_t l = strlen(s);if (l > info->size+1 || s[l-1] != '\n') {fprintf(stderr, "%s:%zu: line too long!\n", filename, y+1);fclose(fp);return 0;}for (size_t x=0; x<l-1; ++x) {uint8_t c = buf[x];if (isalpha(c)) {int color = c > 127 ? 0xff : info->color_tbl[c];if (color >= info->num_colors) {fprintf(stderr, "%s:%zu: color %c not found!\n", filename, y+1, c);fclose(fp);return 0;}int pos = pos_from_coords(x, y);hint[pos] = color;}}}fclose(fp);return 1;}//
// Pick the next color to move deterministicallyint game_next_move_color(const game_info_t* info,const game_state_t* state) {size_t last_color = state->last_color;if (last_color < info->num_colors &&!(state->completed & (1 << last_color))) {return last_color;}if (!info->user_order &&g_options.order_most_constrained) {size_t best_color = -1;int best_free = 4;/*size_t worst_color = -1;int worst_free = 0;*/for (size_t i=0; i<info->num_colors; ++i) {int color = info->color_order[i];if (state->completed & (1 << color)) {continue;}int num_free = game_num_free_pos(info, state,state->pos[color]);if (num_free < best_free) {best_free = num_free;best_color = color;}/*if (num_free > worst_free) {worst_free = num_free;worst_color = color;}*/}/*if (best_free == 1 && worst_free == 4) {game_print(info, state);printf("color %s has %d free",color_name_str(info, best_color), best_free);printf(" and %s has %d free\n",color_name_str(info, worst_color), worst_free);exit(0);}*/assert(best_color < info->num_colors);return best_color;} else {for (size_t i=0; i<info->num_colors; ++i) {int color = info->color_order[i];if (state->completed & (1 << color)) { continue; }return color;}assert(0 && "unreachable code");return -1;} }//
// Compare 2 intsint cmp(int a, int b) {return a < b ? -1 : a > b ? 1 : 0;
}//
// For sorting colorsint color_features_compare(const void* vptr_a, const void* vptr_b) {const color_features_t* a = (const color_features_t*)vptr_a;const color_features_t* b = (const color_features_t*)vptr_b;int u = cmp(a->user_index, b->user_index);if (u) { return u; }int w = cmp(a->wall_dist[0], b->wall_dist[0]);if (w) { return w; }int g = -cmp(a->wall_dist[1], b->wall_dist[1]);if (g) { return g; }return -cmp(a->min_dist, b->min_dist);}//
// Place the game colors into a set ordervoid game_order_colors(game_info_t* info,game_state_t* state,const char* user_order) {if (g_options.order_random) {srand(now() * 1e6);for (size_t i=info->num_colors-1; i>0; --i) {size_t j = rand() % (i+1);int tmp = info->color_order[i];info->color_order[i] = info->color_order[j];info->color_order[j] = tmp;}} else { // not randomcolor_features_t cf[MAX_COLORS];memset(cf, 0, sizeof(cf));for (size_t color=0; color<info->num_colors; ++color) {cf[color].index = color;cf[color].user_index = MAX_COLORS;}if (g_options.order_autosort_colors) {for (size_t color=0; color<info->num_colors; ++color) {int x[2], y[2];for (int i=0; i<2; ++i) {pos_get_coords(state->pos[color], x+i, y+i);cf[color].wall_dist[i] = get_wall_dist(info, x[i], y[i]);}int dx = abs(x[1]-x[0]);int dy = abs(y[1]-y[0]);cf[color].min_dist = dx + dy;}}if (user_order) {for (size_t k=0; user_order[k]; ++k) {uint8_t c = user_order[k];int color = c < 127 ? info->color_tbl[c] : MAX_COLORS;if (color >= info->num_colors) {fprintf(stderr, "error ordering colors: %c not in puzzle\n", c);exit(1);}if (cf[color].user_index < info->num_colors) {fprintf(stderr, "error ordering colors: %c already used\n", c);exit(1);}cf[color].user_index = k;}info->user_order = 1;}mergesort(cf, info->num_colors, sizeof(color_features_t),color_features_compare);for (size_t i=0; i<info->num_colors; ++i) {info->color_order[i] = cf[i].index;}}if (!g_options.display_quiet) {if (g_options.order_most_constrained && !user_order) {printf("will choose color by most constrained\n");} else {printf("will choose colors in order: ");for (size_t i=0; i<info->num_colors; ++i) {int color = info->color_order[i];printf("%s", color_name_str(info, color));}printf("\n");}}}//
// This and other connected component analysis functions from
// https://en.wikipedia.org/wiki/Disjoint-set_data_structure
//
// Create a new set with a single member:region_t region_create(pos_t pos) {region_t rval;rval.parent = pos;rval.rank = 0;return rval;
}//
// Loop up root region for the given index ppos_t region_find(region_t* regions, pos_t p) {if (regions[p].parent != INVALID_POS &&regions[p].parent != p) {assert(p != INVALID_POS);assert(p < MAX_CELLS);regions[p].parent = region_find(regions, regions[p].parent);}return regions[p].parent;
}//
// Merge two regionsvoid region_unite(region_t* regions,pos_t a, pos_t b) {pos_t root_a = region_find(regions, a);pos_t root_b = region_find(regions, b);if (root_a == root_b) { return; }if (regions[root_a].rank < regions[root_b].rank) {regions[root_a].parent = root_b;} else if (regions[root_a].rank > regions[root_b].rank) {regions[root_b].parent = root_a;} else {regions[root_b].parent = root_a;++regions[root_a].rank;}}//
// Perform connected components analysis on game board. This is a
// 2-pass operation: one to build and merge the disjoint-set data
// structures, and another to re-index them so each unique region of
// free space gets its own index, starting at zero. Returns the number
// of freespace regions.size_t game_build_regions(const game_info_t* info,const game_state_t* state,uint8_t rmap[MAX_CELLS]) {region_t regions[MAX_CELLS];// 1 pass to build regionsfor (size_t y=0; y<info->size; ++y) {for (size_t x=0; x<info->size; ++x) {pos_t pos = pos_from_coords(x, y);if (state->cells[pos]) {regions[pos] = region_create(INVALID_POS);} else {regions[pos] = region_create(pos);if (x) {pos_t pl = pos_from_coords(x-1, y);if (!state->cells[pl]) {region_unite(regions, pos, pl);}}if (y) {pos_t pu = pos_from_coords(x, y-1);if (!state->cells[pu]) {region_unite(regions, pos, pu);}}}}}uint8_t rlookup[MAX_CELLS];size_t rcount = 0;memset(rlookup, 0xff, sizeof(rlookup));memset(rmap, 0xff, MAX_CELLS);// 2nd pass to order regionsfor (size_t y=0; y<info->size; ++y) {for (size_t x=0; x<info->size; ++x) {pos_t pos = pos_from_coords(x, y);pos_t root = region_find(regions, pos);if (root != INVALID_POS) {if (rlookup[root] == INVALID_POS) {rlookup[root] = rcount++;}rmap[pos] = rlookup[root];} else {rmap[pos] = INVALID_POS;}}}return rcount;}//
// Helper function for game_regions_ok below -- this is used to add
// the current color bit flag to the regions adjacent to the current
// or goal position. void game_regions_add_color(const game_info_t* info,const game_state_t* state,const uint8_t rmap[MAX_CELLS],pos_t pos,uint16_t cflag,uint16_t* rflags) {for (int dir=0; dir<4; ++dir) {pos_t neighbor_pos = pos_offset_pos(info, pos, dir);if (neighbor_pos != INVALID_POS) {// find out what region it is inint neighbor_region = rmap[neighbor_pos];// if it is in a valid regionif (neighbor_region != INVALID_POS) {// add this color to the regionrflags[neighbor_region] |= cflag;}}}}//
// Helper function for belowint game_is_deadend(const game_info_t* info,const game_state_t* state,pos_t pos) {assert(pos != INVALID_POS && !state->cells[pos]);int x, y;pos_get_coords(pos, &x, &y);int num_free = 0;for (int dir=0; dir<4; ++dir) {pos_t neighbor_pos = offset_pos(info, x, y, dir);if (neighbor_pos != INVALID_POS) {if (!state->cells[neighbor_pos]) {++num_free;} else {for (size_t color=0; color<info->num_colors; ++color) {if (state->completed & (1 << color)) {continue;}if (neighbor_pos == state->pos[color] ||neighbor_pos == info->goal_pos[color]) {++num_free;}}}}}return num_free <= 1;}//
// Check for dead-end regions of freespace where there is no way to
// put an active path into and out of it. Any freespace node which
// has only one free neighbor represents such a dead end. For the
// purposes of this check, cur and goal positions count as "free".int game_check_deadends(const game_info_t* info,const game_state_t* state) {size_t color = state->last_color;if (color >= info->num_colors) { return 0; }pos_t cur_pos = state->pos[color];int x, y;pos_get_coords(cur_pos, &x, &y);for (int dir=0; dir<4; ++dir) {pos_t neighbor_pos = offset_pos(info, x, y, dir);if (neighbor_pos != INVALID_POS &&!state->cells[neighbor_pos] &&game_is_deadend(info, state, neighbor_pos)) {return 1;}}return 0;}//
// Check the results of the connected-component analysis to make sure
// that every color can get solved and no freespace is isolatedint game_regions_stranded(const game_info_t* info,const game_state_t* state,size_t rcount,const uint8_t rmap[MAX_CELLS],size_t chokepoint_color,int max_stranded) {// For each region, we have bitflags to track whether current or// goal position is adjacent to the region. These get initted to 0.uint16_t cur_rflags[rcount];uint16_t goal_rflags[rcount];memset(cur_rflags, 0, sizeof(cur_rflags));memset(goal_rflags, 0, sizeof(goal_rflags));int num_stranded = 0;uint16_t colors_stranded = 0;int for_chokepoint = chokepoint_color < info->num_colors;// For each color, figure out which regions touch its current and// goal position, and make sure no color is "stranded"for (int color=0; color<info->num_colors; ++color) {uint16_t cflag = (1 << color);// No worries if completed:if ((state->completed & cflag) || color == chokepoint_color) {continue;}// Add color flag to all regions for cur_posgame_regions_add_color(info, state, rmap,state->pos[color],cflag, cur_rflags);// Add color flag to all regions for goal_posgame_regions_add_color(info, state, rmap,info->goal_pos[color],cflag, goal_rflags);if (!g_options.node_check_touch) {int delta = state->pos[color] - info->goal_pos[color];delta = delta < 0 ? -delta : delta;if (delta == 1 || delta == 16) { // adjacentcontinue;}}// Ensure this color is not "stranded" -- at least region must// touch each non-completed color for both current and goal.size_t r;// for each regionfor (r=0; r<rcount; ++r) {// see if this region touches the colorif ((cur_rflags[r] & cflag) &&(goal_rflags[r] & cflag)) {break;}}// There was no region that touched both current and goal,// unsolvable from here.if (r == rcount) {colors_stranded |= cflag;if (++num_stranded >= max_stranded) {return colors_stranded;}}}if (!for_chokepoint) {// For each region, make sure that there is at least one color whose// current and goal positions touch it; otherwise, the region is// stranded.for (size_t r=0; r<rcount; ++r) {if (!(cur_rflags[r] & goal_rflags[r])) {return -1;}}}// Everything a-ok.return 0;}//
// Print connected components of freespacevoid game_print_regions(const game_info_t* info,const game_state_t* state,uint8_t rmap[MAX_CELLS]) {printf("%s", BLOCK_CHAR);for (size_t x=0; x<info->size; ++x) {printf("%s", BLOCK_CHAR);}printf("%s\n", BLOCK_CHAR);for (size_t y=0; y<info->size; ++y) {printf("%s", BLOCK_CHAR);for (size_t x=0; x<info->size; ++x) {pos_t pos = pos_from_coords(x, y);pos_t rid = rmap[pos];const color_lookup_t* l = &color_dict[rid % MAX_COLORS];if (!state->cells[pos]) {assert(rid != INVALID_POS);char c = 65 + rid % 60;printf("%s", color_char(l->ansi_code, c, c));} else {assert(rid == INVALID_POS);printf(" ");}}printf("%s\n", BLOCK_CHAR);}printf("%s", BLOCK_CHAR);for (size_t x=0; x<info->size; ++x) {printf("%s", BLOCK_CHAR);}printf("%s\n", BLOCK_CHAR);printf("\n");}//
// Helper function for game_find_forced below.int game_is_forced(const game_info_t* info,const game_state_t* state,int color, pos_t pos) {int num_free = 0;int num_other_endpoints = 0;for (int dir=0; dir<4; ++dir) {pos_t neighbor_pos = pos_offset_pos(info, pos, dir);if (neighbor_pos == INVALID_POS ||neighbor_pos == state->pos[color]) {continue;} else if (state->cells[neighbor_pos] == 0) {++num_free;} else {for (size_t other_color=0; other_color<info->num_colors; ++other_color) {if (other_color == color) { continue; }if (state->completed & (1 << other_color)) { continue; }if (neighbor_pos == state->pos[other_color] ||neighbor_pos == info->goal_pos[other_color]) {++num_other_endpoints;}}} } // for each neighborreturn (num_free == 1 && num_other_endpoints == 0);}//
// Find a forced move. This could be optimized to not search all
// colors all the time, maybe?int game_find_forced(const game_info_t* info,const game_state_t* state,int* forced_color,int* forced_dir) {// if there is a freespace next to an endpoint and the freespace has// only one free neighbor, we must extend the endpoint into it.for (size_t i=0; i<info->num_colors; ++i) {size_t color = info->color_order[i];if (state->completed & (1 << color)) { continue; }int free_dir = -1;int num_free = 0;for (int dir=0; dir<4; ++dir) {pos_t neighbor_pos = pos_offset_pos(info, state->pos[color], dir);if (neighbor_pos == INVALID_POS) { continue; }if (state->cells[neighbor_pos] == 0) {free_dir = dir;++num_free;if (game_is_forced(info, state, color, neighbor_pos)) {*forced_color = color;*forced_dir = dir;return 1;}}} // for each neighbor      }return 0;}//
// Create simple linear allocator for search nodes.node_storage_t node_storage_create(size_t max_nodes) {node_storage_t storage;storage.start = malloc(max_nodes*sizeof(tree_node_t));if (!storage.start) {fprintf(stderr, "unable to allocate memory for node storage!\n");exit(1);}storage.capacity = max_nodes;storage.count = 0;return storage;}//
// Allocate the next tree node.tree_node_t* node_storage_alloc(node_storage_t* storage) {if (storage->count >= storage->capacity) {return NULL;}tree_node_t* rval = storage->start + storage->count;++storage->count;return rval;
}//
// De-allocate a tree node -- note that we can only safely de-allocate
// the last node that was allocated. Of course we only check this with
// an assert, tho.void node_storage_unalloc(node_storage_t* storage,const tree_node_t* n) {assert( storage->count && n == storage->start + storage->count - 1 );--storage->count;}//
// Free the memory allocated for this.void node_storage_destroy(node_storage_t* storage) {free(storage->start);
}//
// Compare total cost for nodes, used by heap functions below.int node_compare(const tree_node_t* a,const tree_node_t* b) {double af = a->cost_to_come + a->cost_to_go;double bf = b->cost_to_come + b->cost_to_go;if (af != bf) {return af < bf ? -1 : 1;} else {return a < b ? -1 : 1;}}//
// Create a binary heap to store the given # of nodesqueue_t heapq_create(size_t max_nodes) {queue_t rval;rval.heapq.start = malloc(sizeof(tree_node_t*) * max_nodes);if (!rval.heapq.start) {fprintf(stderr, "out of memory creating heapq!\n");exit(1);}rval.heapq.count = 0;rval.heapq.capacity = max_nodes;return rval;
}//
// Indexing macros for heaps#define HEAPQ_PARENT_INDEX(i) (((i)-1)/2)
#define HEAPQ_LCHILD_INDEX(i) ((2*(i))+1)//
// For debugging, not used presentlyint heapq_valid(const queue_t* q) {for (size_t i=1; i<q->heapq.count; ++i) {const tree_node_t* tc = q->heapq.start[i];const tree_node_t* tp = q->heapq.start[HEAPQ_PARENT_INDEX(i)];if (node_compare(tp, tc) > 0) {return 0;}}return 1;
}//
// Is heap queue empty?int heapq_empty(const queue_t* q) {return q->heapq.count == 0;
}//
// Peek at the next item to be removedconst tree_node_t* heapq_peek(const queue_t* q) {assert(!heapq_empty(q));return q->heapq.start[0];
}//
// Enqueue a node onto the heapvoid heapq_enqueue(queue_t* q, tree_node_t* node) {assert(q->heapq.count < q->heapq.capacity);size_t i = q->heapq.count++;size_t pi = HEAPQ_PARENT_INDEX(i);q->heapq.start[i] = node;while (i > 0 && node_compare(q->heapq.start[pi], q->heapq.start[i]) > 0) {tree_node_t* tmp = q->heapq.start[pi];q->heapq.start[pi] = q->heapq.start[i];q->heapq.start[i] = tmp;i = pi;pi = HEAPQ_PARENT_INDEX(i);}}//
// Helper function used for dequeueingvoid _heapq_repair(queue_t* q, size_t i) {size_t li = HEAPQ_LCHILD_INDEX(i);size_t ri = li + 1;size_t smallest = i;if (li < q->heapq.count &&node_compare(q->heapq.start[i], q->heapq.start[li]) > 0) {smallest = li;}if (ri < q->heapq.count &&node_compare(q->heapq.start[smallest], q->heapq.start[ri]) > 0) {smallest = ri;}if (smallest != i){tree_node_t* tmp = q->heapq.start[i];q->heapq.start[i] = q->heapq.start[smallest];q->heapq.start[smallest] = tmp;_heapq_repair(q, smallest);}    }//
// Pop a node off the heaptree_node_t* heapq_deque(queue_t* q) {assert(!heapq_empty(q));tree_node_t* rval = q->heapq.start[0];--q->heapq.count;if (q->heapq.count) {q->heapq.start[0] = q->heapq.start[q->heapq.count];_heapq_repair(q, 0);}return rval;}//
// Free memory allocated for heapvoid heapq_destroy(queue_t* q) {free(q->heapq.start);
}//
// FIFO via flat arrayqueue_t fifo_create(size_t max_nodes) {queue_t rval;rval.fifo.start = malloc(sizeof(tree_node_t*) * max_nodes);if (!rval.fifo.start) {fprintf(stderr, "out of memory creating fifo!\n");exit(1);}rval.fifo.count = 0;rval.fifo.capacity = max_nodes;rval.fifo.next = 0;return rval;
}//
// Check if emptyint fifo_empty(const queue_t* q) {return q->fifo.next == q->fifo.count;
}//
// Push node into FIFOvoid fifo_enqueue(queue_t* q, tree_node_t* n) {assert(q->fifo.count < q->fifo.capacity);q->fifo.start[q->fifo.count++] = n;
}//
// Peek at current FIFO nodeconst tree_node_t* fifo_peek(const queue_t* q) {assert(!fifo_empty(q));return q->fifo.start[q->fifo.next];
}//
// Dequeue node from FIFOtree_node_t* fifo_deque(queue_t* q) {assert(!fifo_empty(q));return q->fifo.start[q->fifo.next++];
}//
// De-allocate storage for FIFOvoid fifo_destroy(queue_t* q) {free(q->fifo.start);
}//
// Call this before calling the generic queue functions above.void queue_setup() {if (g_options.search_best_first) {queue_create = heapq_create;queue_enqueue = heapq_enqueue;queue_deque = heapq_deque;queue_destroy = heapq_destroy;queue_empty = heapq_empty;queue_peek = heapq_peek;} else {queue_create = fifo_create;queue_enqueue = fifo_enqueue;queue_deque = fifo_deque;queue_destroy = fifo_destroy;queue_empty = fifo_empty;queue_peek = fifo_peek;}}//
// Create a node from the linear allocator. This does not properly set
// the cost to come and cost to go, those need to be finished later by
// node_update_costs.tree_node_t* node_create(node_storage_t* storage,tree_node_t* parent,const game_info_t* info,const game_state_t* state) {tree_node_t* rval = node_storage_alloc(storage);if (!rval) { return 0; }rval->parent = parent;rval->cost_to_come = 0;rval->cost_to_go = 0;memcpy(&(rval->state), state, sizeof(game_state_t));return rval;}//
// Update the cost-to-come and cost-to-go for a node after a
// successful move has been made.void node_update_costs(const game_info_t* info,tree_node_t* n,size_t action_cost) {// update cost to comeif (n->parent) {n->cost_to_come = n->parent->cost_to_come + action_cost;} else {n->cost_to_come = 0;}n->cost_to_go = n->state.num_free;}//
// Animate the solution by printing out boards in reverse order,
// following parent pointers back from solution to root.void game_animate_solution(const game_info_t* info,const tree_node_t* node) {if (node->parent) {game_animate_solution(info, node->parent);}printf("%s", unprint_board(info));game_print(info, &node->state);fflush(stdout);delay_seconds(0.1);}//
// Return free if in bounds and unoccupiedint game_is_free(const game_info_t* info,const game_state_t* state,int x, int y) {return (coords_valid(info, x, y) &&state->cells[pos_from_coords(x, y)] == 0);}//
// This is a helper function used by game_check_bottleneck below.  If
// the given color moves n steps, it will split a region of
// freespace. Check to see how many colors would be unsolvable if this
// occurred. If the number is greater than n, we have a problem!int game_check_chokepoint(const game_info_t* info,const game_state_t* state,int color, int dir, int n) {// Make the proposed move.game_state_t state_copy = *state;for (int i=0; i<n; ++i) {/*game_print(info, &state_copy);printf("trying to move step %d/%d %s\n", i+1, n+1,color_cell_str(info, cell_create(TYPE_PATH, color, dir)));assert( state_copy.cells[pos_offset_pos(info, state_copy.pos[color], dir)] == 0 );*/game_make_move(info, &state_copy, color, dir, 1);}// Build new region mapuint8_t rmap[MAX_CELLS];size_t rcount = game_build_regions(info, &state_copy, rmap);// See if we are stranded int result = game_regions_stranded(info, &state_copy, rcount, rmap,color, n+1);if (result) {return result;}return 0;}//
// Identify bottlenecks -- narrow regions -- created by a recent move
// of a color, then see if it renders the puzzle unsolvable.int game_check_bottleneck(const game_info_t* info,const game_state_t* state) {size_t color = state->last_color;if (color >= info->num_colors) { return 0; }pos_t pos = state->pos[color];int x0, y0;pos_get_coords(pos, &x0, &y0);for (int dir=0; dir<4; ++dir) {int dx = DIR_DELTA[dir][0];int dy = DIR_DELTA[dir][1];int x1 = x0+dx;int y1 = y0+dy;if (game_is_free(info, state, x1, y1)) {for (int n=0; n<g_options.node_bottleneck_limit; ++n) {int x2 = x1+dx;int y2 = y1+dy;if (!game_is_free(info, state, x2, y2)) {int r = game_check_chokepoint(info, state, color, dir, n+1);if (r) { return r; }break;}x1 = x2;y1 = y2;}}}return 0;}//
// Perform diagnostics on the given nodevoid game_diagnostics(const game_info_t* info,const tree_node_t* node) {printf("\n###################################""###################################\n\n");printf("node has cost to come %'g and cost to go %'g\n",node->cost_to_come, node->cost_to_go);if (node->state.last_color < info->num_colors) {printf("last move was for color %s\n",color_name_str(info, node->state.last_color));} else {printf("no moves yet?\n");}game_state_t state_copy = node->state;int forced = 1;while (forced) {printf("game state:\n\n");game_print(info, &state_copy);printf("\n");uint8_t rmap[MAX_CELLS];size_t rcount = game_build_regions(info, &state_copy, rmap);if (game_check_deadends(info, &state_copy)) {printf("dead-ended -- state should be pruned!\n");printf("game regions:\n\n");game_print_regions(info, &state_copy, rmap);break;}if (game_regions_stranded(info, &state_copy, rcount, rmap, MAX_COLORS, 1)) {printf("stranded -- state should be pruned!\n");printf("game regions:\n\n");game_print_regions(info, &state_copy, rmap);break;}int r = game_check_bottleneck(info, &state_copy);if (r) {printf("chokepoint for ");for (size_t color=0; color<info->num_colors; ++color) {if (r & (1 << color)) {printf("%s", color_name_str(info, color));}}printf(" -- state should be pruned!\n");break;}int color, dir;forced = game_find_forced(info, &state_copy,&color, &dir);if (forced) {cell_t move = cell_create(TYPE_PATH, color, dir);printf("color %s is forced to move %s\n",color_name_str(info, color),color_cell_str(info, move));if (!game_can_move(info, &state_copy, color, dir)) {printf("...but it is not allowed -- state should be pruned!\n");break;}game_make_move(info, &state_copy, color, dir, 1);}}}tree_node_t* game_validate_ff(const game_info_t* info,tree_node_t* node,node_storage_t* storage) {assert(node == storage->start+storage->count-1);const game_state_t* node_state = &node->state;if (g_options.search_fast_forward &&g_options.order_forced_first) {int color, dir;if (game_find_forced(info, node_state,&color, &dir)) {if (!game_can_move(info, node_state, color, dir)) {goto unalloc_return_0;}tree_node_t* forced_child = node_create(storage, node, info,node_state);// if null, we ran out of memory and returning node is fine.if (forced_child) {game_make_move(info, &forced_child->state,color, dir, 1);node_update_costs(info, forced_child, 0);forced_child = game_validate_ff(info, forced_child, storage);if (!forced_child) {goto unalloc_return_0;} else {return forced_child;}}}}if (g_options.node_check_deadends &&game_check_deadends(info, node_state)) {goto unalloc_return_0;}if (g_options.node_check_stranded) {uint8_t rmap[MAX_CELLS];size_t rcount = game_build_regions(info, node_state, rmap);if (game_regions_stranded(info, node_state, rcount, rmap,MAX_COLORS, 1)) {goto unalloc_return_0;}}if (g_options.node_bottleneck_limit && game_check_bottleneck(info, node_state)) {goto unalloc_return_0;}return node;unalloc_return_0:assert(node == storage->start+storage->count-1);node_storage_unalloc(storage, node);return 0;}//
// Peforms A* or BFS searchint game_search(const game_info_t* info,const game_state_t* init_state,const uint8_t* hint,double* elapsed_out,size_t* nodes_out,game_state_t* final_state) {size_t max_nodes = g_options.search_max_nodes;if (!max_nodes) {max_nodes = floor( g_options.search_max_mb * MEGABYTE /sizeof(tree_node_t) );}node_storage_t storage = node_storage_create(max_nodes);tree_node_t* root = node_create(&storage, NULL, info, init_state);node_update_costs(info, root, 0);if (!g_options.display_quiet) {printf("will search up to %'zu nodes (%'.2f MB)\n",max_nodes, max_nodes*(double)sizeof(tree_node_t)/MEGABYTE);printf("heuristic at start is %'g\n\n",root->cost_to_go);game_print(info, init_state);}queue_t q = queue_create(max_nodes);int result = SEARCH_IN_PROGRESS;const tree_node_t* solution_node = NULL;double start = now();root = game_validate_ff(info, root, &storage);if (!root) {result = SEARCH_UNREACHABLE;} else {queue_enqueue(&q, root);}while (result == SEARCH_IN_PROGRESS) {if (queue_empty(&q)) {result = SEARCH_UNREACHABLE;break;}tree_node_t* n = queue_deque(&q);assert(n);game_state_t* parent_state = &n->state;int color = game_next_move_color(info, parent_state);int hint_dir = -1;if (hint) {pos_t pos = parent_state->pos[color];if (hint[pos] == color || hint[pos] >= info->num_colors) {for (int dir=0; dir<4; ++dir) {pos_t neighbor_pos = pos_offset_pos(info, pos, dir);if (neighbor_pos != INVALID_POS && parent_state->cells[neighbor_pos] == 0 &&hint[neighbor_pos] == color) {hint_dir = dir;break;}}}}for (int dir=0; dir<4; ++dir) {if (hint_dir >= 0 && dir != hint_dir) { continue; }int forced = 0;if (g_options.order_forced_first && !g_options.search_fast_forward) {forced = game_find_forced(info, &n->state, &color, &dir);}if (game_can_move(info, &n->state,color, dir)) {tree_node_t* child = node_create(&storage, n, info,parent_state);if (!child) {result = SEARCH_FULL;break;}size_t action_cost = game_make_move(info, &child->state,color, dir, forced);node_update_costs(info, child, action_cost);child = game_validate_ff(info, child, &storage);if (child) {const game_state_t* child_state = &child->state;if ( child_state->num_free == 0 && child_state->completed == (1 << info->num_colors) - 1 ) {result = SEARCH_SUCCESS;solution_node = child;break;}queue_enqueue(&q, child);}} // if can moveif (forced) { break; }} // for each dir} // while search activedouble elapsed = now() - start;if (elapsed_out) { *elapsed_out = elapsed; }if (nodes_out)   { *nodes_out = storage.count; }if (!g_options.display_quiet) {if (result == SEARCH_SUCCESS) {assert(solution_node);if (!g_options.display_animate) {printf("\n");game_print(info, &solution_node->state);} else {if (elapsed < 1.0) {delay_seconds(1.0 - elapsed);}game_animate_solution(info, solution_node);delay_seconds(1.0);}} double storage_mb = (storage.count * (double)sizeof(tree_node_t) / MEGABYTE);printf("\nsearch %s after %'.3f seconds and %'zu nodes (%'.2f MB)\n",SEARCH_RESULT_STRINGS[result],elapsed,storage.count, storage_mb);if (result == SEARCH_SUCCESS) {assert(solution_node);printf("final cost to come=%'g, cost to go=%'g\n",solution_node->cost_to_come,solution_node->cost_to_go);} else if (result == SEARCH_FULL && g_options.display_diagnostics) {printf("here's the lowest cost thing on the queue:\n");game_diagnostics(info, queue_peek(&q));printf("\nand here's the last node allocated:\n");game_diagnostics(info, storage.start+storage.count-1);}}if (final_state) {if (result == SEARCH_SUCCESS) {assert(solution_node);*final_state = solution_node->state;} else if (storage.count) {*final_state = storage.start[storage.count-1].state;} else {*final_state = *init_state;}}node_storage_destroy(&storage);queue_destroy(&q);return result;}//
// Command line usagevoid usage(FILE* fp, int exitcode) {fprintf(fp,"usage: flow_solver [ OPTIONS ] [ -H HINT1.txt ] ""[ -o ORDER1 ] BOARD1.txt\n""                   [ [ -H HINT2.txt ] [ -o ORDER2 ] ""BOARD2.txt [ ... ] ]\n\n""Display options:\n\n""  -q, --quiet             Reduce output\n""  -D, --diagnostics       Print diagnostics when search unsuccessful\n""  -A, --no-animation      Disable animating solution\n""  -F, --fast              Speed up animation 4x\n"
#ifndef _WIN32          "  -C, --color             Force use of ANSI color\n"
#endif"  -S, --svg               Output final state to SVG\n""\n""Node evaluation options:\n\n""  -t, --touch             Disable path self-touch test\n""  -s, --stranded          Disable stranded checking\n""  -d, --deadends          Disable dead-end checking\n""  -b, --bottlenecks N     Set bottleneck limit check (default %d)\n""  -e, --no-explore        Penalize exploring away from walls\n""\n""Color ordering options:\n\n""  -a, --no-autosort       Disable auto-sort of color order\n""  -r, --randomize         Shuffle order of colors before solving\n""  -f, --forced            Disable ordering forced moved first\n""  -c, --constrained       Disable order by most constrained\n""\n""Search options:\n\n""  -O, --no-outside-in     Disable outside-in searching\n""  -B, --breadth-first     Breadth-first search instead of best-first\n""  -n, --max-nodes N       Restrict storage to N nodes\n""  -m, --max-storage N     Restrict storage to N MB (default %'g)\n""  -Q, --queue-always      Disable \"fast-forward\" queue bypassing\n""\n""Options affecting the next input file:\n\n""  -o, --order ORDER       Set color order on command line\n""  -H, --hint HINTFILE     Provide hint for previous board.\n""\n""Help:\n\n""  -h, --help              See this help text\n\n",g_options.node_bottleneck_limit,g_options.search_max_mb);exit(exitcode);}//
// Check file existsint exists(const char* fn) {FILE* fp = fopen(fn, "r");if (fp) {fclose(fp);return 1;} else {return 0;}}//
//const char* get_argument(int argc, char** argv, int* i) {assert(*i < argc);if ((*i)+1 == argc) {fprintf(stderr, "%s needs argument\n", argv[*i]);usage(stderr, 1);}return argv[++(*i)];}//
// Parse command-line optionssize_t parse_options(int argc, char** argv,const char** input_files,const char** user_orders,const char** hint_files) {size_t num_inputs = 0;if (argc < 2) {fprintf(stderr, "not enough args!\n\n");usage(stderr, 1);}typedef struct flag_options_struct {int short_char;const char* long_string;int* dst_flag;int dst_value;} flag_options_t;flag_options_t options[] = {{ 'q', "quiet",         &g_options.display_quiet, 1 },{ 'D', "diagnostics",   &g_options.display_diagnostics, 1 },{ 'A', "animation",     &g_options.display_animate, 0 },
#ifndef _WIN32    { 'C', "color",         &g_options.display_color, 1 },
#endif{ 'F', "fast",          &g_options.display_fast, 1 },{ 'S', "svg",           &g_options.display_save_svg, 1 },{ 't', "touch",         &g_options.node_check_touch, 0 },{ 's', "stranded",      &g_options.node_check_stranded, 0 },{ 'd', "deadends",      &g_options.node_check_deadends, 0 },{ 'b', "bottlenecks",   0, 0 },{ 'e', "no-explore",    &g_options.node_penalize_exploration, 1 },{ 'a', "no-autosort",   &g_options.order_autosort_colors, 0 },{ 'o', "order",         0, 0 },{ 'r', "randomize",     &g_options.order_random, 1 },{ 'f', "forced",        &g_options.order_forced_first, 0 },{ 'c', "constrained",   &g_options.order_most_constrained, 0 },{ 'O', "no-outside-in", &g_options.search_outside_in, 0 },{ 'B', "breadth-first", &g_options.search_best_first, 0 },{ 'Q', "queue-always",  &g_options.search_fast_forward, 0 },{ 'n', "max-nodes",     0, 0 },{ 'm', "max-storage",   0, 0 },{ 'H', "hint",          0, 0 },{ 'h', "help",          0, 0 },{ 0, 0, 0, 0 }};for (int i=1; i<argc; ++i) {const char* opt = argv[i];int match_id = -1;for (int k=0; options[k].short_char; ++k) {if (options[k].short_char > 0) {char cur_short[3] = "-?";cur_short[1] = options[k].short_char;if (!strcmp(opt, cur_short)) {match_id = k;break;}}if (options[k].long_string) {char cur_long[1024];snprintf(cur_long, 1024, "--%s", options[k].long_string);if (!strcmp(opt, cur_long)) {match_id = k;break;}}}if (match_id >= 0) {int match_short_char = options[match_id].short_char;if (options[match_id].dst_flag) {*options[match_id].dst_flag = options[match_id].dst_value;} else if (match_short_char == 'b') {opt = get_argument(argc, argv, &i);char* endptr;g_options.node_bottleneck_limit = strtol(opt, &endptr, 10);if (!endptr || *endptr) {fprintf(stderr, "error parsing bottleneck limit %s ""on command line!\n\n", opt);exit(1);}} else if (match_short_char == 'n') {opt = get_argument(argc, argv, &i);char* endptr;g_options.search_max_nodes = strtol(opt, &endptr, 10);if (!endptr || *endptr) {fprintf(stderr, "error parsing max nodes %s ""on command line!\n\n", opt);exit(1);}} else if (match_short_char == 'm') {opt = get_argument(argc, argv, &i);char* endptr;g_options.search_max_mb = strtod(opt, &endptr);if (!endptr || *endptr || g_options.search_max_mb <= 0) {fprintf(stderr, "error parsing max storage %s ""on command line!\n\n", opt);exit(1);}} else if (match_short_char == 'H') {opt = get_argument(argc, argv, &i);if (!exists(opt)) {fprintf(stderr, "error opening %s\n", opt);exit(1);}hint_files[num_inputs] = opt;} else if (match_short_char == 'o') {user_orders[num_inputs] = get_argument(argc, argv, &i);} else if (match_short_char == 'h') {usage(stdout, 0);} else { // should not happenfprintf(stderr, "unrecognized option: %s\n\n", opt);usage(stderr, 1);}} else if (exists(opt)) {input_files[num_inputs++] = opt;} else {fprintf(stderr, "unrecognized option: %s\n\n", opt);usage(stderr, 1);}}if (!num_inputs) {fprintf(stderr, "no input files\n\n");exit(1);} else if (user_orders[num_inputs]) {fprintf(stderr, "order specified *after* last input file!\n\n");exit(1);} else if (hint_files[num_inputs]) {fprintf(stderr, "hint file specified *after* last input file!\n\n");exit(1);}return num_inputs;}//
// Main functionint main(int argc, char** argv) {setlocale(LC_NUMERIC, "");g_options.display_quiet = 0;g_options.display_diagnostics = 0;g_options.display_animate = 1;g_options.display_color = terminal_has_color();g_options.display_fast = 0;g_options.display_save_svg = 0;g_options.node_check_touch = 1;g_options.node_check_stranded = 1;g_options.node_check_deadends = 1;g_options.node_bottleneck_limit = 3;g_options.node_penalize_exploration = 0;g_options.order_autosort_colors = 1;g_options.order_most_constrained = 1;g_options.order_forced_first = 1;g_options.search_outside_in = 1;g_options.search_best_first = 1;g_options.search_max_nodes = 0;g_options.search_max_mb = 128;g_options.search_fast_forward = 1;const char* input_files[argc];const char* user_orders[argc];const char* hint_files[argc];memset(input_files, 0, sizeof(input_files));memset(user_orders, 0, sizeof(user_orders));memset(hint_files,  0, sizeof(hint_files));size_t num_inputs = parse_options(argc, argv,input_files,user_orders,hint_files);queue_setup();game_info_t  info;game_state_t state;pos_t hint[MAX_CELLS];int max_width = 11;for (size_t i=0; i<num_inputs; ++i) {int l = strlen(input_files[i]);if (l > max_width) { max_width = l; }}int boards = 0;double total_elapsed[3] = { 0, 0, 0 };size_t total_nodes[3]   = { 0, 0, 0 };int    total_count[3]   = { 0, 0, 0 };for (size_t i=0; i<num_inputs; ++i) {const char* input_file = input_files[i];const char* hint_file = hint_files[i];const char* user_order = user_orders[i];if (game_read(input_file, &info, &state)) {if (boards++ && !g_options.display_quiet) {printf("\n***********************************""***********************************\n\n");}if (hint_file) {if (!game_read_hint(&info, &state, hint_file, hint)) {hint_file = 0;} }if (!g_options.display_quiet) {printf("read %zux%zu board with %zu colors from %s\n",info.size, info.size, info.num_colors, input_file);if (hint_file) {printf("read hint file from %s\n", hint_file);}printf("\n");}game_order_colors(&info, &state, user_order);double elapsed;size_t nodes;game_state_t final_state;if (g_options.display_quiet) { printf("%*s ", max_width, input_file);fflush(stdout);}int result = game_search(&info, &state, hint_file ? hint : 0,&elapsed, &nodes, &final_state);assert( result >= 0 && result < 3 );total_elapsed[result] += elapsed;total_nodes[result] += nodes;total_count[result] += 1;if (g_options.display_quiet) {printf("%c %'12.3f %'12zu\n",SEARCH_RESULT_CHARS[result],elapsed, nodes);}if (g_options.display_save_svg) {char output_file[1024];size_t start = 0;size_t end = strlen(input_file);for (size_t i=0; input_file[i]; ++i) {if (input_file[i] == '/') { start = i+1; }if (input_file[i] == '.' && i > start) { end = i; }}size_t l = end-start;if (l > 1019) { l = 1019; }strncpy(output_file, input_file+start, l);for (int i=0; i<5; ++i) {output_file[l++] = ".svg"[i];}game_save_svg(output_file, &info, &final_state);if (!g_options.display_quiet) {printf("wrote %s\n", output_file);}}}}if (boards > 1) {double overall_elapsed = 0;size_t overall_nodes = 0;int types = 0;for (int i=0; i<3; ++i) {overall_elapsed += total_elapsed[i];overall_nodes += total_nodes[i];if (total_nodes[i]) { ++types; }}if (!g_options.display_quiet) {printf("\n***********************************""***********************************\n\n");for (int i=0; i<3; ++i) {if (total_count[i]) {printf("%'d %s searches took a total of %'.3f seconds and %'zu nodes\n",total_count[i], SEARCH_RESULT_STRINGS[i],total_elapsed[i], total_nodes[i]);}}if (types > 1) {printf("\n");printf("overall, %'d searches took a total of %'.3f seconds ""and %'zu nodes\n",boards, overall_elapsed, overall_nodes);}} else {printf("\n");for (int i=0; i<3; ++i) {if (total_count[i]) {printf("%*s%3d total %c %'12.3f %'12zu\n",max_width-9, "",total_count[i],SEARCH_RESULT_CHARS[i],total_elapsed[i],total_nodes[i]);}}if (types > 1) {printf("\n");printf("%*s%3d overall %'12.3f %'12zu\n",max_width-9, "",boards,overall_elapsed,overall_nodes);}}}  return 0;}

Flow Free solver[连线游戏求解器]相关推荐

  1. 【JY】No.7.1力学架构结构力学求解器(SM)使用教程

    软件讲解示例之理论(电算/手算)分析思路: 1.结构建模,并完成结构假定,如定义梁端弯矩释放(详见第二章). 2.线性屈曲分析(是否失稳破坏分析): 3.强度/挠度计算:(构件本身强度是否达标) 4. ...

  2. CST入门——求解器简介与时域、频域和积分求解器设置

    目录 1. 高频电磁仿真求解器简介 1.1. 时域求解器 Time Domain Solver(主) 1.2. 频域求解器 Frequency Domain Solver(主) 1.3. 本征模求解器 ...

  3. CST微波工作室学习笔记—9.求解器

    CST微波工作室_求解器详解 - 求解器的分类 Time Domain Solver--时域求解器 Frequency Domain Solver--频域求解器 Eigenmode Solver--本 ...

  4. CST仿真指导 | 问题类型与求解器的选择

    目录 前言 1. Home > Problem Type 2. 选择合适的求解器 关注"电磁学社",让电磁仿真不再复杂! 前言 用户根据仿真目的与求解问题的类型,应该选择合适 ...

  5. “芯”自主,更安全。国产三维云CAD:CrownCAD完全自主知识产权三维几何建模内核、约束求解器。

    CAD软件是产品创新的工具,是研发设计师不可或缺的必备软件,它解放设计师的双手和大脑,将设计师精力更多地集中于设计和创新上.在长期的使用过程中,设计师用户既是CAD技术的受益者,也是CAD技术的历史创 ...

  6. Simulink求解器综合介绍

    目录 1. 概要 2. Simulink求解器 2.1 Simulink仿真过程 2.2 Simulink求解器分类 2.3 Simulink仿真参数设置界面 3. 定步长求解器 fixed step ...

  7. matlab中solver函数_Simulink求解器(Solver)相关知识

    更多精彩内容参见专业MATLAB技术交流平台--MATLAB技术论坛http://www.matlabsky.com 1.变步长(Variable-Step)求解器 可以选择的变步长求解器有:ode4 ...

  8. 【工具笔记】Microsoft数学求解器Math Solver

    [工具笔记]Microsoft数学求解器Math Solver 工具笔记用于记录各种有用的工具,这里记录的是一个由Microsoft提供的数学求解器Math Solver. 可以用于求解代数,三角学, ...

  9. 游戏热血江湖 满线自动查询器制作游戏分析脱壳与查询器源码分享

    热血江湖满线自动查询器制作 N久没玩网游,某天心血来潮想去了热血江湖,如是用以前的账号,尝试着无聊一下,结果发现热血江湖现在竟然异常火爆,服务器满线,尝试着登录的几个大号都满线,不只知道啥时候能登录, ...

最新文章

  1. 新手也能立即上手,用Python90多行代码画出“樱花园”仙境(源码+注释)
  2. MnasNet:迈向移动端机器学习模型设计的自动化之路
  3. jstat 内存泄漏_一次Java内存泄漏的排查!要了自己的老命!
  4. 超级Wi-Fi未来潜力不容小觑 有望带动无线地区型网路发展
  5. 大数据WEB阶段Spring框架(四)Spring-MVC
  6. java做日历怎么对齐日期_如何使用Java日历从日期中减去X天?
  7. 腾讯实习笔试:关于几个有序数组求交集的问题
  8. mysql 5.5免安装配置_mysql的参考文档mysql5.5.21免安装版的配置方法
  9. 【UI/UX】Web应用GUI设计
  10. finalshell连接超时怎么解决_vncviewer连接超时,vncviewer连接超时怎么解决
  11. 【转】移动前端工作的那些事---前端制作篇之框架篇--jqMobi框架
  12. docker安装mysql【网易镜像方式】
  13. 计算机职业素养论文1500字,计算机职业论文
  14. 将Excel数据批量生成条形码
  15. oracle 列名sql,SQL查询表名、列名、列属性-Oracle
  16. 电容去耦原理笔记(彻底理解并伴有公式计算)
  17. 数据库视图概念,优缺点及作用
  18. java出现报错java.lang.IndexOutOfBoundsException
  19. 长江学者石照耀剖析精密减速机国产化之路—山坳上的机器人精密减速器
  20. 在世界读书日之后,重温与好书相遇的时光 | O'Reilly赠书活动

热门文章

  1. 小迪渗透应急响应(拾)
  2. Linux 命令行下的好东西
  3. 计算机开机后关机,计算机开机后自动关机的原因有哪些[解决方法]
  4. 如何选择漏电保护器规格型号_家用漏电开关型号和规格有哪些?如何选择?老电工说了这几点!...
  5. 海康 nvr获取历史视频流
  6. 新资料丨飞凌嵌入式A40i及全志T3系列开发板 对CAN的支持补充
  7. nRF51822蓝牙开发
  8. 期货开户的需要什么资料?
  9. 期货开户几条建议帮助你
  10. 关于Servlet服务器中的 **Caused by: java.lang.IllegalArgumentException: servlet映射中的[servletDemo]无效**