图的m着色问题——回溯法及其优化(变量排序MRV, 值排序MCV, 前向检查ForwardChecking, 智能回溯, 边相容,K阶相容)python C++实现

图的m着色问题背景

背景知识

       为地图或其他由不同区域组成的图形着色时,相邻国家/地区不能使用相同的颜色。 我们可能还想使用尽可能少的不同颜色进行填涂。一些简单的“地图”(例如棋盘)仅需要两种颜色(黑白),但是大多数复杂的地图需要更多颜色。
       每张地图包含四个相互连接的国家时,它们至少需要四种颜色。1852年,植物学专业的学生弗朗西斯·古思里(Francis Guthrie)于1852年首次提出“四色问题”。他观察到四种颜色似乎足以满足他尝试的任何地图填色问题,但他无法找到适用于所有地图的证明。这个问题被称为四色问题。长期以来,数学家无法证明四种颜色就够了,或者无法找到需要四种以上颜色的地图。直到1976年德国数学家沃尔夫冈·哈肯(Wolfgang Haken)(生于1928年)和肯尼斯·阿佩尔(Kenneth Appel,1932年-2013年)使用计算机证明了四色定理,他们将无数种可能的地图缩减为1936种特殊情况,每种情况都由一台计算机进行了总计超过1000个小时的检查。
       他们因此工作获得了美国数学学会富尔克森奖。在1990年,哈肯(Haken)成为伊利诺伊大学(University of Illinois)高级研究中心的成员,他现在是该大学的名誉教授。
       四色定理是第一个使用计算机证明的著名数学定理,此后变得越来越普遍,争议也越来越小 更快的计算机和更高效的算法意味着今天您可以在几个小时内在笔记本电脑上证明四种颜色定理。

问题描述

       我们可以将地图转换为平面图,每个地区变成一个节点,相邻地区用边连接,我们要为这个图形的顶点着色,并且两个顶点通过边连接时必须具有不同的颜色。附件是给出的地图数据,请针对三个地图数据尝试分别使用5个(le450_5a),15个(le450_15b),25个(le450_25a)颜色为地图着色。

回溯法的原理及其实现

回溯法基本思想

       回溯法(backtracking)是暴力搜索法中的一种。对于某些计算问题而言,回溯法是一种可以找出所有(或一部分)解的一般性算法,尤其适用于约束补偿问题(在解决约束满足问题时,我们逐步构造更多的候选解,并且在确定某一部分候选解不可能补全成正确解之后放弃继续搜索这个部分候选解本身及其可以拓展出的子候选解,转而测试其他的部分候选解)。
       在图着色问题中,我们去尝试每个顶点可能的取值,保证两个邻接的顶点颜色不一致,是一个典型的约束满足问题,可以使用回溯法解决。
       回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现,现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
              A.找到一个可能存在的正确的答案
              B.在尝试了所有可能的分步方法后宣告该问题没有答案
       在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。

朴素回溯法解决图的m着色问题

       给定无向连通图G=(V,E) 和 c种不同的颜色,用这些颜色为图G的各顶点着色,每个顶点涂一种颜色。如果一个图最少需要c种颜色才能使图中每条边连接的2个顶点涂不同颜色,则称c为该图的色数。
       在这个问题的解决中,我们首先需要将实际的地图抽象成一个无向图,并为其建立邻接矩阵或者邻接表。如下图所示。
在这里插入图片描述

