目录
6.3 CSP的回溯搜索
6.3.1 变量和取值顺序
6.3.2 搜索与推理交错进行
6.3.3 智能回溯:向后看
6.4 CSP局部搜索
6.5 问题的结构
数独游戏被设计成通过约束间的推理来求解。但很多只用推理是不能求解的;这时我们必须通过搜索来求解。这里讨论部分赋值的回溯搜索算法;后面会讨论完整赋值的局部搜索算法。
可以应用标准的深度优先搜索。状态可能是部分赋值,行动是将加入到赋值中。考虑中有个值域大小为的变量,我们很快就能注意到一些可怕的情况:顶层的分支因子是,因为有个变量,每个变量的取值可以是个值中的任何一个。在下一层,分支因子是, 依此类推层。生成了一棵有个叶子的搜索树,尽管可能的完整赋值只有个!
看来合理但是弱智的问题形式化忽略了所有的一个共同的至关紧要的性质:可交换性。
如果行动的先后顺序对结果没有影响,那么问题就是可交换的。
是可交换的,因为给变量赋值的时候不需考虑赋值的顺序。因此,只需考虑搜索树一个结点的单个变量。
例如,在澳大利亚地图着色的搜索树的根结点,要在,和之
间选择,但永远不需要在和之间选择。就是说我们关注单个变量的可能取值就可以了。有了这个限制,叶结点的个数如我们所希望的减少到了个。
回溯搜索
术语回溯搜索用于深度优先搜索中,它每次为一变量选择一个赋值,当没有合法的值可以赋给某变量时就回溯。
回溯搜索的算法如下:
它不断选择未赋值变量,轮流尝试变量值域中的每一个值,试图找到一个解。一旦检测到不相容,失败,返回上一次调用尝试另一个值。
下图显示了澳大利亚问题的部分搜索树,其中按照的顺序来给变量赋值,因为表示是标准化的,不需要给提供领域专用的初始状态、行动函数、转移模型或目标测试。
要注意的是只维护状态的单个表示,修改该表示而不是生成一个新的表示。
回溯算法包括这样一行:
最简单的策略是按照列表顺序选择未赋值变量。这种静态的变量排序很少能使得搜索高效。例如图中,在赋值和之后,只剩下一个可能的赋值,因此下一个赋值要比给赋值有意义。事实上,赋完值之后,、和的选择都是强制性的。
最少剩余值(MRV)启发式
这种直观的想法——选择“合法"取值最少的变量——称为最少剩余值()启发式。也称为“最受约束变量”或“失败优先”启发式,之所以被称为后者是因为它选择了最可能很快导致失败的变量,从而对搜索树剪枝。
如果变量没有可选的合法取值,那么启发式将选择并马上检测到失败——避免其他无意义的搜索继续进行。相比随机的或静态的排序,启发式通常性能更好,取决于问题不同,它的性能有时要好到1000倍以上。
启发式在澳大利亚问题中对选择第一个着色区域没有帮助,因为初始的时候每个区域都有三种合法的颜色。在这种情况下,提出了度启发式。
度启发式
通过选择与其他未赋值变量约束最多的变量来试图降低未来的分支因子。
在图中,的度最高为5;其他变量的度为2或者3,除了的度为0:
实际上,一旦选择了的赋值,应用度启发式来求解可以不走错任何一步你可以在每个选择点上选择任何相容的颜色,仍然可以不回溯就找到解。最少剩余值启发式通常是一个强有力的导引,而度启发式对打破僵局非常有用。
一旦一个变量被选定,算法需要要决定检验它的取值顺序。
为此,有时最少约束值启发式很有效:
最少约束值启发式
最少约束值启发式优先选择的值是给邻居变量留下更多的选择。
应用场景:变量已经被选定,算法需要检验它的取值顺序,并且只寻求一个解
例如,我们已经赋值和,下一步要为选择值。这里蓝色是一个不好的选择,因为它消除了的邻居的最后一个可选合法值。 最少约束值启发式因此更愿意选择红色而不是蓝色。一般来说,启发式应该试图为剩余变量赋值留下最大的空间。当然,如果试图找到问题的所有解,而不只是第一个解, 那么这个排序毫无意义,因为无论如何要考查所有情况。当问题没有解的情况也是一样。
为什么变量选择是失败优先,而值的选择是失败最后呢?
现在的发现是,对于各种各样的问题,选择具有最少剩余值的变量通过早期的有效剪枝而有助于最小化搜索树中的结点数。而对于值的排序,窍门是只需找到一个解;因此首先选择最有可能的值是有意义的。如果需要枚举所有的解而不是只找一个解,值的排序毫无意义。
迄今为止,讨论了和其他算法的推理如何能在搜索前缩小变量的值域空间。但是搜索过程中的推理更加有力:每次我们决定给某变量某个值时,都有机会推理其邻接变量的值域空间。
前向检验
最简单的推理形式是前向检验。只要变量被赋值了,前向检验过程对它进行弧相容检查:对每个通过约束与相关的未赋值变量,从的值域中删去与不相容的那些值。由于前向检验只做弧相容推理,如果在预处理步骤做过前向检验的则不必考虑。
下图给出了在地图着色搜索中使用前向检验的回溯过程:
要注意的是这个例子中有两点很重要。首先,在赋值和之后,和的值域都缩小到了只有单个值:我们通过和的约束传播信息删除了这些变量上的一些分支。第二点需要注意的是,当赋值之后,的值域为空。因此,前向检验检测到部分赋值与问题约束不相容,算法立刻回溯。
如果联合使用启发式和前向检验,很多问题的搜索将更有效。 可以把前向检验看成是计算启发式需要的信息的有效途径。
考虑图中赋值之后。直觉上来说,它似乎约束的是它邻接的变量和,所以应当紧接着处理,然后才是其他变量。正是这么做的:和都有两个值,所以一个先选,接着是另一个,然后才依次是、和。最后,还有三个值,这三个值都有效。
前向检验存在的问题
尽管前向检验能够检测出很多不相容,它无法检测出所有不相容。问题是它使当前变量弧相容,但它并不向前看使其他变量弧相容。例如,考虑图中的第三行:
它指出当是、是后,和都只能是蓝色了。前向检验向前看得不够远,不足以观察出这里的不相容:和邻接所以不能取相同的值。
MAC:维护弧相容
算法(维护弧相容)能够检测这类不相容性。当变量赋值后,调用,并不是考虑中的所有弧,而是从与邻接的弧中所有未赋值变量开始。从这出发,进行正常的约束传播,一旦某变量的值域变为空,则调用失败并立即回溯。可以看出比前向检验更强有力,原因是前向检验在检查第一步与相同;与不同的是,前向检验在变量值域发生变化时并不递归传播约束。
算法当一个分支上的搜索失败时采取简单的处理原则:退回前一个变量并且尝试另外一个值。这称为时序回溯,因为重新访问的是时间最近的决策点。这里讨论更好的可能。
考虑图中的问题,按照固定的变量顺序、、、、、、,应用简单回溯算法求解。假设已经进行了部分赋值。在处理下一个变量时,发现任何值都违反约束条件。退回到,试着给赋一种新的颜色!显然这种做法是愚蠢的——对重新着色不能解决南澳大利亚州的问题。
一种更智能的回溯方法是退回到可能解决这个问题的变量
导致赋值失败的变量集合中的变量。要做到这一点,需要跟踪与的某些赋值冲突的一组赋值。这个集合(这里的情况是)称为的冲突集。回跳方法回溯到冲突集中时间最近的赋值;在这种情况下,回跳将跳过而尝试的新值。回跳方法可以通过简单地修改而实现,算法在检验合法值的时候保存冲突集。如果没有找到合法值,那么它按照失败标记返回冲突集中时间最近的元素。
前向检验算法可以不需要额外工作量就能提供冲突集:当基于赋值的前向检验删除变量的值域中的值时,应该把加入到的冲突集里。当从的值域中删除最后一个值的时候,把的冲突集中的每个赋值都要加到的冲突集中。这样,当到达的时候就知道需要回溯的时候应该回到哪个变量。
回跳发生在值域中的每个值都和当前的赋值有冲突的情况下:但是前向检验能检测到这个事件并且能阻止搜索到达这样的结点!事实上,可以证明回跳剪掉的每个分支在前向检验算法中也被剪枝。因此,简单的回跳在前向检验搜索中是多余的,或者说在诸如这样使用更强的相容性检验的搜索中是多余的。除了上一段中的观察结果,回跳的思想仍然是好的:基于失败的根源回溯。回跳发现失败是在一个变量的值域为空的时候,但是在很多情况下,一个分支在很早的时候就已经注定要失败了。再次考虑部分赋值(从前面的讨论中知道它是矛盾的)。假设尝试,然后给、、、赋值。知道对这最后四个变量没有可能的赋值,因此最终用完了的所有可能取值。现在的问题是回溯到哪儿?回跳是行不通的,因为确实有和前面的赋值相容的值——并没有导致失败的完全冲突集。然而我们知道,四个变量、、和放在一起会失败是因为前面的变量集,那些变量一定与这四个变量有直接冲突。这引出了关于诸如的冲突集的更深入概念:
是前面的变量集合致使连同任何后继变量一起没有相容解。
在这种情况下,变量集是和,所以算法会越过回溯到。使用这种方式定义的冲突集的回跳算法称为冲突指导的回跳。
下面解释这些新的冲突集是怎样计算的。实际上方法很简单。搜索分支的“终端"失败是当一个变量的值域变为空时;该变量有一个标准的冲突集。
在例子中, 失败了,它的冲突集是。
我们回跳到,将的冲突集(当然减去本身)吸收到自己的冲突集里,即;新的冲突集是。即,在给定了的赋值之后,从向前是无解的。
因此回溯到——集合中最近的一个。吸收— 到自己的直接冲突集里,得到(如上一段提到的)。
现在算法如期待的那样回跳到。
总结一下:令是当前变量,再令。为其冲突集。如果的每个可能取值都失败了,回跳到中最近的变量,并置
当到达一矛盾时,回跳能够告诉要退回多远,所以无需浪费时间来修改不能根本解决问题的变量。但我们希望不要再遇到同样的问题。每当搜索遇到了矛盾,我们知道这是冲突集中的某个最小子集引起的。约束学习的思想就是从冲突集中找出导致问题的最小变量集合。这组变量及它的对应值,被称为无用。可以通过添加中的新约束记录下这些无用,也可以独立地用缓存来保留这些无用。
例如,考虑上图最下面一行的状态。前向检验告诉我们这个状态是个无用,因为 没有合法赋值。在这种特殊情况下,记录这个无用没有任何帮助,因为一旦从搜索树中剪掉这个分支,就再也不可能遇到这样的组合。假设图中的搜索树是个大搜索树的一部分,而这个大搜索树的赋值是从和开始的。这时就值得记录无用,因为将来会遇到和赋值的同样的问题。
前向检验和回跳都可以有效地使用无用。约束学习则是现代求解器有效求解复杂问题的最重要技术之一。
局部搜索算法对求解许多CSP都是很有效的。它们使用完整状态的形式化:初始状态是给每个变量都赋一个值,搜索过程是一次改变一个变量的取值。例如,在八皇后问题中,初始状态是8个皇后在8列上的一个随机布局,然后每步都是选择一个皇后并把它移动到该列的新位置上。
典型地,初始布局会违反一些约束。局部搜索的目的就是要消除这些矛盾。
在为变量选择新值的时候,最明显的启发式是选择与其他变量冲突最少的值——最少冲突启发式。下图给出了该算法,然后将算法应用于八皇后问题:
每一步选择一个皇后,在它所在列重新分配位置。方格中的数字表示冲突的个数(在这个问题中是能攻击到的皇后的个数)。算法将皇后移到最小冲突的方格里,随机地打乱平衡。
令人惊讶的是最小冲突对许多都有效。神奇的是在后问题中,如果不依赖于皇后的初始放置情况,最少冲突算法的运行时间大体上独立于问题的规模。它甚至能在平均(初始赋值之后)50步之内求解百万皇后问题。大致来说,对局部搜索求解后问题十分容易,因为解密集地分布于整个状态空间。最少冲突算法也适用于难题求解。例如,它用于安排哈勃太空望远镜的观察日程时间表,安排一周的观察日程所花费的时间从三周减少到了大概10分钟。
使用最少冲突启发式的地形通常有一系列高原。可能有上百万个变量赋值却只有一个有冲突。高原搜索——通话走小路移动到另一个分数相同的状态——可以帮助局部搜索离开高原。这种高原上的流浪可以由禁忌搜索导引:
将最近访问过的状态记录在表中,并禁止算法再回到那些状态。模拟退火也可以用于逃离高原。
另一项技术称为约束加权,可以帮助搜索把精力集中在重要约束上。每个约束都有一个数字权重W;初始都为1。搜索的每一步,算法都选择使得违反约束权重和最小的变量/值对来修改。接着增加当前赋值违反的约束的权重值。这有两个好处:给高原增加了地形,确保能够从当前状态改善,并且它随着时间的进行不断给难于解决的约束增加权重。
局部搜索的另一个优势是当问题改变时它可以用于联机设置。这在调度问题中特别重要。一周的航班日程表可能涉及上千次航班和上万次的赋值,但是一个恶劣天气可能就会打乱原来的机场日程安排。我们希望以在最小的改动来修正日程。从当前日程开始使用局部搜索算法,这项工作很容易地完成。使用新约束集的回溯搜索通常需要更多的时间,找到的解也有可能要对当前日程进行很多改动。
本节讨论如何以结构化方式表示问题,如约束图所表示的,能帮助快速地找到解。这里讨论的多数方法也同样适用于以外的其他问题,例如概率推理。总之,在处理实际世界问题时我们通常希望将它分解为很多子问题。回头再来看看澳大利亚问题中的约束图,可以看到一个事实:和大陆不相连。直观上来说,显然对着色和对大陆着色是两个独立的子问题——任何对大陆区域着色的解和任何对着色的解合并起来都得到整个问题的一个解。独立性可以简单地通过在约束图中寻找连通子图来确定。每个连通子图对应于一个子问题 。如果赋值是的一个解,那么是的一个解。为什么这样很重要?考虑以下问题:
假设每个包含所有个变量中的个变量,这里是一个常数。那么就会有个子问题,解决每个子问题最多花费步工作,这里为值域大小。因此总的工作量是,是的线性函数;如果不进行分解,总的工作量是,是的指数函数。
看一个更具体的例子:将的布尔分解成4个的子问题,会使最坏情况下的时间复杂度从宇宙寿命那么长减少到不到一秒。
完全独立的子问题是很诱人的,但是很少见。幸运的是,有些图结构也很容易求解。例如当约束图形成一棵树时,指的是任何两个变量间最多通过一条路径连通。我们将证明任何一个树状结构的可以在变量个数的线性时间内求解。这里的关键是称为直接弧相容或的新概念。假设的变量顺序为,该是直接弧相容的当且仅当对所有每个与都是弧相容的。
求解树结构时,首先任意选择一个变量为树的根,选择变量顺序,这样每个变量在树中出现在父结点之后。这样的排序称为拓扑排序。下图为约束图:
上图为一种可能的排序。个结点的树有条弧,所以在步内可以将此图改造成直接弧相容,每一步需要比较两个变量的个可能取值,所以总时间是。一旦有了直接弧相容的图,就可以沿着变量列表并选择任意剩余值。
由于父结点与其子结点的弧是相容的,我们知道无论父结点选择什么值,子结点都有值可选。这意味着无须回溯;可以沿着变量线性前进。
完整算法参见下图:
现在已经有了求解树结构的高效算法,下面讨论更一般的约束图是否能化简成树的形式。可以有两种基本方法:一种是基于删除结点的,一种是基于合并结点的。
约束图删除结点化简成树
第一种方法是先对部分变量赋值,使剩下的变量能够形成一棵树。考虑澳大利亚问题这个图就会变的约束图,如图(a)所示。如果能删除南澳大利亚州,这个图就会变成成像(b)中的一棵树。幸运的是,可以这样做(只是在图中删除,而不是真的从大陆上删除),给变量一个固定的值并且从其他变量的值域中删除任何与的取值不相容的值。
现在,在删除了和它的约束之后,的每个解都将与的值相容。(这对二元是可行的;在高阶约束问题中情况会更复杂。)因此,用上面给出的算法求解剩余的树,并由此得到整个问题的解。当然,在一般情况下(与地图着色不同),为选择的值可能是错误的,因此将需要尝试所有的可能值。一般算法如下:
(1)从的变量中选择子集,使得约束图在删除之后成为一棵树。称为环割集(cycle cutset)。
(2)对于满足所有约束的中变量的每个可能赋值:
如果环割集的大小为,那么总的运行时间为:我们需要尝试中变量的赋值组合共种,对其中的每个组合需要求解规模为的树问题。如果约束图“近似于一棵树”,那么将会很小,直接回溯将节省巨大开销。然而在最坏情况下,可能大到。虽然找出最小的环割集是难题,但是已经有一些高效的近似算法。算法的总体方法叫做割集调整。
约束图合并结点化简成树
第二种方法以约束图的树分解为基础,它把约束图分解为相关联的子问题。每个子问题独立求解,再把得到的结果合并起来。像大多数分治算法一样,如果没有一个子问题特别大,它的效果就会很好。下图给出了地图着色问题的树分解,形成5个子问题。树分解必须满足以下3个条件:
前两个条件保证了所有的变量和约束都在分解中都有表示。第三个条件看起来更有技术,但是它只是反映了任何给定的变量在每个子问题中必须取值相同的约束;子问题之间的连接强化了这个约束。例如,在图中出现在连接起来的所有四个子问题中。
下面来独立地求解每个子问题;如果其中任何一个无解,那么整个问题无解。如果能求解所有的子问题,接下来构造如下一个完整解。首先,把每个子问题视为一个“巨型变量”,它的值域是这个子问题的所有解的集合。例如,上图中最左边的子问题是有三个变量的地图着色问题,因此有六个解——其中一个是。然后,可以用前面给出的树算法来求解连接这些子问题的约束。子问题之间的约束要求它们的共享变量要取相同的值。例如,第一个子问题的解,下一个子问题的相容解只能是。
一个给定的约束图允许有多种树分解;进行分解的时候,原则是分解出来的子问题越小越好。图的树分解的树宽是最大的子问题的大小减1;图本身的树宽定义为它所有树分解的最小树宽。如果一个图的树宽为,并且给定对应的树分解,那么此问题的时间复杂度是。因此,如果的约束图树宽有界,则该在多项式时间内可解。不幸的是,找到最小树宽的树分解是一个难题,不过实际中有一些可行的启发式方法。
到目前为止,讨论了约束图的结构。变量的取值的结构也同样重要。考虑色地图着色问题。对每个相容的解,通过重新排列颜色名字实际上有个解。例如,澳大利亚地图中我们知道、和必须着不同的颜色,但是有种方法把颜色值赋给这三个变量。这被称为值对称。我们希望通过打破这种对称来使搜索空间减小倍。这可以通过打破对称约束做到。例如,可以给变量一个特定的序,,要求变量的取值要满足字典序。这个约束确保了个选择中只有一个是解。
对于地图着色问题,很容易可以找到约束来消除对称性,一般来说在多项式时间内找到约束来消除对称性并保留一个对称的解是可能的,但消除搜索过程中所有中间值集合的对称性是难题。实际上,对很多问题来说,打破值对称都被证明是重要并且有效的。