解决数独的多线程算法?

时间:2009-05-12 02:16:35

标签: java multithreading algorithm sudoku

我有一个家庭作业来编写一个多线程的数独求解器,它可以找到给定谜题的所有解决方案。我以前写过一个非常快速的单线程回溯数独求解器,所以我不需要任何帮助解决数独问题。

我的问题可能与不真正的并发性有关,但我不知道这个问题如何从多线程中受益。我不明白如何在不保留拼图的多个副本的情况下同时找到同一问题的不同解决方案。鉴于这种假设(请证明它是错误的),我没有看到多线程解决方案如何比单线程更有效。

如果有人能给我一些算法的开始建议(请没有代码......),我将不胜感激。


我忘了提一下,要使用的线程数被指定为程序的参数,所以据我所知,它与任何方式的拼图状态无关......

此外,可能没有一个独特的解决方案 - 有效的输入可能是一个完全空的板。我必须报告min(1000, number of solutions)并显示其中一个(如果存在)

14 个答案:

答案 0 :(得分:17)

非常简单。基本概念是,在您的回溯解决方案中,您可以在有选择时进行分支。你试过一个分支,回溯然后尝试了另一个选择。

现在,为每个选择产生一个线程并同时尝试它们。如果有<只生成一个新线程系统中已有一些线程(这将是你的输入参数),否则只需使用一个简单的(即你现有的)单线程解决方案。为了提高效率,请从线程池中获取这些工作线程。

在许多方面,这是一种分而治之的技术,您将选择作为将搜索空间分成两半并为每个线程分配一半的机会。很可能一半比另一半更难,因为线程生命周期会有所不同,但这就是使优化变得有趣的原因。

处理明显的同步问题的简单方法是复制当前的板状态并将其传递到函数的每个实例中,因此它是一个函数参数。这种复制意味着您不必担心任何共享并发。如果您的单线程解决方案使用全局或成员变量来存储板状态,则需要在堆栈(简单)或每个线程(更难)上复制此类。您需要返回的所有功能都是电路板状态以及为达到它而采取的一些措施。

调用多个线程来执行工作的每个例程应该在有n个工作时调用n-1个线程,执行第n个工作,然后等待同步对象直到所有其他线程完成。然后你评估他们的结果 - 你有n个板状态,返回移动次数最少的那个。

答案 1 :(得分:9)

答案 2 :(得分:5)

多线程背后的想法是利用多个CPU,允许您同时进行多次计算。当然每个线程都需要自己的内存,但这通常不是问题。

大多数情况下,您要做的是将可能的解决方案状态划分为多个尽可能独立的子空间(以避免在线程创建开销上浪费太多资源),并且“适合”您的算法(实际上从多线程中受益)。

答案 3 :(得分:4)

这是一个贪婪的强力单线程解算器:

  1. 选择下一个空单元格。如果没有更多的空单元,胜利!
  2. 可能的单元格值= 1
  3. 检查无效的部分解决方案(行,列或3x3块中的重复项)。
  4. 如果部分解决方案无效,请递增单元格值并返回步骤3.否则,请转到步骤1.
  5. 如果你看一下上面的大纲,步骤2和3的组合显然是多线程的候选者。更雄心勃勃的解决方案涉及创建递归探索,产生提交给线程池的任务。

    编辑回应这一点:“我不明白如何在不保留谜题的多个副本的情况下同时找到同一问题的不同解决方案。”

    你做不到。这就是重点。但是,具体的9线程示例可能会使收益更加清晰:

    1. 从示例问题开始。
    2. 找到第一个空单元格。
    3. 创建9个线程,其中每个线程都有自己的问题副本,其自己的索引作为空单元格中的候选值。
    4. 在每个线程中,在此线程本地修改的问题副本上运行原始单线程算法。
    5. 如果其中一个线程找到答案,请停止所有其他线程。
    6. 可以想象,每个线程现在运行的问题空间略小,每个线程都有可能在自己的CPU核心上运行。仅使用单线程算法,您无法获得多核机器的好处。

