算法设计与分析:实验三 回溯法——地图填色问题

实验内容与要求:

问题描述:

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

实验要求:

对下面这个小规模数据,利用四色填色测试算法的正确性;

对附件中给定的地图数据填涂;

随机产生不同规模的图,分析算法效率与图规模的关系(四色)

基本回溯算法

思路

  • 从图的第一个节点开始,尝试为其染色。
  • 对于当前节点,尝试为其选择一种颜色,然后递归地尝试为其相邻的未染色节点染色。
  • 如果染色过程中出现了冲突,即当前节点与已染色的相邻节点颜色相同,就回溯到上一步重新尝试其他颜色。
  • 当所有节点都染色完毕,即递归到最后一个节点时,找到了一种合法的染色方案,返回 1。
  • 统计所有合法染色方案的数量,并返回结果。

 思维导图

伪代码

深搜回溯函数

  1. function DFS1(x):
  2.     // 如果已经遍历完所有节点,则返回 1 表示找到了一种合法的染色方案
  3.     if x > n:
  4.         return 1
  5.     cnt = 0
  6.     u = id[x] // 当前节点的编号
  7.     // 遍历尝试每一种颜色
  8.     for i from 1 to MAXCOL:
  9.         valid = true
  10.         // 遍历当前节点之前的节点(即染过的节点)
  11.         for j from 1 to x - 1:
  12.             // 如果当前节点与之前染色的节点相邻且颜色相同,则该颜色不合法
  13.             if link[u][id[j]] and color[id[j]] == i:
  14.                 valid = false
  15.                 break
  16.         // 如果当前颜色合法,则尝试用该颜色染色当前节点并继续递归
  17.         if valid:
  18.             color[u] = i // 尝试用颜色 i 染色节点 u
  19.             cnt += DFS1(x + 1) // 继续递归下一个节点
  20.     return cnt
  21. function Solve1():
  22.     // 初始化 color 数组,全部置为 0,表示未染色
  23.     initialize color array with zeros
  24.     // 调用深度优先搜索函数从第一个节点开始染色
  25.     return DFS1(1)

运行结果

由运行结果可以看出得到正确的填色方案数为480,但是运行时间为0.00ms,应该是因为数据规模较小,导致程序执行速度非常快,低于计时的精度。

算法过程:

在着色尝试过程中,依次尝试可用的颜色,并更新与当前节点相邻的节点的颜色状态,剔除不可用的颜色。若成功找到一种着色方案,即所有顶点都被成功着色,则返回 True。下面图示以其中一种染色方案为例(从左往右,从上往下):

 

时间复杂度

  • 在每一次递归调用 DFS1 中,对当前节点进行颜色的尝试,最多需要尝试 MAXCOL 种颜色,所以对于每一个节点,该部分的时间复杂度为 O(MAXCOL)。
  • 在每一次颜色尝试中,需要遍历当前节点的所有邻接节点,最坏情况下,有 x-1 个邻接节点,因此该部分的时间复杂度为 O(x)。
  • 因为递归调用会在每个节点处进行,总的递归调用次数最多为节点数量 x。
  • 因此,总的时间复杂度为 O(x * MAXCOL * x) = O(x^2 * MAXCOL)。即O(n^3),这是一个巨大的指数级别,如果节点数量较少,并且最大可选颜色数量也不是很大时,算法的性能是可以接受的,但是一旦数量增加就很难运行出结果,所以我们一定要对此回溯法进行优化。

 优化深搜回溯算法

四种深搜回溯优化剪枝优化搜索顺序,排除等效冗余,可行性剪枝和最优性剪枝。

  • 优化搜索顺序:通过合理的搜索顺序,优先探索最有希望的分支,可以尽早地找到解,从而减少搜索时间。
  • 排除等效冗余:在搜索过程中,避免探索等效的状态或者已经探索过的状态,以减少冗余的搜索。
  • 可行性剪枝:在搜索过程中,根据当前状态的局部信息,判断某些分支不可能产生解,从而减少不必要的搜索。
  • 最优性剪枝:在搜索过程中,根据问题的特性,通过一些启发式方法,提前排除掉不可能比当前最优解更好的分支,从而加速搜索过程。

