頂點覆蓋問題可以用幾種不同的演算法來實現,本篇文章使用的是分支限界法來實現,或許以後會介紹其他的實現演算法,嘿嘿。
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 |
#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; } |
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!