人工智能导论 图着色问题 CSP (Python实现)

澳大利亚地图着色问题

dcf7ccfc92924c99b1d4f2b6d0c5e0e6.png

图源 : 《人工智能——一种现代的方法》

实验内容

  1. 使用回溯搜索算法解决地图着色问题。
  2. 实现最大受限变量、最大约束变量、最少约束值原则和前向检查原则,并将其融入回溯搜索算法中以解决地图着色问题。
  3. 在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)
回溯算法实现
回溯算法伪码

f8ded0b6af4b4dba90ff2b0fbf4f7d41.png

图源 : 《人工智能——一种现代的方法》

尝试翻译伪码
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'}

4abe7acd251d4a49823a7a4c510a00b6.png

结果分析

        当然,由于每一步向下拓展的城市有赖于字典的排列顺序是否足够好,因此并不一定能跑出结果,这时就需要进行下一步的优化。

融入最大受限变量、最少约束值原则、前向检验的回溯算法

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的解释

5f5584abb872482fbc284773b034e65e.png

        寻找剩余合法取值最少的变量。

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的解释

c1d7ad202e32442aa1c45f2524123ca8.png

        转译能力有限,大意是尽量不选择之前未出现过的颜色,保证后面拓展到的城市的定义域足够充足。

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
前向检验
前向检验解释

7a973fa82b4f4c6ca5d6ba5eb4301b21.png

        这里的举例是每为一个变量(城市)赋值,就相应地对该变量的邻接变量的定义域删减。

如第二行中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伪码

6ec8c46da76641a7852155dd594ae368.png

        大意是令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判断是否满足弧相容,可以剔除更多无法找到解的情况,优化搜索空间,同时对比第二种回溯算法,也可以发现运行时间更短了。

其他地图的着色

顺德地图

b141af7313a3f38af269202d98bc2f9b.jpeg

图源:https://img.zcool.cn/community/01cbbc564ad0f76ac7251c946bba9f.jpg?x-oss-process=image/auto-orient,1/resize,m_lfit,w_1280,limit_1/sharpen,100/quality,q_100/format,webp

地图形式化
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次数增多大大增加了时间复杂度。

广州地图

7fef870a429c7b5f1f3bf5d848f14b31.jpeg

图源: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'}

        还是可以用回原来的结论,第一种回溯算法由于极度依赖排列顺序,有时候无法得出解,尤其在复杂的地图中。

后记

    小弟转述课本原文以及伪码转译能力有限,如有难以理解的部分,还望指出。

    希望文章能帮助到各位。

  • 15
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值