澳大利亚地图着色问题
图源 : 《人工智能——一种现代的方法》
实验内容
- 使用回溯搜索算法解决地图着色问题。
- 实现最大受限变量、最大约束变量、最少约束值原则和前向检查原则,并将其融入回溯搜索算法中以解决地图着色问题。
- 在2的基础上,实现弧相容算法AC-3,并将其加入回溯搜索算法中以解决地图着色问题。
过程分析及设计思想
问题的抽象
地图的形式化
Australia_Map = {
'WA': ['NT', 'SA'],
'NT': ['WA', 'SA', 'Q'],
'Q': ['NT', 'SA', 'NSW'],
'SA': ['WA', 'NT', 'Q', 'NSW', 'V'],
'NSW': ['Q', 'SA', 'V'],
'V': ['SA', 'NSW'],
'T': []
}
使用字典容器存储地图各城市的邻接关系。
着色情况与定义域的形式化
Coloring_State = {}
color = ['red', 'green', 'blue']
Domain = {}
for city in Australia_Map.keys():
Coloring_State[city] = 0
Domain[city] = copy.deepcopy(color)
回溯算法实现
回溯算法伪码
图源 : 《人工智能——一种现代的方法》
尝试翻译伪码
def recursive_backtracking(map, coloringState, domain, index):
if index >= len(list(map)):
return coloringState
city = list(map)[index]
if ColorComplete(map, coloringState):
return coloringState
for color in domain[city]:
coloringState[city] = color
if constraint(map, coloringState):
updateDomain(map, coloringState, domain)
coloringState = recursive_backtracking(map, coloringState, domain, index + 1)
elif not constraint(map, coloringState):
coloringState[city] = 0
return coloringState
def backtracking(map, coloringState, domain):
print("1st:")
t0 = time.perf_counter()
coloringState = recursive_backtracking(map, coloringState, domain, 0)
t1 = time.perf_counter()
print((t1 - t0) * 1000, 'ms')
print(coloringState)
运行结果展示
1st:
0.10940000356640667 ms
{'WA': 'red', 'NT': 'green', 'Q': 'red',
'SA': 'blue', 'NSW': 'green', 'V': 'red', 'T': 'blue'}
结果分析
当然,由于每一步向下拓展的城市有赖于字典的排列顺序是否足够好,因此并不一定能跑出结果,这时就需要进行下一步的优化。
融入最大受限变量、最少约束值原则、前向检验的回溯算法
24/4/21 修改
修改了递归部分代码
改善了Shunde地图DaLiang定义域出错的情况。
def recursive_backtracking(map, coloringState, domain, index):
if index >= len(list(map)):
return coloringState
city = list(map)[index]
if ColorComplete(map, coloringState):
return coloringState
for color in domain[city]:
tmpState = copy.deepcopy(coloringState)
tmpDomain = copy.deepcopy(domain)
coloringState[city] = color
if constraint(map, coloringState):
updateDomain(map, coloringState, domain)
coloringState = recursive_backtracking(map, coloringState, domain, index + 1)
elif not constraint(map, coloringState):
domain = copy.deepcopy(tmpDomain)
coloringState = copy.deepcopy(tmpState)
return coloringState
MRV
MRV的解释
寻找剩余合法取值最少的变量。
MRV代码实现
def MRV(map, coloringState, domain):
tmpmap = {}
for city in map:
tmpmap[city] = map[city]
for city in map:
if coloringState[city] != 0:
del tmpmap[city]
Restdomain = []
excess = []
for city in tmpmap:
Restdomain.append(len(domain[city]))
minN = min(Restdomain)
for i in range(len(Restdomain)):
if minN == Restdomain[i]:
excess.append(i)
n = random.randint(0, len(excess))
return list(tmpmap)[n]
可以注意到,这里的MRV返回的是一个随机抽取的城市,这是因为拥有最少合法取值的城市结点往往不止一个,这里选择了返回随机一个最小定义域的城市。但是这并不一定会令回溯有解。
因此,对此进行优化,返回一个最小定义域的城市列表。虽会增加时间复杂度,但会保证有解。
def MRV_2(map, coloringState, domain):
tmpmap = copy.deepcopy(map)
for city in map:
if coloringState[city] != 0:
del tmpmap[city]
Restdomain = []
cities = []
for city in tmpmap:
Restdomain.append(len(domain[city]))
minN = min(Restdomain)
for i in range(len(Restdomain)):
if minN == Restdomain[i]:
cities.append(list(tmpmap)[i])
return cities
LCV
LCV的解释
转译能力有限,大意是尽量不选择之前未出现过的颜色,保证后面拓展到的城市的定义域足够充足。
LCV代码实现
def LCV(chosencity, map, coloringState, domain):
Colors = {}
for color in domain[chosencity]:
Colors[color] = 0
for i in map.keys():
if coloringState[i] != 0:
for color in domain[i]:
if color in domain[chosencity]:
Colors[color] += 1
if len(list(Colors)) != 0:
minColor = min(Colors.values())
for key, value in Colors.items():
if value == minColor:
bestColor = key
break
return bestColor
else:
return False
前向检验
前向检验解释
这里的举例是每为一个变量(城市)赋值,就相应地对该变量的邻接变量的定义域删减。
如第二行中WA=red,就相应地令它的邻接城市NT和SA的定义域删去red。
若检测到有邻接变量的定义域已经空了,就说明当前情况执行下去必然无解,就跳出该层,回溯。避免作不必要的运算。
前向检验代码实现
def forward_check(map, city, color, domain, coloringState):
for neighbor in map[city]:
if coloringState[neighbor] == 0:
if color in domain[neighbor]:
domain[neighbor].remove(color)
cnt = len(domain[neighbor])
if cnt <= 0:
return False
return True
结合后的回溯算法
代码
def Recursive_2nd(map, coloringState, domain):
#if cnt >= len(list(map)):
#return coloringState
if ColorComplete(map, coloringState):
return coloringState
cities = MRV_2(map, coloringState, domain)
for city in cities:
tmpdomain = copy.deepcopy(domain)
tmpcling = copy.deepcopy(coloringState)
color = LCV(city, map, coloringState, domain)
if color == False: return coloringState
if forward_check(map, city, color, domain, coloringState):
if constraint(map, coloringState):
coloringState[city] = color
updateDomain(map, coloringState, domain)
Recursive_2nd(map, coloringState, domain)
if ColorComplete(map, coloringState):
return coloringState
else:
coloringState = copy.deepcopy(tmpcling)
domain = copy.deepcopy(tmpdomain)
for othercolor in tmpdomain[city]:
if othercolor != color:
if forward_check(map, city, othercolor, domain, coloringState):
coloringState[city] = othercolor
updateDomain(map, coloringState, domain)
Recursive_2nd(map, coloringState, domain)
return coloringState
def BackTracking2nd(map, coloringState, domain):
print("2nd:")
t2 = time.perf_counter()
Recursive_2nd(map, coloringState, domain)
t3 = time.perf_counter()
print((t3 - t2) * 1000, 'ms')
print(coloringState)
由于本人在调试过程中深受浅拷贝的迫害,因此在代码中调用了许多deepcopy,也许空间复杂度异常大。
结果展示
2nd:
0.6265999982133508 ms
{'WA': 'red', 'NT': 'green', 'Q': 'red',
'SA': 'blue', 'NSW': 'green', 'V': 'red', 'T': 'blue'}
分析
经过断点调试发现,它跳过了很多无法找到解的赋值,优化了搜索过程,理论而言,应该极大优化了空间复杂度。
加入AC-3 的回溯算法
AC3
AC-3伪码
大意是令X->Y这样的蕴含等值式为真,即X取任何一种取值时,Y都有相应合法取值。加入X、Y是邻接的,X={r,g},Y={g,b},X取r时,Y定义域的两个取值都合法;X取g时,Y还剩b作为合法取值,那么这个时候X与Y便弧相容。
代码转化
def AC3(city, map, coloringState, domain):
for neighbor in map[city]:
if coloringState[neighbor] == 0:
for color in domain[city]:
cnt = len(domain[neighbor])
if color in domain[neighbor]:
cnt = len(domain[neighbor]) - 1
if cnt <= 0:
return False
return True
AC-3融入回溯
代码
def recursive_3rd(map, coloringState, domain):
if ColorComplete(map, coloringState):
return coloringState
cities = MRV_2(map, coloringState, domain)
for city in cities:
tmpcling = copy.deepcopy(coloringState)
tmpdomain = copy.deepcopy(domain)
if AC3(city, map, coloringState, domain):
for color in tmpdomain[city]:
coloringState[city] = color
if constraint(map, coloringState):
updateDomain(map, coloringState, domain)
recursive_3rd(map, coloringState, domain)
if ColorComplete(map, coloringState):
return coloringState
else:
coloringState = copy.deepcopy(tmpcling)
domain = copy.deepcopy(tmpdomain)
elif not AC3(city, map, coloringState, domain):
continue
return coloringState
def BackTracking3nd(map, coloringState, domain):
print("3rd:")
t4 = time.perf_counter()
recursive_3rd(map, coloringState, domain)
t5 = time.perf_counter()
print((t5 - t4) * 1000, 'ms')
print(coloringState)
结果展示
3rd:
0.4887000031885691 ms
{'WA': 'red', 'NT': 'green', 'Q': 'red',
'SA': 'blue', 'NSW': 'green', 'V': 'red', 'T': 'red'}
结果分析
在MRV返回城市列表并开始遍历时,先用AC-3判断是否满足弧相容,可以剔除更多无法找到解的情况,优化搜索空间,同时对比第二种回溯算法,也可以发现运行时间更短了。
其他地图的着色
顺德地图
地图形式化
Shunde_map = {
'DaLiang':['LunJiao','RongGui','LeLiu'],
'RongGui':['DaLiang','LeLiu','XingTan'],
'LunJiao':['DaLiang','BeiJiao','LeLiu'],
'BeiJiao':['ChenCun','LeCong','LunJiao','LeLiu'],
'ChenCun':['LeCong','BeiJiao'],
'LeCong':['ChenCun','BeiJiao','LeLiu','LongJiang'],
'LongJiang':['LeCong','LeLiu','XingTan'],
'LeLiu':['DaLiang','RongGui','LunJiao','BeiJiao','LeCong','LeCong','LongJiang','XingTan'],
'XingTan':['RongGui','LeLiu','LongJiang','JunAn'],
'JunAn':['XingTan']
}
运行结果展示
1st:
0.316400000883732 ms
{'DaLiang': 0, 'RongGui': 'green', 'LunJiao': 'blue', 'BeiJiao': 'red', 'ChenCun': 'green', 'LeCong': 'blue', 'LongJiang': 'green', 'LeLiu': 'yellow', 'XingTan': 'blue', 'JunAn': 'green'}
2nd:
1.2170999980298802 ms
{'DaLiang': 'red', 'RongGui': 'yellow', 'LunJiao': 'yellow', 'BeiJiao': 'red', 'ChenCun': 'green', 'LeCong': 'blue', 'LongJiang': 'red', 'LeLiu': 'green', 'XingTan': 'blue', 'JunAn': 'green'}
3rd:
1.1254999990342185 ms
{'DaLiang': 'red', 'RongGui': 'green', 'LunJiao': 'yellow', 'BeiJiao': 'green', 'ChenCun': 'blue', 'LeCong': 'red', 'LongJiang': 'green', 'LeLiu': 'blue', 'XingTan': 'red', 'JunAn': 'green'}
结果分析
由于是较复杂的地图,遵循四可着色定理,放出四种颜色RGBY。
可以发现,第一种依赖地图字典排列顺序的回溯算法,在并不是最优排序情况下,并不能完成着色,第一个城镇赋值为0。(也许是鄙人代码能力的问题,没有写出最好的回溯)
因此在图着色问题上,鄙人认为MRV是必要的,待拓展的城市结点需要一定的排序。
24/4/21 修改后结果
1st:
0.7145999989006668 ms
{'DaLiang': 'red', 'RongGui': 'green', 'LunJiao': 'blue', 'BeiJiao': 'red', 'ChenCun': 'green', 'LeCong': 'blue', 'LongJiang': 'green', 'LeLiu': 'yellow', 'XingTan': 'blue', 'JunAn': 'green'}
2nd:
0.8557999972254038 ms
{'DaLiang': 'red', 'RongGui': 'yellow', 'LunJiao': 'yellow', 'BeiJiao': 'red', 'ChenCun': 'green', 'LeCong': 'blue', 'LongJiang': 'red', 'LeLiu': 'green', 'XingTan': 'blue', 'JunAn': 'green'}
3rd:
0.712499997462146 ms
{'DaLiang': 'red', 'RongGui': 'green', 'LunJiao': 'yellow', 'BeiJiao': 'green', 'ChenCun': 'blue', 'LeCong': 'red', 'LongJiang': 'green', 'LeLiu': 'blue', 'XingTan': 'red', 'JunAn': 'green'}
2nd:
受浅拷贝迫害,原先版本的回溯算法难以找到解。同时也发现,deepcopy次数增多大大增加了时间复杂度。
广州地图
图源:https://img.alicdn.com/bao/uploaded/i3/386099135/O1CN01HAS23h2HLsmB6QKaH_%21%21386099135.jpg
地图形式化
Canton_Map = {
'LiWan' : ['YueXiu','HaiZhu','BaiYun','PanYu'],
'HaiZhu' : ['LiWan','YueXiu','TianHe','PanYu','HuangPu'],
'YueXiu':['LiWan','TianHe','HaiZhu','BaiYun'],
'TianHe':['YueXiu','HaiZhu','HuangPu','BaiYun'],
'PanYu':['LiWan','HaiZhu','HuangPu','NanSha'],
'NanSha':['PanYu'],
'BaiYun':['LiWan','YueXiu','TianHe','HuangPu','CongHua','HuaDu'],
'HuaDu':['BaiYun','CongHua'],
'HuangPu':['TianHe','HaiZhu','PanYu','ZengCheng','CongHua','BaiYun'],
'ZengCheng':['CongHua','HuangPu'],
'CongHua':['HuaDu','BaiYun','HuangPu','ZengCheng']
}
结果展示
2nd:
1.4500999968731776 ms
{'LiWan': 'red', 'HaiZhu': 'yellow', 'YueXiu': 'green', 'TianHe': 'red', 'PanYu': 'green', 'NanSha': 'red', 'BaiYun': 'yellow', 'HuaDu': 'green', 'HuangPu': 'blue', 'ZengCheng': 'green', 'CongHua': 'red'}
3rd:
1.3534000026993454 ms
{'LiWan': 'red', 'HaiZhu': 'green', 'YueXiu': 'blue', 'TianHe': 'red', 'PanYu': 'yellow', 'NanSha': 'red', 'BaiYun': 'green', 'HuaDu': 'blue', 'HuangPu': 'blue', 'ZengCheng': 'green', 'CongHua': 'red'}
24/4/21 优化后结果展示
1st:
1.6166999994311482 ms
{'LiWan': 'red', 'HaiZhu': 'green', 'YueXiu': 'blue', 'TianHe': 'red', 'PanYu': 'yellow', 'NanSha': 'green', 'BaiYun': 'yellow', 'HuaDu': 'blue', 'HuangPu': 0, 'ZengCheng': 0, 'CongHua': 0}
2nd:
0.8592000012868084 ms
{'LiWan': 'red', 'HaiZhu': 'yellow', 'YueXiu': 'green', 'TianHe': 'red', 'PanYu': 'green', 'NanSha': 'red', 'BaiYun': 'yellow', 'HuaDu': 'green', 'HuangPu': 'blue', 'ZengCheng': 'green', 'CongHua': 'red'}
3rd:
0.8270999969681725 ms
{'LiWan': 'red', 'HaiZhu': 'green', 'YueXiu': 'blue', 'TianHe': 'red', 'PanYu': 'yellow', 'NanSha': 'red', 'BaiYun': 'green', 'HuaDu': 'blue', 'HuangPu': 'blue', 'ZengCheng': 'green', 'CongHua': 'red'}
还是可以用回原来的结论,第一种回溯算法由于极度依赖排列顺序,有时候无法得出解,尤其在复杂的地图中。
后记
小弟转述课本原文以及伪码转译能力有限,如有难以理解的部分,还望指出。
希望文章能帮助到各位。