演算法題:頂點覆蓋問題

五道口宅男瀟澗發表於2015-05-21

頂點覆蓋問題可以用幾種不同的演算法來實現,本篇文章使用的是分支限界法來實現,或許以後會介紹其他的實現演算法,嘿嘿。

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++程式碼效率不高,僅供參考,:-)

 

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

演算法題:頂點覆蓋問題

相關文章