答案 4 :(得分:2)

当你说出给定拼图的所有解决方案时,你的意思是最后一个解决方案到拼图吗?或到达一个解决方案的不同方式?我的理解是,根据定义,数独谜题只能有一个解决方案......

对于前者,Pax's rule based approachTom Leys' take on multi-threading your existing backtracking algorithm可能是最佳选择。

如果是后者,你可以实现某种分支算法,在这个难题的每个阶段为每个可能的移动启动一个新线程(带有它自己的谜题副本)。

答案 5 :(得分:2)

是否需要从多线程中获益,或者只是使用多线程以便您可以学习分配?

如果使用强力算法,则很容易分成多个线程,并且如果分配专注于编码可能是可接受的解决方案的线程。

答案 6 :(得分:1)

根据您对单线程解算器的编码方式,您可以重新使用逻辑。您可以使用一组不同的策略来编写多线程解算器以启动每个线程来解决难题。

使用这些不同的策略,您的多线程解算器可以在比单线程求解器更短的时间内找到整套解决方案(请记住,虽然真正的Sudoku难题只有一个解决方案......你不是只有一个人必须在课堂上处理那个上帝糟糕的比赛)

答案 7 :(得分:1)

一些一般要点:我不会并行运行流程,除非1)很容易划分问题2)我知道这样做会带来好处 - 例如我不会遇到另一个瓶颈。我完全避免在线程之间共享可变值 - 或者最小化它。有些人足够聪明,可以安全地使用互斥锁。我不是。

您需要在算法中找到创建自然分支或大型工作单元的点。一旦确定了一个可以工作的单元,就可以将其放入队列中以获取一个线程。作为一个微不足道的例子。要升级的10个数据库。在所有10台服务器上启动升级异步。等待所有人完成。我可以轻松避免在线程/进程之间共享状态,并且可以轻松地聚​​合结果。

数独的想法是,一个有效的suduko解决方案应该结合2-3个(或更多)从未超过某个深度的策略。当我做Sudoku时,很明显,在任何给定时刻,不同的算法都会为解决方案提供最少的工作。你可以简单地解雇一些策略,让他们调查到有限的深度,等待报告。冲洗,重复。这避免了“暴力破解”解决方案。每个算法都有自己的数据空间,但您可以将答案组合在一起。

Sciam.com在这一年或两年后发表了这篇文章 - 但看起来它并不公开。

答案 8 :(得分:1)

你说你用回溯来解决问题。你可以做的是将搜索空间分成两个并将每个空间处理成一个线程,然后每个线程都会这样做,直到你到达最后一个节点。我做了一个解决方案,可以找到www2.cs.uregina.ca/~hmer200a,但使用单线程,但拆分搜索空间的机制是使用分支和绑定。

答案 9 :(得分:1)

TL; DR

是的,根据难题,基于回溯的数独求解器可以从并行化中受益匪浅!难题的搜索空间可以建模为树数据结构,并且回溯执行此树的深度优先搜索(DFS),这本质上是不可并行的。但是,通过将DFS与相反的树遍历形式宽度优先搜索(BFS)相结合,可以解锁并行性。这是因为BFS允许同时发现多个独立的子树,然后可以并行搜索这些子树。

因为BFS解锁了并行性,所以使用它保证使用全局线程安全队列,所有线程都可以在其中从中推入/弹出所发现的子树,并且与DFS相比,这会带来很大的性能开销。因此,并行化这样的求解器需要对执行的BFS量进行微调,以便执行足够的操作以确保树的遍历已充分并行化,但又不过分,以免导致线程通信的开销(推送/弹出子树)队列)超过了并行化提供的加速效果。

