演算法題系列:頂點覆蓋問題

TP_funny發表於2015-05-22

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++程式碼效率不高,僅供參考,:-)
#include <iostream>
#include <vector>
using namespace std;
#define MAX_NODE 101
#define INDEBUG 0
int8_t graph[MAX_NODE][MAX_NODE];//int -> int8_t
//int edges[MAX_NODE];//0 is redudent
//int nodes[MAX_NODE];//the order of node
int t,m,n,k,a,b;
class VCNode {//Vertex Cover Node
public:
    int p;//points can be used
    int e;//edges to cover!!
    int index[MAX_NODE];//the index of each node in array [node], index[k]=i!!
    int edge[MAX_NODE];//MAX_NODE the edge number of each node, edge[i]=j!!
    int node[MAX_NODE];//the order of the node
    int state[MAX_NODE];//the state of each node ** 0 can be used / 1 used / -1 can not be used
//    int graph[MAX_NODE][MAX_NODE];//the graph on the node//no need,just use the global graph
    // node k is in index[k]=i position in array [node]
    // node i has number of edge[i]=j edges
};
class Minheap {//Min Heap
public:
    vector<VCNode> nodes;

    void insert(VCNode node);
    VCNode popmin();
//  void print();
};
void Minheap::insert(VCNode node) {
    nodes.push_back(node);
    //  cout << "size is " << nodes.size() << endl;//
    int curpos = (int)nodes.size() - 1; // current position
    int parent = (curpos - 1) / 2; //parent position
    while (curpos != parent && parent >= 0) { //parent is still in heap
        if (nodes[parent].e > nodes[curpos].e) { //swap parent and child
            VCNode temp = nodes[parent];
            nodes[parent] = nodes[curpos];
            nodes[curpos] = temp;
        } else {
            break; //no longer level up!!!
        }
        curpos = parent; //when curpos=parent=0, exit!!!
        parent = (curpos - 1) / 2; //relocate the parent position
    }
}
VCNode Minheap::popmin() {
    VCNode node;
    if (nodes.size() > 0) { //have nodes left
        node = nodes[0]; //get the first element
        nodes.erase(nodes.begin()); //remove the first element
        if (nodes.size() > 0) { //at least have one element more
            VCNode last = nodes[nodes.size() - 1]; //get the last element
            nodes.pop_back(); //pop the last element
            nodes.insert(nodes.begin(), last); //put it in the first place
            int csize = (int)nodes.size(); //current size
            int curpos = 0; //current position

            // rebuild the minheap
            while (curpos < (csize / 2)) { //reach to the last parent node!!
                int left = 2 * curpos + 1; //left child
                int right = 2 * curpos + 2; //right child
                int min = left; //min store the min child
                if (right < csize) { //have left and right childs
                    if (nodes[right].e < nodes[left].e) {
                        min = right;
                    }
                }
                if (min < csize) { //min child exist!!
                    if (nodes[min].e < nodes[curpos].e) { //need to swap current position with child
                        VCNode temp = nodes[min];
                        nodes[min] = nodes[curpos];
                        nodes[curpos] = temp;
                    }else { //min child no exits!! exit!!
                        break; //can break now!!
                    }
                }
                curpos = min;
            }
        }
    }
    return node;
}
//void Minheap::print() {
//  cout << "print heap" << endl;
//  for (int i = 0; i < (int)nodes.size(); i++) {
//      cout << "edge: " << nodes[i].e << " node: " << nodes[i].p << endl;
//  }
//  cout << "heap end" << endl;
//}
// print array
void printArray(int a[], int start, int end){
    if (INDEBUG) {
        cout << "print array form " << start << " to " << end << endl;
        for (int i=start; i<=end; i++) {
            cout << a[i] << " ";
        }
        cout << endl << "print array end" << endl;
    }
}
// print the graph
void printGraph(int graph[][MAX_NODE]){
    if (INDEBUG) {
        for(int i=1;i<=n;i++){//0 no need
            for(int j=1;j<=n;j++){
                cout << graph[i][j] << " ";
            }
            cout << endl;
        }
    }
}
// partition function for quick sort
int partition2(int a[], int low, int high, int b[]){
    int key = a[high];
    int i=low-1;
    for (int j=low; j<high; j++) {
        if (a[j]>=key) {
            i++;
            swap(a[i], a[j]);
            swap(b[i], b[j]);
        }
    }
    swap(a[high], a[i+1]);
    swap(b[high], b[i+1]);
    return i+1;
}
// quick sort
void quicksort2(int a[], int low, int high, int b[]) {
    if (low < high) {
        int p = partition2(a,low,high, b);
        quicksort2(a, low, p-1, b);
        quicksort2(a, p+1, high, b);
    }
}
// sum of the first k elements with state==0!!!
int sumofkmax(int edges[], int p, int nodes[], int state[]){
    quicksort2(edges, 1, n, nodes);
    int sum=0,count=0;
    // edges[i] corresponse to nodes[i], its state is state[nodes[i]]
    for(int i=1;i<=n;i++){//attention to i range!!
        if (state[nodes[i]]==0) {
            sum+=edges[i];
            count++;
            if (count == p) {//enough!
                break;
            }
        }
    }
    return sum;
}
// verify the current node can be achievable
bool verify(int edges[], int p, int e, int nodes[], int state[]){
    //caculate the sum of the first p max elements in array edges!!
    int sum = sumofkmax(edges, p, nodes, state);
    // edge of nodes[i] is edges[i]!!!
    if(sum >= e){// may be this can be achieved
        return true;
    }
    return false;
}
// build the index of node in array [index]
void buildIndex(int node[],int index[]){
    for (int i=1; i<=n; i++) {
        index[node[i]] = i;
    }
}
// get the next node: state==0 && order first!!!
int nextNode(int state[], int nodes[]){
    for (int i=1; i<=n; i++) {
        if (state[nodes[i]]==0) {
            return nodes[i];
        }
    }
    return -1;
}
// generate the left child
VCNode genLeft(VCNode curnode, int label){
    VCNode left;//choose node label!
    left.p = curnode.p - 1;//remove one node
    left.e = curnode.e;
    for (int i=0; i<=n; i++) {//first copy all infos
        left.index[i]=curnode.index[i];
        left.state[i]=curnode.state[i];//init node state
        left.edge[i]=curnode.edge[i];//copy edge info
        left.node[i]=curnode.node[i];//copy node info
//        for (int j=0; j<=n; j++) {
//            left.graph[i][j] = curnode.graph[i][j];
//        }
    }
    // following code will not use curnode anymore!!

    ///
    int sum=0;//removed edge
    for (int j=1; j<=n; j++) {
        //new
        if (label < j && left.state[j]!=1 && graph[label][j]==1 ) {//row!
            sum++;
//            left.graph[label][j]=0;
            left.edge[left.index[j]]--;//how to cut it down
        }else if(label > j && left.state[j]!=1 && graph[j][label]==1 ){ // col
            sum++;
//            left.graph[j][label]=0;
            left.edge[left.index[j]]--;//how to cut it down
        }
    }
    ///

    left.state[label] = 1;//use label directly!
    left.edge[left.index[label]] = 0;//only use index!!
//    cout << "remove edge sum is " << sum << endl;
    quicksort2(left.edge, 1, n, left.node);
    left.e = left.e - sum;//remove some edges
    buildIndex(left.node, left.index);

    if (INDEBUG) {
        cout << "======== " << label << " gen left begin===========" << endl;
        cout << "edge is " << left.e << " node is " << left.p << endl;
        cout << "array edge:" << endl;
        printArray(left.edge,1,n);
        cout << "array node:" << endl;
        printArray(left.node, 1, n);
        cout << "array index:" << endl;
        printArray(left.index, 1, n);
        cout << "array state:" << endl;
        printArray(left.state, 1, n);
//        printGraph(left.graph);
        cout << "======== " << label << " gen left end===========" << endl;
    }

    return left;
}
// generate the right child
VCNode genRight(VCNode curnode, int label){
    VCNode right;//choose node label!
    right.p = curnode.p;//remain
    right.e = curnode.e;
    for (int i=0; i<=n; i++) {//first copy all infos
        right.index[i]=curnode.index[i];
        right.state[i]=curnode.state[i];//init node state
        right.edge[i]=curnode.edge[i];//copy edge info
        right.node[i]=curnode.node[i];//copy node info
//        for (int j=0; j<=n; j++) {
//            right.graph[i][j] = curnode.graph[i][j];
//        }
    }
    // following code will not use curnode anymore!!
    right.state[label] = -1;//use label directly!

    if (INDEBUG) {
        cout << "======== " << label << " gen right begin===========" << endl;
        cout << "edge is " << right.e << " node is " << right.p << endl;
//        cout << "array edge:" << endl;
//        printArray(right.edge,1,n);
//        cout << "array node:" << endl;
//        printArray(right.node, 1, n);
//        cout << "array index:" << endl;
//        printArray(right.index, 1, n);
//        cout << "array state:" << endl;
//        printArray(right.state, 1, n);
//        printGraph(right.graph);
        cout << "======== " << label << " gen right end===========" << endl;
    }

    return right;
}
// greedy find a way to solve VCP
void greedyFind(int edges[], int nodes[]/*, int graph[][MAX_NODE]*/){
    VCNode node;
    node.e = m;
    node.p = k;

    for (int i=0; i<=n; i++) {
        node.index[i]=0;
        node.state[i]=0;//init node state
        node.edge[i]=edges[i];//copy edge info
        node.node[i]=nodes[i];//copy node info
//        for (int j=0; j<=n; j++) {
//            node.graph[i][j] = graph[i][j];
//        }
    }
    buildIndex(node.node, node.index);

    Minheap minheap;
    minheap.insert(node);

    while (minheap.nodes.size() > 0) {
        // get the heap top node to extend
        VCNode curnode = minheap.popmin();

//        if (INDEBUG) {
//            cout << "...current graph..." << endl;
//            printGraph(curnode.graph);
//        }

        // validate the current node
        if (curnode.e == 0) {
            int points = k - curnode.e;
            cout << points << endl;
            int count = 1;
            for (int i=1; i<=n; i++) {
                if (curnode.state[i]==1) {
                    if(count == points){
                        cout << i;
                    }else{
                        cout << i << " ";
                    }
                    count++;
                }
            }
            cout << endl;
            return;
        }

        // generate child nodes
        int label = nextNode(curnode.state, curnode.node);//the label of the node
        if (label != -1) {
            // node i is in index[k] position in array [node]
            // node i has number of edge[i] edges
            VCNode left = genLeft(curnode, label);
            VCNode right = genRight(curnode, label);
            if (verify(left.edge, left.p, left.e, left.node, left.state)) {
//                cout << "insert " << label << " left" << endl;
                minheap.insert(left);
            }
            if (verify(right.edge, right.p, right.e, right.node, right.state)) {
//                cout << "insert " << label << " right" << endl;
                minheap.insert(right);
            }
        }

    }

    // if not find, then return -1
    cout << -1 << endl;

}
int main() {
//    freopen("/Volumes/hujiawei/Users/hujiawei/workspace/appleworkspace/algorithmworks/Exp1-2/Exp1-2/in3.txt", "rt", stdin);//
    cin >> t;
    while(t-->0){
        cin >> n >> m >> k;
//        int graph[n+1][MAX_NODE];
        for (int i=0; i<= n; i++) {
            for (int j=0; j<= n; j++) {
                graph[i][j]=0;
            }
        }
        int edges[n+1], nodes[n+1], state[n+1];
        for (int i=0; i<= n; i++) {
            edges[i]=0;
            state[i]=0;
            nodes[i]=i;
        }
        int temp = m;
        while(temp-->0){
            cin >> a >> b;
            graph[min(a, b)][max(a,b)]=1;
//          graph[a][b]=1;
//          graph[b][a]=1;//just save half a<=b
            edges[a]++;
            edges[b]++;
        }
        bool flag = verify(edges, k, m, nodes, state);

        if (!flag) {//must not be achieved!!!
            cout << -1 << endl;
        }else{
            greedyFind(edges,nodes/*,graph*/);
        }
    }

    return 0;
}
來自:碼農網
相關閱讀
評論(1)

相關文章