一个无向图G(V ,E)的vertex cover VC是顶点集V的一个子集,如果边uv∈ E,则顶点u,v至少有一个点属于VC。
可以看出其实寻找vertex cover并不难,因为顶点集V本身就是一个vertex cover。而难的是寻找最小的vertex cover。vertex cover的寻找可以用于街道安装摄像头等情况的计算。如果能够找到最小的vertex cover,就能够找到最节约的安装摄像头的方法。
寻找最小的vertex cover是一个NP问题,因此我们在寻找时并没有特别高效的解法。
在这里介绍三种解法,其中两种无法保证找到最小的vertex cover,而第三种能够保证找到最小的vertex cover。
该方法的思路如下:
(1) 找到度最大的顶点。
(2) 移除该顶点以及与该顶点相连的其他顶点。
(3) 重复(1)、(2)步直至所有顶点都被去除。
可以计算出这种方法的时间。其中|E|是边的数量。而我们可以证明用这种方法求出来的vertex cover的近似比是O(logn)。具体的证明可以参考著名的算法分析书本CLRS的相关证明。
该方法的代码如下:
bool approx_vc_1(int vertices_num,std::vector edge,std::vector &vertex_cover)
{
if(vertices_num == 0)
std::cout << "APPROX-VC-1: " << std::endl;
else if(vertices_num == 1)
std::cout << "APPROX-VC-1: " << "0" << std::endl;
else if(edge.size() == 0)
std::cout << "APPROX-VC-1: " << std::endl;
else
{
int count[vertices_num];
int edge_num = edge.size();
//寻找度数最多的顶点
while(edge_num != 0)
{
for(int i = 0;i < vertices_num;i++)
{
count[i] = 0;
}
for(int i = 0;i < edge.size();i++)
{
if(edge[i] != INF)
count[edge[i]]++;
}
int max = std::max_element(count, count + vertices_num) - count;
vertex_cover.push_back(max);
//去除相邻的顶点
for(int i = 0;i < edge.size();i+=2)
{
if(edge[i] == max || edge[i+1] == max)
{
edge[i] = INF;
edge[i+1] = INF;
edge_num -= 2;
}
}
}
return 1;
}
return 0;
}
该方法的思路如下:
(1)随机从边集E中找到一个条边uv。
(2) 移除u,v以及与u,v顶点相连的其他顶点。
(3) 重复(1)、(2)步直至所有顶点都被去除。
该方法的时间复杂度同样为。但是理论上该方法所需时间是比APPROX-VC-1少。并且可以证明得到用该方法算出来的vertex cover不会大于最小vertex cover的两倍。即近似比为2。
该方法代码如下
bool approx_vc_2(int vertices_num,std::vector edge,std::vector &vertex_cover)
{
if(vertices_num == 0)
std::cout << "APPROX-VC-2: " << std::endl;
else if(vertices_num == 1)
std::cout << "APPROX-VC-2: " << "0" << std::endl;
else if(edge.size() == 0)
std::cout << "APPROX-VC-2: " << std::endl;
else
{
for(int i = 0;i < edge.size();i+=2)
{
if(edge[i] != INF)
{
vertex_cover.push_back(edge[i]);
vertex_cover.push_back(edge[i+1]);
for(int j = i + 2;j < edge.size();j+=2)
{
if(edge[j] == edge[i] || edge[j+1] == edge[i] || edge[j] == edge[i+1] || edge[j+1] == edge[i+1])
{
edge[j] = INF;
edge[j+1] = INF;
}
}
edge[i] = INF;
edge[i+1] = INF;
}
}
return 1;
}
return 0;
}
学习过算法分析的同学都应该知道reduction这个概念。事实上我们可以将计算vertex cover问题reduce to CNF-SAT的形式。即变为解可满足性逻辑命题的问题。
具体的转换过程其实并不复杂,这里就不细说了。有兴趣的上google搜索一下即可。(其实我懒得打。。。。)总之在转换成相关的CNF-SAT之后,我们的任务就变成寻找一个能够满足该式子的assignment。
解SAT的满足的assignment,除了暴力解(也就是循环尝试不同的解)之外,还有一些相对有效的算法。其中一种算法称为DPLL算法。该算法使用unit propagation和pure literal rule这两个东西来求得一些原子式的值。如果还有剩下的无法使用这两个方法求出,那只能使用暴力解法获取值。
其中pure literal rule意思是如果CNF-SAT中有某一个原子式都是正的或者都是负的,那么这个原子式的解就得出来了,并且其涉及的所有公式都可以去除。
如:中。原子式a全都是正的,那么我们就知道a肯定是1,并且b和c的值我们只要考虑就可以了。
至于unit propagation,如果式子: ,那么我们看到有,这就等于我们可以再或上c。
即当我们看到这种形式时,就等同于可以再或c: 。改变之后的式子与改变前的式子解是一样的。但是我们已经知道了c的值是1。
通过上面的描述可以看出DPLL算法写起来是很难的。因此我们一般借助专用的计算SAT的工具。
而minisat-solver就是这样的工具。通过调用API我们可以计算出满足SAT式子的assignment。
当图的顶点数多起来后,最小vertex cover的大小也会上涨。由于这个方法的时间复杂度是,所以当图的定点数多时需要很长的计算时间。如我在计算20个点的图时,计算了15个小时依然没有计算出来。有兴趣的朋友可以去尝试一下。
代码如下:
bool binary_calculate_vertex_cover(int vertices_num,std::vector edge,std::vector &vertex_cover)
{
if(vertices_num == 0)
std::cout << "CNF-SAT-VC: " << std::endl;
else if(vertices_num == 1)
std::cout << "CNF-SAT-VC: " << "0" << std::endl;
else if(edge.size() == 0)
std::cout << "CNF-SAT-VC: " << std::endl;
else
{
int hi = vertices_num;
int lo = 1;
int mid;
//由于我们不知道最小的vertex cover的大小,因此我们要一个一个数字去试。这里我们采用二分法的方式尝试。
while(hi >= lo)
{
mid = (hi+lo)/2;
if(calculate_vertex_cover(vertices_num,edge,mid,vertex_cover) != 1)
{
lo = mid+1;
}
else
{
hi = mid - 1;
}
}
//print_vertex_cover(vertex_cover,"CNF-SAT-VC: ");
return 1;
}
return 0;
}
bool calculate_vertex_cover(int vertices_num,std::vector edge,int k,std::vector &vertex_cover)
{
Solver S;
Var propositions[vertices_num][k];
for(int i = 0;i < vertices_num;i++)
{
for(int j = 0;j < k;j++)
{
propositions[i][j] = S.newVar();
}
}
vec clause;
for(int i = 0;i < k;i++)
{
for(int j = 0;j < vertices_num;j++)
{
clause.push(mkLit(propositions[j][i]));
}
S.addClause(clause);
clause.clear();
}
for(int i = 0;i < vertices_num;i++)
{
for(int j = 0;j < k - 1;j++)
{
for(int h = j + 1;h < k;h++)
{
S.addClause(mkLit(propositions[i][j],true),mkLit(propositions[i][h],true));
}
}
}
for(int i = 0;i < k;i++)
{
for(int j = 0;j < vertices_num - 1;j++)
{
for(int h = j + 1;h < vertices_num;h++)
{
S.addClause(mkLit(propositions[j][i],true),mkLit(propositions[h][i],true));
}
}
}
for(int i = 0;i < edge.size();i+=2)
{
for(int j = 0;j < k;j++)
{
clause.push(mkLit(propositions[edge[i]][j]));
clause.push(mkLit(propositions[edge[i+1]][j]));
}
S.addClause(clause);
clause.clear();
}
if(S.solve())
{
vertex_cover.clear();
for(int i = 0;i < k;i++)
{
for(int j = 0;j < vertices_num;j++)
{
if(S.modelValue(propositions[j][i]) == l_True)
{
vertex_cover.push_back(j);
break;
}
}
}
return 1;
}
return 0;
}