解决非图表(Picross)

时间:2009-05-01 21:22:04

标签: algorithm puzzle

这是星期五下午,让我们有一个有趣的拼图/算法问题来解决。

我最喜欢的Nintendo DS游戏之一是Picross DS。游戏很简单,它涉及解决名为Nonograms的谜题。您可以在此处尝试一个简单的在线Picross克隆:TylerK's Picross

Nonograms是一个网格,为网格的每一行和每列定义了数字序列。这些数字定义了该行/列的“填充”方块的块,但未定义块两侧的未填充区域。例如,如果您有一行如下所示:


(来源:steam-punk.net

该行的可能解决方案包括:


(来源:steam-punk.net

(来源:steam-punk.net

“4 5”只是告诉你,在行的某个地方,有4个连续的块填充,然后填充了5个连续的块。这些将是填充的唯一块,以及/之前的空间量在它们之后没有定义。

当所有行和列符合其定义时,拼图就完成了,没有任何矛盾。

概念上非常简单的游戏,但手动解决其中一些可能需要相当长的时间。 Picross DS的谜题逐渐增加到25x20网格,这通常需要半个小时才能解决。

然而,我总是想到编写一个程序来解决它是一个非常简单的游戏。我从未接触过它,但也许这里的一些人会喜欢这个挑战。如果发布了相当数量的解决方案,我会将它们基于一个大型拼图相互比较,类似于Paolo Bergantino did here with his Boggle question。关于攻击谜题的方法,Nonogram Wikipedia page有很多关于谜题的信息,如果你想引用它的话。

这是TylerK的Picross中拼图#1的易于解析的定义,因此您可以为程序提供一些东西。第1行是拼图维度(可能是不必要的),第2行是行定义,第3行是列定义。这只是我想到的第一件事,所以如果有人能想到更好的输入格式,请随意评论或编辑这篇文章以包含它。

15 15
15,4 5,2 4,1 3,2,2,2 4 3,2 6 2,2 1 6 2,2 1 1 4 2,1 1,1 3 2 1,2 2 1 2 1,3 3 2 1,9
4 4,3 1 2 3,2 1 2 2,2 1 1,1 4 2,1 3,1 8,1 3 1 1,1 4 2 1,1 4,2 4 3,3 3 3,4 1,10 3,10

10 个答案:

答案 0 :(得分:21)

是的,这个问题是NP完全的,但这只意味着随着拼图大小的增加你(几乎)陷入指数增长的运行时间。但在现实生活中,拼图尺寸不会增长。几乎没有人愿意制造大于100x100的谜题,绝大多数都小于50x50。构建一个能够在几秒钟内解决95%的书籍和杂志中谜题的解算器实际上并不是特别具有挑战性。一个相当直接的深度优先搜索系统就可以做到。

不太明显的是,有一小部分谜题非常讨厌,并且会导致大多数简单求解器的运行时间爆炸。其中大多数是设计糟糕的谜题,人类难以或不可能解决,但有些谜题是人类解算者可以轻松解决的特别聪明的谜题,使用比大多数AI程序可以管理的更深入的见解。

我写了一个我自己的解算器,可以很快解决大多数难题,我已经做了survey of many other solvers基准测试结果比较他们的表现。我还讨论了一个有趣的难题类(多米诺拼图),它演示了一些对于一个聪明的人来说难以为大多数程序所用的难题。调查中将找到我的求解器和Domino Puzzle的链接。

我认为仍有很大的改进空间,并会鼓励有新想法的人去拍它。但值得注意的是,显而易见的事情已经完成,并且再次做这些事情的用处并不多。

答案 1 :(得分:6)

确定Nonogram解决方案是否存在/是唯一的是NP-hard。见http://en.wikipedia.org/wiki/Nonogram#cite_note-0

答案 2 :(得分:4)

如果您尝试从“最受限制”的行或列中获取信息,而不是尝试放置“第一”行,它将大大减少您的搜索,这可能有几个强制值。一个快速指示是将行/列中的所有值相加并添加#_of_values - 1,然后查找具有最大此类值的行/列(或此值与行数或列数之间的最小差异,如果行和列不同)。因此,如果您有一个25x25拼图,其中一行是“5 1 1 6 1 1 3”,那么该行的值为24,这意味着它非常受约束 - 除了一个空白方块之外的所有行的相对位置在该行中是已知的,并且最后一个空白方块可以处于8个不同的相对位置中的任何一个。因此,对于每组实心方块,只有两种可能性:该组左侧有额外的空白方块,或者该组右侧有额外的空白方块。因此,例如,该组中有6个已填充的正方形已知。

从一个方向获得一些强制值后,这会进一步限制与已知信息相交的另一个方向上的任何组。因此,随着更多信息的出现,最好的方法可能是在列和行之间来回切换,这几乎就是您需要手动解决的方法。

答案 3 :(得分:2)

对于使用回溯的深度优先搜索,这似乎是一个非常明显的情况,就像“n皇后”问题一样。可爱的部分是,除了放置行/列,你可以移动块。

好的,这是一个大纲。

  1. 从空白板开始,放置第一行

  2. 现在,使用该板,放置第二行,根据列约束检查它。如果它通过,递归地尝试针对该状态的下一行;如果它没有通过,那么尝试下一行可能的位置。

  3. 如果您在任何时候用尽了满足约束的行的可能位置,那么拼图就没有解决方案。否则,当你用完大麻时,你就解决了这个问题。

  4. 这些行可以被视为二进制数字很方便,因此可能性存在自然顺序。

答案 4 :(得分:2)

真正的问题是,是否有人能够提出一种比人类更快地解决上述谜题的算法?这对于相对简单的谜题(例如参考谜题)来说很容易,但如果谜题增长,这里的大多数算法都会很快变慢。这是我试图解决的难题。问题是第四行例如我相信有2220075种可能的组合。如果Charlie的算法暂时接受第3行的错误行,它将遍历第4行的所有这些组合。这就更不用说算法在第35行与第2行的错误相矛盾的情况。

我的算法类似于约翰的算法。它无法在我的64位桌面上以x86模式运行。当我将其切换到64位模式并让它在一夜之间运行时,我的电脑在早上完全无法使用。这个过程保留了8 Gig的内存(桌面上有8 Gigs的内存),并且由于疯狂的交换,计算机无法响应。它甚至没有解决所有可能的行。更不用说它甚至没有涉及可能的列组合。

List<List<int>> rows =
                new List<List<int>>()
                {
                    new List<int> { 8,29,4 },
                    new List<int> { 6,4,25,4,3 },
                    new List<int> { 5,3,2,3,9,4,2,1,3 },
                    new List<int> { 4,2,2,2,2,1,2,2 },
                    new List<int> { 4,1,1,9,10,2,2,1 },
                    new List<int> { 3,2,6,5,5,1,1 },
                    new List<int> { 3,1,5,5,1,1 },
                    new List<int> { 3,1,4,4,1,1 },
                    new List<int> { 3,1,4,4,1,1 },
                    new List<int> { 3,1,3,3,1,1 },
                    new List<int> { 3,1,3,6,2 },
                    new List<int> { 3,1,2,3,2,4,2 },
                    new List<int> { 4,3,1,8,7,1,2,3 },
                    new List<int> { 4,2,1,12,11,1,2,4 },
                    new List<int> { 5,1,2,7,2,2,6,1,1,4 },
                    new List<int> { 4,1,1,1,6,2,2,6,1,2,1,3 },
                    new List<int> { 4,1,1,2,4,3,4,3,1,1,1,1,3 },
                    new List<int> { 4,1,1,2,1,4,1,2,3,2,1,2,2 },
                    new List<int> { 3,1,1,1,2,5,6,1,1,1,3,2 },
                    new List<int> { 3,2,1,1,2,1,5,4,4,2,1,2,1,2 },
                    new List<int> { 3,2,2,1,1,4,2,2,3,1,1,2,1,1,2 },
                    new List<int> { 3,1,3,2,1,1,4,1,5,3,2,1,3,1,2 },
                    new List<int> { 3,1,2,1,2,1,3,7,4,1,4,2,2 },
                    new List<int> { 2,1,4,1,1,1,2,6,2,2,2,3,2,1 },
                    new List<int> { 2,2,4,1,2,1,2,5,2,1,1,3,2,1 },
                    new List<int> { 2,2,1,4,1,1,3,3,2,1,4,4,1 },
                    new List<int> { 2,3,3,2,1,3,3,7,4,1 },
                    new List<int> { 2,3,2,4,5,8,1,2,1 },
                    new List<int> { 1,1,3,11,6,7,1,3,1 },
                    new List<int> { 1,1,2,2,13,10,2,3,2 },
                    new List<int> { 1,2,3,1,6,1,1,7,1,5,2 },
                    new List<int> { 1,1,3,2,6,1,1,1,1,4,1,4,2 },
                    new List<int> { 1,1,6,7,2,4,2,5,6,1 },
                    new List<int> { 1,1,2,3,1,4,2,2,11,2,1 },
                    new List<int> { 1,1,1,1,2,1,5,10,1,1,1 },
                    new List<int> { 1,1,1,1,4,7,4,10,1,1,1 },
                    new List<int> { 1,2,1,1,28,1,1,3 },
                    new List<int> { 1,2,1,2,27,2,1,3 },
                    new List<int> { 1,1,1,1,26,1,1,1,1 },
                    new List<int> { 2,3,1,28,2,1,2,1 }
                };
            List<List<int>> cols =
                new List<List<int>>()
                {
                    new List<int> { 40 },
                    new List<int> { 28,1 },
                    new List<int> { 23,8 },
                    new List<int> { 5,6,7,4 },
                    new List<int> { 3,6,1,9,3,1 },
                    new List<int> { 2,3,2,5,4,2,2 },
                    new List<int> { 1,2,4,1,2,5,2 },
                    new List<int> { 1,1,4,9,2,3,2 },
                    new List<int> { 2,4,2,6,1,4,3 },
                    new List<int> { 1,4,1,3,4,1,6 },
                    new List<int> { 1,4,3,2,3,5,5 },
                    new List<int> { 2,4,1,2,3,4,1,3 },
                    new List<int> { 1,2,3,4,2,2,4,4,1 },
                    new List<int> { 1,1,2,3,2,1,4,2,4 },
                    new List<int> { 2,3,5,3,3,5,4 },
                    new List<int> { 3,1,6,1,2,5,5 },
                    new List<int> { 3,2,6,2,15 },
                    new List<int> { 3,1,8,2,13 },
                    new List<int> { 2,2,4,5,15 },
                    new List<int> { 2,2,2,2,22 },
                    new List<int> { 2,1,1,1,12,6 },
                    new List<int> { 2,1,10,4,5 },
                    new List<int> { 3,1,3,1,2,4 },
                    new List<int> { 3,1,1,4,3,1,4 },
                    new List<int> { 3,2,2,3,2,2,5 },
                    new List<int> { 3,1,1,5,1,1,5 },
                    new List<int> { 3,1,1,5,1,1,5 },
                    new List<int> { 3,1,1,5,1,1,5 },
                    new List<int> { 3,2,5,2,1,1,4 },
                    new List<int> { 3,1,1,3,2,2,4 },
                    new List<int> { 3,1,6,4,5 },
                    new List<int> { 2,2,12,2,6 },
                    new List<int> { 2,2,1,1,22 },
                    new List<int> { 2,1,2,2,5,15 },
                    new List<int> { 3,1,4,3,2,14 },
                    new List<int> { 3,1,7,2,1,13 },
                    new List<int> { 3,2,6,1,1,6,8 },
                    new List<int> { 3,2,5,2,2,4,7 },
                    new List<int> { 2,1,2,4,1,1,1,4,1,4,2 },
                    new List<int> { 1,1,4,4,3,1,4,5,1 },
                    new List<int> { 1,1,5,1,1,2,1,2,2,3,2 },
                    new List<int> { 1,5,2,2,1,5,5,3 },
                    new List<int> { 1,6,2,1,4,2,6,1 },
                    new List<int> { 1,6,2,6,5,2 },
                    new List<int> { 1,5,3,1,9,2 },
                    new List<int> { 2,2,4,2,6,3 },
                    new List<int> { 1,2,2,2,9,2,1 },
                    new List<int> { 3,5,5,8,4 },
                    new List<int> { 4,13,9 },
                    new List<int> { 27,2 }
                };

版权归信息技术的坦佩雷公会/ Kaisapais /芬兰啤酒厂所有。

答案 5 :(得分:1)

我现在没有足够的时间来充实解决方案,但这就是我要解决的问题。

“function1”确定行或列的可能组合,给定顶部或侧面数字的约束,以及已填充的正方形和已清空的正方形。

“function2”将function1的输出和逻辑上的所有结果合在一起 - 可以填写带有1的点。

“function3”将function1的输出与逻辑或所有结果放在一起 - 可以清空带有零的点。

继续将function2和function3应用于所有行和列,直到不再清空或填充框。如果拼图已经解决,那么就完成了。

如果拼图没有解决,那么请选择具有最少可能性的行或列并应用第一种可能性。然后将function2和function3应用于新板。如果它导致矛盾(行或列的0种可能性),则取消应用可能性并尝试不同的可能性。继续这样递归,直到找到解决方案。

答案 6 :(得分:1)

几个月前,我在C ++上为非图形编写了一个求解器。它只是查找阴影和空白单元格的所有允许位置。在每个解决方案步骤中,它会查看每个位置是否正常。 所以这是Chad Birch非定时的结果: http://i.stack.imgur.com/aW95s.png

我也为Mikko Rantanen的nongram尝试了我的求解器: http://i.stack.imgur.com/o1J6I.png

答案 7 :(得分:0)

Steven Simpson编写了一个非图解算器,可以在不同版本中免费获得,包括JavaScript脚本。他讨论了该网站上算法的细节(例如here - 基本上,他使用的是一系列线条,它们可以在速度与完整性之间进行权衡,并通过猜测所有线条到达死胡同的时间来进行分而治之。他还与其他方法有联系,这些方法比我们在这里有更多的基础。

答案 8 :(得分:0)

让我在经典的非图形拼图中指出2个有趣的曲折:

算法设计者面临的一个特殊挑战是,彩色非图形在一起考虑水平/垂直约束时会大大受益。通常的每行解算器在这里处于明显的劣势。

答案 9 :(得分:0)

这是我发现的:

所有NP完全问题都有相同的感觉。解决接下来80%的剩余案例总是很容易。例如,纳克分解成单行。可以编写例程solve_one_line(old_state, line_definition, new_state)来更新已知的单行,然后继续迭代行和列。然后它在几个案例中失败了,所以你写了一些东西来解决80%的案例。然后你添加随机数并解决你遇到过的每一个案例,但是有可能构建一个最差的案例。

遵循此模式的其他游戏是 MineSweeper Soduku

并行思考很难。例如,如果在一行或另一列上运行(如果在列上运行),则可能会发现上面的solve_one_line例程不会影响另一行。

现在你得到:

  all_until_completion(rows):
      solve_one_line(...)
  all_until_completion(cols):
      solve_one_line(...)

这使您可以运行20个核心(20x20)而无需数据锁定或其他任何操作。然后你考虑在每个单元处理器的图形卡上运行。然后你会注意到已经过了多长时间。

有一次我感觉自己老了看现代计算机科学教科书,其中 O(n)表示法被 O(n,p)符号替换,因为没人会关心评估单个处理器的算法。 8皇后解决方案是一种优秀的快速递归算法,具有快速失败,高效的内存使用,并且只能在一个处理器上运行。

问题是发挥的好借口。磨出更多相同的东西变得无聊。查看可能需要更多经验的技术列表:行为驱动的测试;依赖注入;纯函数式编程;神经网络;遗传算法;快速,草率和失控; GPGPU; OCR;示例驱动的学习;众包;选择一个并尝试以某种方式使用它来解决问题。有时,目标不是解决问题,而是漫游未知领域。

贡献一些东西。* 这可以像写一样简单。更好的可能是数百纳克的存储库,所以其他人可以玩。 [让我知道存在一个存储库,否则我会制作一个存储库]。一旦你找到了一些整洁的东西,就立即开始贡献。听取克林贡语的话:也许今天 是美好的一天;我说我们发货了。

因此,为NP问题编写奇异的并行解决方案并分享它们。祝你有美好的一天!

相关问题