在地图填色这个问题中,目标是为每个节点分配一个颜色,以使相邻节点的颜色不同,但通常不存在一个“最佳”的颜色分配方案。因此不存在最优解。所以使用到了前面三种剪枝方法,在下面的优化方法种会指明该种优化方法是属于哪一种优化方法。

最大度结点优先涂色(最大度优化-优化搜索顺序)

按照节点的度数从大到小进行排序,优先考虑处理度数较高的节点。这是因为度数较大的节点在填色过程中受到的约束更多,因此在确定其颜色时会更加复杂。当相邻节点的颜色选择较多且不相同时,度数较大的节点的可选颜色将会受到更大的限制,可能导致其剩余可填涂的颜色数量减少甚至为零。以实验样例为例,当4、7、8、9这四个节点各自填上不同的颜色时,节点6就会发现没有可选的颜色可以填充,从而需要进行回溯操作,这会导致时间的浪费。如果能够首先处理度数较大的节点6,就可以尽量避免这种情况,提高算法的效率。

伪代码

最大化优化DFS

  1. function DFS2(x):
  2.     if x > n:
  3.         return 1  
  4.     u = 0  
  5.     for i = 1 to n:
  6.         if not colored[i] and deg[i] > deg[u]:  // 如果节点未被着色且度数比当前最大度数大
  7.             u = id[i]  // 更新节点编号为当前节点编号
  8.     temp = 0  
  9.     for i = 1 to MAXCOL:  
  10.         valid = true  // 初始化当前颜色是否可用
  11.         for j = 1 to x - 1:  // 遍历已经着色的相邻节点
  12.             if link[u][id[j]] and color[id[j]] == i:  // 如果当前颜色与相邻节点的颜色相同
  13.                 valid = false  // 当前颜色不可用
  14.                 break  
  15.         if valid:  
  16.             color[u] = i  
  17.             temp += DFS2(x + 1)  // 递归调用DFS2,继续填充下一个节点的颜色
  18.             color[u] = 0  // 回溯,将当前节点颜色还原为未着色状态
  19.     return temp  // 返回当前节点的所有合法着色方案总数

剩余可填颜色数量最少的先涂(最少颜色优化-优化搜索顺序)

在给地图涂色的时候,优先选择先给剩余可填涂颜色比较少的点涂色。

  • 考虑对每一个点定义两个属性:一个属性表示剩余可填颜色的数量,另一个属性表示可以填充哪些颜色
  • 涂色时,先在所有的点中找到剩余可填涂颜色最少的点,遍历该点剩余可以填涂的颜色进行搜索。
  • 在涂完该点之后,需要对该点的临接点的两个属性——剩余可填涂颜色以及剩余可填涂颜色的数量,进行更新。
  • 在回溯时,需要对临接点的修改进行复原。

填涂颜色状态表示:使用二进制来表示使用一个二进制串来记录该点可以填涂的颜色有哪些,既二进制串中的第0位表示第一种颜色,第1位表示第二种颜色,该位为1时表示该点可以填涂该颜色,为0时表示该点不可以填涂该颜色。通过异或操作来填色(1^1=0),通过或操作来复原(1|0=1)。

伪代码:

最少颜色优化DFS

  1. function DFS3(x):
  2.     if x > n:  return 1
  3.     u = 0  
  4.     for i = 1 to n:  
  5.         if not color[i] and siz[i] < siz[u]:  // 如果节点未被着色且剩余可填涂颜色数量比当前最小值小
  6.             u = id[i]  // 更新节点编号为当前节点编号
  7.     cnt = 0  
  8.     for i = 1 to MAXCOL:  // 遍历所有颜色
  9.         if state[u] & (1 << (i - 1)):  // 如果当前颜色可用(二进制判断)
  10.             color[u] = i  // 将当前节点着色为当前颜色
  11.             limit = 1 << (i - 1)  // 当前颜色的限制
  12.             for j = 1 to n:  // 遍历所有邻接点
  13.                 if link[u][j] and not color[j] and (state[j] & limit):  // 如果相邻且未被着色,并且该颜色可用
  14.                     colored[u][j] = true  // 记录该点被染色(被更新了)
  15.                     state[j] = state[j] ^ limit  // 更新该点把1变成0
  16.                     siz[j]--  // 减少相邻点的可填涂颜色数量
  17.             cnt += DFS3(x + 1)  // 递归调用DFS3,继续填充下一个节点的颜色
  18.             color[u] = 0  // 回溯,将当前节点颜色还原为未着色状态
  19.             for j = 1 to n:  
  20.                 if colored[u][j]:  // 如果已被染色
  21.                     colored[u][j] = false  // 取消染色标记
  22.                     state[j] = state[j] | limit  // 把0变成1
  23.                     siz[j]++  // 恢复相邻点的可填涂颜色数量
  24.     return cnt  // 返回当前节点的所有合法着色方案总数
  25. function Solve3():
  26.     initialize color array with zeros  
  27.     initialize state array with -1  // 初始化状态数组为-1,表示所有节点都可以填充任意颜色
  28.     initialize colored array with false  // 初始化染色标记数组为false,表示所有相邻点未被染色
  29.     for i = 1 to n:
  30.         siz[i] = MAXCOL  // 初始化每个节点的剩余可填涂颜色数量为最大可填涂颜色数量
  31.     siz[0] = 100  
  32.     return DFS3(1)

基于最少颜色优化提前中断(可行性剪枝)

在最少颜色优化中的过程中,可以发现给一个点填上对应的颜色的时候,会使得对应的点的邻接点的可填涂颜色数量变少。而当某一个未涂色点的可填涂颜色数量被更新为0的时候,就表明该点已经不能在满足要求的情况下填色,所以没有必要继续搜索下去,而是可以直接让涂色点跳过该颜色找下一个颜色是否可以填涂。以实验样例为例,当4、7、5、8这四个节点各自填上不同的颜色时,当枚举节点8的邻点时就会发现节点6没有可选的颜色可以填充,此时节点8染红色的方案就没必要再递归下去直接剪掉即可,提高了算法的效率。

伪代码

基于最少颜色优化提前中断DFS

  1. function DFS4(x):
  2.     if x > n:  return 1  
  3.     u = 0
  4.     for i = 1 to n:
  5.         if not color[i] and siz[i] < siz[u]:  
  6.             u = id[i]  
  7.     cnt = 0  
  8.     for i = 1 to MAXCOL:  
  9.         if state[u] & (1 << (i - 1)):
  10.             color[u] = i  
  11.             flag = true  // 初始化标志位,表示当前方案是否合法
  12.             limit = 1 << (i - 1)
  13.             for j = 1 to n:  
  14.                 if link[u][j] and not color[j] and (state[j] & limit):  // 如果相邻且未被着色,并且该颜色可用
  15.                     colored[u][j] = true  // 记录该点被染色(被更新了)
  16.                     state[j] = state[j] ^ limit  // 更新该点把1变成0
  17.                     siz[j]--  // 减少相邻点的可填涂颜色数量
  18.                     if siz[j] == 0:  // 如果相邻点没有剩余可填涂颜色
  19.                         flag = false  // 当前方案不合法
  20.                         break  // 中断
  21.             if flag:  // 如果当前方案合法
  22.                 cnt += DFS4(x + 1)  // 递归调用DFS4,继续填充下一个节点的颜色
  23.             color[u] = 0  // 回溯,将当前节点颜色还原为未着色状态
  24.             for j = 1 to n:
  25.                 if colored[u][j]:
  26.                     colored[u][j] = false  
  27.                     state[j] = state[j] | limit  
  28.                     siz[j]++
  29.     return cnt

优先涂邻接点不能涂而该点可以涂的颜色(最小影响-优化搜素顺序)

