该算法来源于我最近所读的一本书。《Artificail Intelligence, A Moden Approach 3rd》中文译名是《人工智能,一种现代的方法》。是人工智能方向非常经典的一本教科书,一千多页的厚度也是令人望而生畏的。(当然,和高德纳的《计算机程序设计艺术》比起来,仍然是小巫见大巫了。)该书的第六章介绍了CSP问题以及一些经典的解决该问题的方案。心血来潮,再加上最近刚刚学习了python,想找个机会试下手,自己用python实现了其中的一部分。
所谓CSP问题(constraint satisfaction problem),是指多个变量(variable)各有自己的取值范围(domain),同时它们之间存在一些值上相互制约的条件(condition),要求给出一组能够自洽的解,或者证明无解的问题。涂色问题就是一种经典的CSP,我们为地图上的不同区域涂色,保证相邻的区域不会出现相同的颜色。在这里,每个区域就是一个变量,取值范围指我们能够选择的颜色种类,条件是和相邻的区域的变量值(颜色)不相等。
一个CSP就是由这三部分组成:变量variable,范围domain,限制条件condition。
我们用中国地图的一部分来描述并且表达一个CSP问题。
为了简单。我们只选取地图上湖南同色的部分省市做讨论。我们可以看到,如果按照上述涂色的要求,假设我们拥有4种颜色(红绿蓝白)用于填涂。(4种颜色足够用来填涂任何平面上的涂色问题,这个经典的四色问题直到近代才被人用穷举配合计算机方法作出了证明。)这里河南湖北相邻,故不能用相同的颜色,以此类推,湖北不能和任何其它三个省之间用相同的颜色。
我们按照以上提到的三个部分来描述这个问题,一种简单的方式如下:
variable:河南,湖北,湖南,江西
domain:河南:[红绿蓝白],湖北:[红绿蓝白],湖南:[红绿蓝白],江西:[红绿蓝白]
condition:不同色(河南,湖北),不同色(湖北,湖南),不同色(湖北,江西),不同色(湖南,江西)
以上三个部分完整的描述了我们面临的涂色CSP。我们的目标是从各个省市的涂色domain中选一个颜色,找到一种各个省市的涂色组合,使得所有的condition都得到满足。
人工的确很容易实现上述的问题,但是当问题规模变得极大的时候,计算机的算法变得尤为重要。下面是CSP的一种经典算法, backTracking search.
所谓backTracking search, 相当于对树的深度搜索技术。一次对一个变量赋一个值,然后检查是否出现与条件不一致的情况,如果没有问题,继续给下一个变量赋值,否则,重新给这个变量换一个值。其定义是很简单的,伪代码大致如下:
function BACKTRACK(当前赋值方案,CSP问题描述) returns 一种解, or 无解
if 当前赋值方案已经完成
return 当前赋值方案
var = 选择一个未赋值的variable
for each value in var的domain的一个序列 do
添加var=value到当前赋值方案中
更新domain //在有变量赋值的情况下,剩余变量可以选择的颜色范围会变小
if 检查依然保持条件一致
result = BACKTRACK(当前赋值方案,CSP问题描述)
if result != 无解
return result
else //该var=value导致无解
从当前赋值方案中去掉var=value,并且取消与其带来的domain的更新
//var遍历均无解
return failure
该算法维持了一个当前的赋值方案,利用递归的方法不断尝试变量的每一个取值,失败则回退,直到成功或者该变量的所有值都没有找到解为止。
为了方便理解,我们来用该算法进行一次人工模拟,为了模拟一个不一致,我们只用三种颜色,去掉白:
从河南起,为河南赋值红,当前赋值方案为{河南:红}
根据condition,当前的domain为 河南[红],湖北[蓝绿],湖南[蓝绿红],江西[蓝绿红]
下一个湖南,为湖南赋值蓝,当前赋值方案为{河南:红,湖北:蓝}
domain更新为:河南[红],湖北[绿],湖南[蓝],江西[红]
为湖北赋值绿 未出现不一致,再为江西赋值红
最终的赋值方案为{河南:红,湖北:蓝,江西:红,湖南:蓝}
在这个过程中,没有一个回退的作用,这是因为相邻不同色的条件太直观太弱了,在赋值之后可以在domain中直接去掉不符合要求的值,如果导致某个变量的domain为空,则直接返回条件不一致。如果条件更加隐晦,比如说数独(关于数独的具体介绍略),条件检查更新domain就容易放过一些看上去可行其实无解的变量取值。这个时候回退就起到它的作用了。
对于数据结构的选择,为了方便随机生成,CSP我们使用大学课本上常常提到的邻接矩阵,但是因为相邻是对称的,我们只用一个下三角矩阵就可以达到描述问题的要求,为变量标号之后和该矩阵的标号对应上。对于domain,我们可以使用一个list的list:[['R','B','G'],[...],[...],[...]]。这里的顺序和邻接矩阵中的序号保持相同意义。
上述问题的邻接矩阵大致如下:(对角线上算法不用,不予考虑,1/0皆可,此处用X表示)
为变量标号:0河南,1湖北,2湖南,3江西
下三角矩阵:[[x],[1,x],[0,1,x],[0,1,1,x]]
以下是一个python代码的实现:
def backTrack(assignment,csp,domain,method):
#assignment的定义:dict index=color
#backTrack:通过递归,对assignment做尝试赋值并AC-3检查,保存副本(浪费空间,但是作为练习够用),失败则回复副本.
#domain:当前domain assignment:当前赋值位置 csp:问题描述(static)
#method:选择下一个变量采用的方法
if len(assignment)==len(csp):
return assignment
backupdomain = list.copy(domain)
index = nextVariableIndex(csp,domain,assignment,method)
for val in valueOrder(index,assignment,csp,domain):
assignment[index]=val
domain[index]=[val]
if AC3(csp,domain):
result = backTrack(assignment,csp,domain,method)
if result:
return result
#运行到这里说明此路不通,回复状态,删掉val=value from domain
domain=backupdomain
domain[index].remove(val)
del assignment[index]
#运行到这里说明全都不行
return False
以上backTrack函数的几个部分,需要另外实现,并且有多种实现方法。下面一一介绍。
1.nextVariableIndex, 该函数负责从剩下的未赋值的变量中选择一个来赋值。有多种不同的技巧,最简单的是使用我们邻接矩阵和domain上的自然序。另外有一种称为MRV的技巧(minimum-remaining-values),该技巧选择那个domain中可选值最少的变量作为下一个变量,这种方法在特定的密集条件下可以快速的检查出failure,减少搜索时间。
python对该两种技巧的实现,返回的是变量的序号:
def nextVariableIndex(csp,domain,assignment,method):
#这里实现两种方式,MRV与自然序
if method=='natural':
for x in range(len(domain)):
if assignment.get(x)==None:
return x
if method=='MRV':
#MRV:find the variable that has the smallest domain
size=float('inf')
for index,unit in zip(range(len(domain)),domain):
if len(unit)
2.valueOrder,这里决定对该变量的可选值的使用顺序,我们直接用自然序:
def valueOrder(index,assignment,csp,domain):
#自然序
return domain[index][0]
3.AC-3,AC-3是一种经典的对两两限制条件的一种更新domain和判断是否一致的方法。其核心思想是:对每一对互相限制的变量(A,B)加入队列查看,使得A的domain得到更新,如果A的domain进行了更新,再次将与A有关系的每一对再次加入队列进行查看。直到队列为空为止,此时所有的两两关系都进行了查看并且没有再次更新。
因为对称的限制关系,下三角的矩阵和不同色的条件特点。AC3函数做了很多简化。下面这个函数比较复杂。其核心作用是根据csp和当前domain,更新domain或者返回不一致False.
def AC3(csp,domain):
#revise for 2-consistent
#input: csp-the csp matrix domain-the current domain list
#output:the revised domain list or failure if there is no solution
queue=[]
#init the queue
queue=[[rowindex,columnindex] for rowindex,row in zip(range(15),csp) for columnindex,column in zip(range(15),row) if column==1 and rowindex!=columnindex]
#loop
while(len(queue)!=0):
constraint = queue.pop()
#一次梳理一对
for c in range(2):
if c==1:
constraint=[constraint[1],constraint[0]]
if revise(domain,constraint):
if len(domain[constraint[0]])==0:
return False
#因为是三角形矩阵,所以在找i的邻接的时候要判断一下i
#添加邻接
#不要相等的情况
for everyone in range(15):
if everyoneconstraint[0]:
if csp[everyone][constraint[0]]==1:
queue.append([everyone,constraint[0]])
return True
def revise(domain,constraint):
#for every item in x there should be a satisfied one in y
#specified for this, there should be at least 2 items in y or delete all x that is the same from y[0]
x=domain[constraint[0]]
y=domain[constraint[1]]
flag=False
if len(y)>=2:
return False
#y cannot be 0.
for o in x:
if o==y[0]:
x.remove(o)
flag=True
return flag
以上代码共同实现了backTracking的CSP解决方案。有兴趣的同学可以尝试用来解决上述的涂色问题。