接着使用回溯法搜索求解,实际上是深度优先+变量分配+约束检查,算法简单描述为:
       A. 初始状态:所有变量为分配颜色;
       B. 后继函数:对未分配的变量分配一个值;
       C. 目标测试:变量都已分配值而且满足约束条件。
       实际上,回溯法=深度优先+变量分配+约束检查。

    def trackback(self):
        """
        普通回溯算法
        :return: True: 有解 or False: 无解
        """
        k = 0  # 递归层数
        while k >= 0:
            self.G[k].color = self.G[k].color + 1  # 给当前顶点涂色
            # 检查涂色是否合法,不合法则尝试其他颜色
            while self.G[k].color <= self.color_num:
                if self.check(k):
                    break
                else:
                    self.G[k].color = self.G[k].color + 1
            # 退出条件:所涂颜色合法并且达到最大递归深度
            if self.G[k].color <= self.color_num and k == self.depth:
                return True
            # 如果颜色合法,但还没达到最大递归深度,则继续递归求解
            elif self.G[k].color <= self.color_num and k < self.depth:
                k = k + 1
            # 否则回溯
            else:
                self.G[k].color = 0
                k = k - 1
        return False

回溯优化策略

回溯法优化——变量排序MRV

       正如前面所说,回溯法如果只是无信息地盲目搜索,在最坏情况下需要达到指数级别的时间复杂度,这对个人笔记本来说就是一个灾难,根本无法求解。
       注意到,根据约束条件我们可以得到一定的启发式信息,利用这些启发式信息进行启发式的回溯搜索可以大大提高速度。而启发式算法一般都会采取剪枝的策略,这样就可以减少空间搜索树的分支,从而提高搜索速率。
       MRV正是针对这一思想出发的一种排序-预剪枝策略,其性能提升有时可以达到1000倍。MRV的思想是使得顶点失败最快,主要由两个部分构成排序规则:值域由少到多,度由大到小。即下一次选择的变量,尽可能选择取值最少的,受到其他变量约束最大的顶点,这样可以让其更快的失败,从而避免没有必要的搜索。下面两张图分别是最少可取值排序和度排序的例子。

在这里插入图片描述

在这里插入图片描述

def MRV_cmp(v1, v2):
    """
    变量排序:可取值从少到多,相同的按照度从大到小排序
    :param v1: 顶点1
    :param v2: 顶点2
    :return: True or False
    """
    if len(v1.legal_color) != len(v2.legal_color):
        return len(v1.legal_color) < len(v2.legal_color)
    else:
        return len(v1.adjacency) > len(v2.adjacency)

回溯法优化——值排序MCV

上面讲了怎么选取变量,一旦变量选好了,变量的值应该怎么选呢?
最少约束值:优先选择的赋值是给邻居变量留下更多的选择
下图中,右上角的Q应选择红色,若选择蓝色,则SA没有可选的颜色了
在这里插入图片描述

回溯法优化——前向检查ForwardChecking

       除了预剪枝之外,我们还可以利用约束条件在搜索过程中进行剪枝,这便是前向检查,前向检查的思想为检查未分配变量的合法剩余值,当任何变量没有合法值时终止。

在这里插入图片描述

       上图中,长条形颜色表示给对应地图上色,正方形颜色表示未上色,但是可以选择的合法颜色,按顺序对WA,Q,V赋值后,SA值域为空,故算法立刻回溯。
       前向检验能检测出很多不相容,但是它无法检测所有不相容。它使当前变量边相容,但是并不向前看使其他变量边相容。上图中第三行,当WA,Q都赋值后,NT和SA都只能是蓝色了。前向检验向前看得不够远,不足以观察出这种不相容;NT和SA邻接所以不能取相同的值。

def forward_checking(vex, color):
    """
    前向检查:将违反约束的现有已分配合法颜色去掉,只检查尚未涂色的邻接点
    :param vex: 顶点
    :param color: 颜色
    :return: 改变了合法颜色取值范围的邻接点id
    """
    record = []
    # 遍历邻接点
    for k in range(0, len(vex.adjacency)):
        # 寻找尚未涂色的邻接点
        if vex.adjacency[k].color == 0:
            # 判断是否需要缩小取值范围
            for j in range(0, len(vex.adjacency[k].legal_color)):
                if vex.adjacency[k].legal_color[j] == color:
                    del vex.adjacency[k].legal_color[j]
                    record.append(k)
                    break
    return record