对一个还没有涂色的点而言,为了让这个点涂的颜色对邻接点的影响尽可能的小(即尽可能不会影响邻接点剩余可填涂颜色),应该优先涂邻接点不能涂而该点可以涂的颜色。实现方式为在给一个点涂色的时候,先遍历邻接的点,对每一个邻接点的“邻接点不能涂而涂色点可以涂”的颜色进行计数,根据计数值从大到小排序,涂色点根据该顺序来遍历涂什么颜色,先选择邻接点不能涂而涂色点可以涂的数量最多的颜色。

最小影响DFS

  1. function DFS6(x):
  2.     if x > n:  return 1  
  3.     u = id[x]  
  4.     for i = 1 to MAXCOL:  // 初始化颜色计数值为0
  5.         col[i].second = i  // 颜色标号
  6.         col[i].first = 0  // 颜色计数值
  7.     for k = 0 to size of vmatrix[u]:  // 遍历当前节点的邻接节点
  8.         j = vmatrix[u][k]  // 获取当前邻接节点编号
  9.         if not color[j]:  // 如果邻接节点未被着色
  10.             for i = 1 to MAXCOL:  // 遍历所有颜色
  11.                 if (state[u] & (1 << (i - 1))) and not (state[j] & (1 << (i - 1))):  // 如果当前节点可以填涂颜色并且邻接节点不能填涂颜色
  12.                     col[i].first++  // 颜色计数值加一
  13.     sort col[] based on col[].first in ascending order  // 根据颜色计数值升序排序
  14.     cnt = 0
  15.     for i = MAXCOL to 1 DEC:  // 枚举计数大的颜色
  16.         ii = col[i].second  // 获取当前颜色标号
  17.         valid = true  
  18.         for j = 1 to x - 1:
  19.             if link[u][id[j]] and color[id[j]] == ii:  
  20.                 valid = false  
  21.                 break
  22.         if valid:
  23.             color[u] = ii  
  24.             cnt += DFS6(x + 1)  
  25.             color[u] = 0
  26.     return cnt

注意:该方法结合前面的优化一起使用,在选点时使用最大度优化和最少颜色优化,在填色时使用最小影响优化。即在尽可能找可选颜色最少的同时找度数尽可能大的点,在该点中选取填涂颜色的时候,先选择邻接点不能涂而涂色点可以涂的数量最多的颜色。

颜色对称性方案等效优化(排除等效冗杂)

根据排列组合数学的知识,当找到一个可行解并且使用了 M 种颜色时,将这个可行解的颜色互相调换一下可以得到另外的 M!−1 种可行解。例如如果只有3种颜色可以涂,则当找到上面的一种可行解的时候,就可以找到另外的3!-1种可行解。

如果可以找到所有“类型不同”的涂色方案可行解,那么就可以直接将每一种可行解乘上对应的M!,再累加起来就可以得到所有的可行解。

实际搜索中,要找到每一个不同类型的涂色方案并不容易,无法让搜索朝着找到每一个类型不同的涂色方案的方向搜索。因此,我们退而求其次,不再考虑所有节点的颜色组合,而是仅考虑初始节点、两个相邻节点以及三个节点的不同颜色组合。

处理思路:

根据节点数量的不同,分别处理不同的情况:

  • 如果节点数量为1,那么方案数为 MAXCOL,其中 MAXCOL 是可填颜色的最大种类数目。
  • 如果节点数量为2,那么方案数为 MAXCOL * (MAXCOL - 1)。
  • 如果节点数量大于2,按照一定策略先给图中的某些节点染色,然后计算剩余节点的涂色方案数。具体策略如下:
  1. 选择第一个节点(标号为1)并染上颜色1,更新与其相邻的节点的状态。
  2. 选择与节点1相邻度数最大的节点作为节点2,并染上颜色2,更新与其相邻的节点的状态。
  3. 再选择与节点1和2相邻度数最大的节点作为节点3,并染上颜色3,更新与其相邻的节点的状态。如果不存在这样的节点,则选择与节点1或2相邻且度数最大的节点作为节点3。
  4. 最后,计算剩余节点的涂色方案数,使用公式进行相乘即为总的涂色方案数。