我不久前并行化了一个基于回溯的Sudoku求解器,并实现了4种不同的并行求解器变体以及一个顺序(单线程)求解器。并行变体都以不同的方式和程度将DFS和BFS组合在一起,最快的变体平均速度是单线程求解器的三倍(请参见底部的图表)。

此外,回答您的问题,在我的实现中,每个线程都会收到初始拼图的副本(一旦生成线程,便会收到副本),因此所需的内存比顺序求解器略高-并行化某些东西时并不少见。但这就是您所说的唯一“低效率”:如上所述,如果对BFS的数量进行了适当的微调,那么通过并行化可实现的加速将大大超过线程通信的并行开销以及更大的内存占用。另外,虽然我的求解器采用了独特的解决方案,但将它们扩展为处理非正确的难题并找到其所有解决方案将很简单,并且由于求解器设计的性质,不会显着降低加速速度。有关更多详细信息,请参见下面的完整答案。


完整答案

Sudoku求解器是否受益于多线程在很大程度上取决于其基础算法。诸如约束传播(即基于规则的方法)之类的常见方法(其中将难题建模为约束满足问题或随机搜索)并没有真正受益,因为使用这些方法的单线程求解器已经非常快。然而,回溯可以在大多数时间中受益(取决于难题)。

您可能已经知道,Sudoku的搜索空间可以建模为树数据结构:这三个的第一级代表第一个空单元格,第二级代表第二个空单元,依此类推。在每个级别上,节点表示该单元的祖先节点的值。 因此,可以通过同时搜索独立的子树来并行搜索此空间。单线程回溯求解器必须自己遍历整棵树,一个子树接一个子树,但必须并行遍历可以让每个线程并行搜索一个单独的子树。

有多种方法可以实现此目的,但是它们都是基于将深度优先搜索(DFS)与广度优先搜索(BFS)相结合的原理,这是树遍历的两种(相反)形式。单线程回溯求解器仅对整个搜索执行DFS,这本质上是不可并行的。但是,通过将BFS添加到混合中,可以解锁并行性。这是因为BFS逐层遍历树(与DFS逐分支相反),从而在移至下一个较低层之前找到给定层上的所有可能节点,而DFS则获取第一个可能的节点并在移至下一个可能的节点之前,将完全搜索其子树。结果,BFS使得可以立即发现多个独立的子树,然后可以通过单独的线程进行搜索; DFS从一开始就不了解其他独立子树,因为它正忙于以深度优先的方式搜索找到的第一个子树。

与多线程通常一样,对代码进行并行化比较棘手,如果您不确切知道自己在做什么,则初次尝试通常会降低性能。在这种情况下,重要的是要意识到BFS比DFS慢得多,因此,主要关注的是调整您执行的DFS和BFS的数量,以便仅执行足够的BFS来解锁发现多个文件的能力。 请注意,BFS本质上不比DFS慢,只是线程需要访问已发现的子树,因此他们可以搜索它们。因此,BFS需要全局线程安全的数据结构(例如队列),子树可以由单独的线程推入/弹出到该数据结构中,与之相比,DFS不需要线程之间的任何通信,因此需要大量开销。 因此,并行化这种求解器是一个微调过程,因此,我们希望执行足够的BFS,以便为所有线程提供足够的子树以进行搜索(即,在所有线程之间实现良好的负载平衡),同时最大程度地减少线程之间的通信(将子树推入/弹出队列)。

我不久前并行化了一个基于回溯的Sudoku求解器,并实现了4种不同的并行求解器变体,并以我也实现的顺序(单线程)求解器作为基准。它们都是用C ++实现的。最佳(最快)并行变量的算法如下:

  • 从拼图树的DFS开始,但只能进行到一定级别,或者搜索深度
  • 在搜索深度进行BFS,并将在该级别发现的所有子树推送到全局队列中
  • 然后,线程将这些子树弹出队列,并对其进行DFS,一直到最后一级(最后一个空单元格)。

