无向图计算vertex cover

1.vertex cover的定义

      一个无向图G(V ,E)的vertex cover VC是顶点集V的一个子集,如果边uv∈ E,则顶点u,v至少有一个点属于VC。

      可以看出其实寻找vertex cover并不难,因为顶点集V本身就是一个vertex cover。而难的是寻找最小的vertex cover。vertex cover的寻找可以用于街道安装摄像头等情况的计算。如果能够找到最小的vertex cover,就能够找到最节约的安装摄像头的方法。

     

2.vertex cover计算

         寻找最小的vertex cover是一个NP问题,因此我们在寻找时并没有特别高效的解法。

         在这里介绍三种解法,其中两种无法保证找到最小的vertex cover,而第三种能够保证找到最小的vertex cover。

 

(1) APPROX-VC-1

         该方法的思路如下:

(1) 找到度最大的顶点。

(2) 移除该顶点以及与该顶点相连的其他顶点。

(3) 重复(1)、(2)步直至所有顶点都被去除。

 

         可以计算出这种方法的时间O(|E|^{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;
}

(2) APPROX-VC-2

         该方法的思路如下:

(1)随机从边集E中找到一个条边uv。

(2) 移除u,v以及与u,v顶点相连的其他顶点。

(3) 重复(1)、(2)步直至所有顶点都被去除。

   

         该方法的时间复杂度同样为O(|E|^{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;
}

 

(3) CNF-SAT

       学习过算法分析的同学都应该知道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\vee b)\wedge (a\vee c)\wedge (b\vee c)中。原子式a全都是正的,那么我们就知道a肯定是1,并且b和c的值我们只要考虑(b\vee c)就可以了。

      至于unit propagation,如果式子: (a\vee b)\wedge (a\vee c)\wedge (b\vee c)\wedge \bar{b},那么我们看到有(b\vee c)\wedge \bar{b},这就等于我们可以再或上c。

即当我们看到(c\vee l)\wedge \bar{l}这种形式时,就等同于可以再或c: (c\vee l)\wedge \bar{l}\wedge c。改变之后的式子与改变前的式子解是一样的。但是我们已经知道了c的值是1。

 

(4)Minisat

            通过上面的描述可以看出DPLL算法写起来是很难的。因此我们一般借助专用的计算SAT的工具。

            而minisat-solver就是这样的工具。通过调用API我们可以计算出满足SAT式子的assignment。  

            当图的顶点数多起来后,最小vertex cover的大小也会上涨。由于这个方法的时间复杂度是O(2^{n}),所以当图的定点数多时需要很长的计算时间。如我在计算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;
}

   

 

你可能感兴趣的:(数据结构)