伪代码:

颜色对称优化DFS

  1. function Solve8():
  2.        for each node i: // 初始化
  3.         color[i] = 0
  4.         state[i] = -1
  5.         for each adjacent node j of i:
  6.             colored[i][j] = false
  7.         siz[i] = MAXCOL
  8.     siz[0] = 100
  9.     ans = 0 // 计算涂色方案数
  10.     if n == 1:
  11.         ans = MAXCOL
  12.     else if n == 2:
  13.         ans = MAXCOL * (MAXCOL - 1)
  14.     else: // 给第一个节点染色为1,并更新相邻节点的状态
  15.         color[1] = 1
  16.         UpdateColor(1, 1)
  17.         // 选择与节点1相邻度数最大的节点作为节点2,并染色为2
  18.         u = 0
  19.         for each node i from 1 to n:
  20.             if link[1][i] and deg[u] < deg[i]:
  21.                 u = i
  22.         color[u] = 2
  23.         UpdateColor(u, 2)
  24.         // 选择与节点1和2相邻度数最大的节点作为节点3,并染色为3
  25.         v = 0
  26.         for each node i from 1 to n:
  27.             if link[u][i] and link[1][i] and deg[v] < deg[i]:
  28.                 v = i
  29.         if v == 0:
  30.             for each node i from 1 to n:
  31.                 if i != 1 and i != u and (link[i][u] or link[i][1]) and deg[v] < deg[i]:
  32.                     v = i
  33.             color[v] = 3
  34.             UpdateColor(v, 3)
  35.             ans = MAXCOL * (MAXCOL - 1) * (MAXCOL - 2) * DFS5(4)
  36.             RecoveryColor(v, 3)
  37.             color[v] = 1
  38.             UpdateColor(v, 1)
  39.             ans += MAXCOL * (MAXCOL - 1) * DFS5(4)
  40.         else:
  41.             color[v] = 3
  42.             UpdateColor(v, 3)
  43.             ans = MAXCOL * (MAXCOL - 1) * (MAXCOL - 2) * DFS5(4)
  44.     return ans

随机生成地图填色算法效率分析

图规模对运行时间的影响

点数为n,设边数为2*n,可填颜色种数为4。

每组规模个测试三次,取运行时间的平均值,运行结果如下图所示:

由上述图表可以知道,随着图规模的增大,算法的运行时间也相应增加。且当解的个数达到一定程度时,图规模大的运行时间显著增加。

这是因为在回溯算法中,每一层的节点对应着图中的一个节点,并且需要尝试填充不同的颜色。因此,随着图规模的扩大,图中节点的数量增加,导致回溯树的深度也随之增加。这样一来,整个回溯树的规模变得更大,算法需要更多的时间来遍历和搜索可能的解决方案。

图边数对运行时间的影响

点数为20,设边数为50、60、70、80,可填颜色种数为4,运行结果如下图所示:

由上面图表可知,当点数不变时,随着边数的增加,可行解数量减少,算法的运行时间也在减小。

这是因为在图的点数相同的情况下,图的边数增多会导致点的平均度数增多,图中的约束条件变得更加明显,因此可能的解决方案数量减少。

又因为在前面的基于最少颜色提前中断优化时,我们已知若某个点填了颜色c后将会使其相邻节点都无法填颜色c,就会把其相邻节点填颜色c的分枝剪掉。现在随着边数的增加,点的平均相邻点个数增多了,那么对于某个点来说,它能剪掉的分枝就变多了。而点数不变意味着回溯树形结构的层数不变。在回溯树形结构层数不变的情况下能剪掉的分枝变多了,减少了搜索空间的大小,因此算法的运行时间变小。

可填颜色数对运行时间的影响

点数为20,设边数为60,可填颜色种数为4、5、6、7。

由上图可知,随着可填颜色数的增加,可行解的个数也呈指数级增长。

这是因为增加了可选颜色的数量,使得每个节点的选择变得更加灵活,从而导致了更多的解的可能性。然而,这同时也导致了更多的搜索空间和更长的运行时间。

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值