下图(从我当时的报告中获取)说明了这些步骤:不同的颜色三角形代表不同的线程和它们遍历的子树。绿色节点表示该级别上允许的单元格值。注意在搜索深度执行的单个BFS;在此级别发现的子树(黄色,紫色和红色)被推到全局队列中,然后并行地独立遍历到树的最后一级(最后一个空单元格)。

enter image description here

如您所见,此实现仅执行一个级别的BFS(在搜索深度)。该搜索深度是可调整的,并且对其进行优化可以表示上述的微调过程。搜索深度越深,执行的BFS越多,因为树的宽度(给定级别上的#个节点)自然会随着距离的增加而增加。有趣的是,最佳搜索深度通常在相当浅的级别(即树中不是很深的级别);这表明进行少量的BFS已经足以生成足够的子树并在所有线程之间提供良好的负载平衡。

此外,由于有全局队列,因此可以选择任意数量的线程。尽管将线程数设置为等于硬件线程数(即逻辑内核数),通常是一个好主意;通常,选择更多选项不会进一步提高性能。此外,还可以通过执行树的第一级(第一空单元)的BFS来并行化在开始时执行的初始DFS:然后并行遍历在一级发现的子树,每个线程在给定搜索深度。这就是上图中所做的。但这并不是绝对必要的,因为如上所述,最佳搜索深度通常很浅,因此即使是单线程的DFS仍然非常快。

我对14个不同的Sudoku难题(特别是为回溯求解器设计的一组特殊设计的难题)彻底测试了所有求解器,下图显示了每个求解器解决 all所需的平均时间难题,适用于各种线程数(我的笔记本电脑有四个硬件线程)。未显示并行变体2,因为它实际上比顺序求解器获得了明显更差的性能。在并行变体1中,#线程是在运行时自动确定的,并且取决于难题(具体来说,第一级的分支因子);因此,蓝线代表其平均总求解时间,而与线程数无关。

enter image description here

所有并行求解器变体以不同的方式和程度将DFS和BFS组合在一起。当使用4个线程时,最快的并行求解器(变体4)的平均速度是单线程求解器的三倍!

答案 10 :(得分:0)

几年前,当我看到解决数独​​游戏时,似乎最佳解决方案使用了逻辑分析算法的组合,并且只在必要时才畏缩。这使得求解器能够非常快速地找到解决方案,并且如果您想使用它来生成新的谜题,还可以通过难度对其进行排名。如果你采用这种方法,你当然可以引入一些并发性,虽然让线程实际上一起工作可能会很棘手。

答案 11 :(得分:0)

我有一个想法,这里非常有趣..用演员模型做吧!我会说使用erlang .. 怎么样?你从原来的董事会开始,然后......

  • 1)在第一个空单元格中创建9个不同数量的孩子,然后自杀
  • 2)每个孩子检查它是否无效,如果是,则自杀,否则
    • 如果有空单元格,请转到1)
    • 如果完成,这个演员是一个解决方案

显然,每个幸存的演员都是解决问题的方法=)

答案 12 :(得分:0)

只是旁注。我实际上实现了一个优化的数独求解器并研究了多线程,但有两件事阻止了我。

首先,启动线程的简单开销需要0.5毫秒,而整个分辨率需要1到3毫秒(我使用Java,其他语言或环境可能会产生不同的结果)。

其次,大多数问题不需要任何回溯。那些做到的,只需要在解决问题的最后阶段,一旦所有游戏规则都用完了,我们就需要做出假设。

答案 13 :(得分:0)

这是我自己的便士。希望它有所帮助。

请记住,处理器间/线程间通信很昂贵。除非你拥有,否则不要多线程。如果在其他线程中没有太多的工作/计算要做,你也可以继续使用单线程。

尽可能尝试避免在线程之间共享数据。仅在必要时使用它们

尽可能利用 SIMD扩展程序。使用Vector Extensions,您可以一次性执行多个数据的计算。它可以帮到你很多。