def forward_recover(vex, color, record):
    """
    前向检查恢复,与前向检查对应
    :param vex: 顶点
    :param color: 颜色
    :param record: 前向检查影响的邻接点id
    """
    for i in range(0, len(record)):
        vex.adjacency[record[i]].legal_color.append(color)

边相容Arc consistency

       边相容的提出,主要是为了解决前向检查看的不够远的问题,其思想是:当变量X赋值后,边相容从与X相邻的弧中所有其他没有赋值的变量出发,若一旦某个变量的值域变为空,则算法立刻回溯。

在这里插入图片描述

       上图中,当WA,Q都赋值后,边相容算法可以检测和SA邻接的NT的取值。若NT没有可取的值,则回溯。
       边相容检测故障早于前向检查。

智能回溯AI Trackback

       智能回溯的思想是:当涂色失败时,回溯到与失败顶点最近冲突的顶点。
在这里插入图片描述

       在上图中,如果按照之前的时序回溯到T,我们给T换一种颜色,继续涂色SA,还是会发生冲突。因此我们给SA建立一个冲突及{Q,NSW,W,V},我们需要回溯到最近冲突的顶点,即V。
       智能回溯的意义在于,可以减少不必要的尝试,直接回溯到能够解决问题的顶点,其性能在一般情况下可以提高将近20倍。

 def MRV_ForwardChecking_AItrackback(self, dep, conflict):
        """
        回溯策略:
        1. 变量排序:可取值由少到多,顶点的度由大到小,也称预剪枝策略之一
        2. 前向检查:将违反约束的现有已分配合法颜色去掉
        3. 智能回溯:区别于时序回溯,智能回溯是失败时回溯到最近冲突的点
        :param dep: 当前递归深度
        :param conflict: 与失败顶点邻接的顶点id
        :return: True: 有解 or False: 无解
        """
        # 如果当前深度已经达到最大递归深度,说明已经求得解
        if dep > self.depth:
            return True

        self.G.sort(key=cmp_to_key(MRV_cmp))  # 排序

        # 如果当前顶点无合法取值,则失败返回
        if len(self.G[dep].legal_color) == 0:
            conflict = self.G[dep].adjId  # 记录失败顶点的邻接点
            return False

        self.visited.append(self.G[dep].id)  # 将当前顶点放入已访问顶点集中

        # 遍历合法取值,每一个都去尝试
        for i in range(0, len(self.G[dep].legal_color)):
            self.G[dep].color = self.G[dep].legal_color[i]  # 涂色
            record = forward_checking(self.G[dep], self.G[dep].color)  # 前向检查
            # 递归回溯
            if self.MRV_ForwardChecking(dep + 1):
                return True
            else:
                forward_recover(self.G[dep], self.G[dep].color, record)  # 前向恢复
                conflict_id = list(set(conflict).intersection(set(self.visited)))  # 求出冲突集
                # 如果当前顶点不在冲突集中,则继续回溯, 智能回溯主体
                if self.G[dep].color not in conflict_id:
                    self.G[dep].color = 0
                    del self.visited[-1]
                    return False

        # 如果没有匹配的值,置当前颜色为0,返回false
        self.G[dep].color = 0
        conflict = self.G[dep].adjId
        del self.visited[-1]  # 失败回溯
        return False

K阶相容

       K阶相容的含义是:在每K个变量中,两两都满足约束条件,特别地1阶相容是顶点相容,2阶相容是边相容,3阶相容是路径相容,强K阶相容(当K等于顶点数时)意味着无需回溯就可以求解问题。但是随着K的增大,计算的代价也相应增大。在本实验中,选择K=3可以得到比较好的结果。

资源下载

实验伪代码下载

Latex伪代码下载

python源码下载

python源码下载

C++源码下载

C++源码下载

地图数据集下载

测试数据-莱顿图和随机平面图下载

授课用ppt下载

ppt下载

注意:以上资源只作为学术交流,请勿将文中任何资料用作商业用途或者作业提交。版权所有,引用请注明出处。

评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kim‘s blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值