顶点覆盖问题可以用几种不同的算法来实现,本篇文章使用的是分支限界法来实现,或许以后会介绍其他的实现算法,嘿嘿。
1.问题描述
给定一个N个点M条边的无向图G(点的编号从1至N),问是否存在一个不超过K个点的集合S,使得G中的每条边都至少有一个点在集合S中。
例如,如下图所示的无向图G(报告中算法分析过程中一直使用下面的图G)
(1)如果选择包含点1,2,6这3个点的集合S不能满足条件,因为边(3,7)两个端点都不在S中。
(2)如果选择包含点1,2,6,7这4个点的集合S虽然满足条件,但是它使用了4个点,其实可以使用更少的点,如下面(3)所示
(3)如果选择包含点1,3,5这3个点的集合S便满足条件,使得G中的每条边都至少有一个点在集合S中。
2.解题思路
我的解题思路基于分支定界和贪心两个策略,用一个优先队列维护当前可行的节点,每个节点维护着该节点情况下还可以选择的顶点数目k、需要覆盖的剩余边数e、顶点的状态state、顶点的边数edge等信息,这些节点的排序遵循下面的贪心策略,节点的扩展遵循下面的分支定界策略。总体思路是:
①将原图数据构造成一个解空间树的节点,利用定界策略判断是否有解,如果无解直接退出,如果有可能有解则插入到优先队列中;
②若优先队列不为空,那么便从优先队列中取出第一个可行的节点,进入步骤③,如果优先队列为空则退出;
③判断当前节点是否满足解的条件,如果满足便输出解退出,如果不满足便进入步骤④;
④检查当前节点是否可以扩展,不能扩展的话便进入②继续循环,如果能扩展的话则扩展,然后验证扩展到左右节点是否有解,将有解的扩展节点插入到优先队列中,然后进入②继续循环。
下面分别介绍下分支定界和贪心这两个策略:
(1)分支定界策略
首先,界的选择。在一个确定的无向图G中,每个顶点的边即确定了,那么对于该无向图中k个顶点能够覆盖的最多的边数e也就可以确定了!只要对顶点按照边的数目降序排列,然后选择前k个顶点,将它们的边数相加即能得到一个边数上界!因为这k个顶点相互之间可能有边存在也可能没有,所以这是个上界,而且有可能达到。以图G为例,各个顶点的边数统计,并采用降序排列的结果如下:
假设取k=3个点,那么有Up(e)=(3+3+2)=8 > 7 条边(7为图G的总边数),也就是说,如果从图G中取3个点,要覆盖8条边是有可能的。但是,如果取k=2个点,那么有Up(e)=(3+3)=6 < 7 条边,说明从图G中取2个点,是不可能覆盖G中的全部7条边的!基于这个上界,可以在分支树中扩展出来的节点进行验证,已知它还可以选择的顶点数目以及还需要覆盖的边的条数,加上顶点的状态(下面会分析说明)即可判断当前节点是否存在解!如果不存在即可进行剪枝了。
其次,顶点的状态。该策略中顶点有三种状态,分别为已经选择了的状态S1,不选择的状态S2,可以选择的状态S3。其中,不选择的状态S2对应解空间树中的右节点,不选择该节点,然后设置该节点为不选择状态S2。这点很重要,因为有了这个状态,可以使得上界的判断更为精确,因为只能从剩余顶点集中选择那些状态S3的顶点,状态S1和S2都不行,那么上界便会更小,也就更加精确,从而利于剪枝!
(2)贪心策略
贪心的策略是指可行的结点都是按照还需要覆盖的剩余边数的降序排列,即,每次选择的节点都是可行节点中还需要覆盖的边数最小的那个节点,因为它最接近结果了。
(3)例子分析
以图G为例,此时e=7(要覆盖的边数),取k=3,图G用邻接矩阵保存为全局数据,计算每个顶点的边数,然后降序排列。
步骤①判断是否可能有解,Up(e)=3+3+2=8>7,可能有解,那么将图G构造成一个解空间树的节点,它包含了还能选择的点数k=3,还需要覆盖的边数e=7,每个顶点的边数以及按边数大小的降序排列(上表),每个顶点的状态(初始时都是可选择的状态S3)。然后,将该节点插入到优先队列中,该优先队列是用最小堆实现的,按照前面的贪心策略对队列中的节点进行降序排列。
步骤②取出了优先队列中的根节点,很显然,还需要覆盖的边数为7,不为0,所以还不满足条件。接下来要检查是否能够进行扩展,从顶点集合中选择状态为可以选择的顶点中边数最多的点,该点存在为顶点2,接着进行扩展,扩展左节点时将还能选择的点数k-1=2,然后计算选择了该点之后删除了几条未覆盖的边,得到还需要覆盖的边数e=4,然后更新所有其他顶点的边数,并重新排序,最后将顶点2的状态设置为已经选择了;扩展右节点时,只要将顶点2的状态设置为不能选择,还能选择的点数k(=3),还需要覆盖的边数e(=7)保持不变。扩展完了之后,同样判断左右节点是否可能有解,如果有解,将该节点插入到优先队列中。这里左右节点都有解,那么将左右节点都插入到优先队列中,因为左节点还需要覆盖的边数e=4小于右节点的e=7,所以根据贪心策略,左节点在右节点的前面。上面两个步骤的图示如下,其中标明了顶点状态颜色。
算法然后继续进入步骤②,此时取出的是节点是刚才插入的左节点,很显然,还需要覆盖的边数为4,不为0,所以还不满足条件。接下来要检查是否能够进行扩展,从顶点集合中选择状态为可以选择的顶点中边数最多的点,该点存在为顶点3,接着进行扩展,扩展左节点时将还能选择的点数k-1=1,然后计算选择了该点之后删除了几条未覆盖的边,得到还需要覆盖的边数e=2,然后更新所有其他顶点的边数,并重新排序,最后将顶点3的状态设置为已经选择了;扩展右节点时,只要将顶点3的状态设置为不能选择,还能选择的点数k(=3),还需要覆盖的边数e(=7)保持不变。扩展完了之后,同样判断左右节点是否可能有解,如果有解,将该节点插入到优先队列中。这里左右节点都不可能有解,那么直接进入步骤②继续循环。上面这一步的图示如下:
算法按照上面的方式不断进行,最后满足条件的分支的过程是:
①不选择顶点2;②选择顶点3;③选择顶点1;④选择顶点5。
最后得到的满足条件的解是选择顶点1,3,5。
(4)复杂度分析
该算法优先队列使用的是最小堆实现的(O(nlgn)),对顶点按照边排序使用的是快速排序算法(O(nlgn)),解空间树的深度最多为顶点数目n,每层都要进行分支定界,所以每层的时间复杂度为O(nlgn),所以算法总的时间复杂度为O(n^2 lgn)。但是,为了实现分支定界,每个节点保存的信息量较多,空间复杂度较大。(有木有分析错了,我不太会分析复杂度)
青橙OJ系统的结果为:时间 156ms 空间 1.0MB
本人对指针领悟能力有限,C++也是一知半解,OJ只能用C或者C++,所以下面的C++代码效率不高,仅供参考,:-)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 blank">专栏作者。 顶点覆盖问题可以用几种不同的算法来实现,本篇文章使用的是分支限界法来实现,或许以后会介绍其他的实现算法,嘿嘿。 1.问题描述给定一个N个点M条边的无向图G(点的编号从1至N),问是否存在一个不超过K个点的集合S,使得G中的每条边都至少有一个点在集合S中。 例如,如下图所示的无向图G(报告中算法分析过程中一直使用下面的图G) (1)如果选择包含点1,2,6这3个点的集合S不能满足条件,因为边(3,7)两个端点都不在S中。 (2)如果选择包含点1,2,6,7这4个点的集合S虽然满足条件,但是它使用了4个点,其实可以使用更少的点,如下面(3)所示 (3)如果选择包含点1,3,5这3个点的集合S便满足条件,使得G中的每条边都至少有一个点在集合S中。 2.解题思路我的解题思路基于分支定界和贪心两个策略,用一个优先队列维护当前可行的节点,每个节点维护着该节点情况下还可以选择的顶点数目k、需要覆盖的剩余边数e、顶点的状态state、顶点的边数edge等信息,这些节点的排序遵循下面的贪心策略,节点的扩展遵循下面的分支定界策略。总体思路是: ①将原图数据构造成一个解空间树的节点,利用定界策略判断是否有解,如果无解直接退出,如果有可能有解则插入到优先队列中; ②若优先队列不为空,那么便从优先队列中取出第一个可行的节点,进入步骤③,如果优先队列为空则退出; ③判断当前节点是否满足解的条件,如果满足便输出解退出,如果不满足便进入步骤④; ④检查当前节点是否可以扩展,不能扩展的话便进入②继续循环,如果能扩展的话则扩展,然后验证扩展到左右节点是否有解,将有解的扩展节点插入到优先队列中,然后进入②继续循环。 下面分别介绍下分支定界和贪心这两个策略: (1)分支定界策略 首先,界的选择。在一个确定的无向图G中,每个顶点的边即确定了,那么对于该无向图中k个顶点能够覆盖的最多的边数e也就可以确定了!只要对顶点按照边的数目降序排列,然后选择前k个顶点,将它们的边数相加即能得到一个边数上界!因为这k个顶点相互之间可能有边存在也可能没有,所以这是个上界,而且有可能达到。以图G为例,各个顶点的边数统计,并采用降序排列的结果如下: 假设取k=3个点,那么有Up(e)=(3+3+2)=8 > 7 条边(7为图G的总边数),也就是说,如果从图G中取3个点,要覆盖8条边是有可能的。但是,如果取k=2个点,那么有Up(e)=(3+3)=6 < 7 条边,说明从图G中取2个点,是不可能覆盖G中的全部7条边的!基于这个上界,可以在分支树中扩展出来的节点进行验证,已知它还可以选择的顶点数目以及还需要覆盖的边的条数,加上顶点的状态(下面会分析说明)即可判断当前节点是否存在解!如果不存在即可进行剪枝了。 其次,顶点的状态。该策略中顶点有三种状态,分别为已经选择了的状态S1,不选择的状态S2,可以选择的状态S3。其中,不选择的状态S2对应解空间树中的右节点,不选择该节点,然后设置该节点为不选择状态S2。这点很重要,因为有了这个状态,可以使得上界的判断更为精确,因为只能从剩余顶点集中选择那些状态S3的顶点,状态S1和S2都不行,那么上界便会更小,也就更加精确,从而利于剪枝! (2)贪心策略 贪心的策略是指可行的结点都是按照还需要覆盖的剩余边数的降序排列,即,每次选择的节点都是可行节点中还需要覆盖的边数最小的那个节点,因为它最接近结果了。 (3)例子分析 以图G为例,此时e=7(要覆盖的边数),取k=3,图G用邻接矩阵保存为全局数据,计算每个顶点的边数,然后降序排列。 步骤①判断是否可能有解,Up(e)=3+3+2=8>7,可能有解,那么将图G构造成一个解空间树的节点,它包含了还能选择的点数k=3,还需要覆盖的边数e=7,每个顶点的边数以及按边数大小的降序排列(上表),每个顶点的状态(初始时都是可选择的状态S3)。然后,将该节点插入到优先队列中,该优先队列是用最小堆实现的,按照前面的贪心策略对队列中的节点进行降序排列。 步骤②取出了优先队列中的根节点,很显然,还需要覆盖的边数为7,不为0,所以还不满足条件。接下来要检查是否能够进行扩展,从顶点集合中选择状态为可以选择的顶点中边数最多的点,该点存在为顶点2,接着进行扩展,扩展左节点时将还能选择的点数k-1=2,然后计算选择了该点之后删除了几条未覆盖的边,得到还需要覆盖的边数e=4,然后更新所有其他顶点的边数,并重新排序,最后将顶点2的状态设置为已经选择了;扩展右节点时,只要将顶点2的状态设置为不能选择,还能选择的点数k(=3),还需要覆盖的边数e(=7)保持不变。扩展完了之后,同样判断左右节点是否可能有解,如果有解,将该节点插入到优先队列中。这里左右节点都有解,那么将左右节点都插入到优先队列中,因为左节点还需要覆盖的边数e=4小于右节点的e=7,所以根据贪心策略,左节点在右节点的前面。上面两个步骤的图示如下,其中标明了顶点状态颜色。 算法然后继续进入步骤②,此时取出的是节点是刚才插入的左节点,很显然,还需要覆盖的边数为4,不为0,所以还不满足条件。接下来要检查是否能够进行扩展,从顶点集合中选择状态为可以选择的顶点中边数最多的点,该点存在为顶点3,接着进行扩展,扩展左节点时将还能选择的点数k-1=1,然后计算选择了该点之后删除了几条未覆盖的边,得到还需要覆盖的边数e=2,然后更新所有其他顶点的边数,并重新排序,最后将顶点3的状态设置为已经选择了;扩展右节点时,只要将顶点3的状态设置为不能选择,还能选择的点数k(=3),还需要覆盖的边数e(=7)保持不变。扩展完了之后,同样判断左右节点是否可能有解,如果有解,将该节点插入到优先队列中。这里左右节点都不可能有解,那么直接进入步骤②继续循环。上面这一步的图示如下: 算法按照上面的方式不断进行,最后满足条件的分支的过程是: ①不选择顶点2;②选择顶点3;③选择顶点1;④选择顶点5。 最后得到的满足条件的解是选择顶点1,3,5。 (4)复杂度分析 该算法优先队列使用的是最小堆实现的(O(nlgn)),对顶点按照边排序使用的是快速排序算法(O(nlgn)),解空间树的深度最多为顶点数目n,每层都要进行分支定界,所以每层的时间复杂度为O(nlgn),所以算法总的时间复杂度为O(n^2 lgn)。但是,为了实现分支定界,每个节点保存的信息量较多,空间复杂度较大。(有木有分析错了,我不太会分析复杂度) 青橙OJ系统的结果为:时间 156ms 空间 1.0MB 本人对指针领悟能力有限,C++也是一知半解,OJ只能用C或者C++,所以下面的C++代码效率不高,仅供参考,:-)
|