首先看一下三者的定義:
定義1 對於圖G=(V,E)來說,最小支配集指的是從V中取盡量少的點組成一個集合,使得對於V中剩余的點都與取出來的點有邊相連。也就是說,設V‘是圖G的一個支配集,則對於圖中的任意一個頂點u,要么屬於集合V’,要么與V‘中的頂點相鄰。在V’中出去任何元素后V‘不再是支配集,則支配集是極小支配集。稱G的所有支配集中頂點個數最少的支配集為最小支配集,最小支配集中頂點的個數稱為支配數。
定義2 對於圖G=(V,E)來說,最小點覆蓋指的是從V中取盡量少的點組成一個集合,使得E中所有的邊都與取出來的點相連。也就是說,設V‘是圖G的一個頂點覆蓋,則對於圖中的任意一條邊(u,v),要么u屬於集合V’,要么v屬於集合V‘。在V‘中除去任何元素后V’不在是頂點覆蓋,則V‘是極小頂點覆蓋。稱G的所有頂點覆蓋中頂點個數最少的覆蓋為最小點覆蓋。
定義3 對於圖G=(V,E)來說,最大獨立集指的是從V中取盡量多的點組成一個集合,使得這些點之間沒有邊相連。也就是說,設V’是圖G的一個獨立集,則對於圖中任意一條邊(u,v),u和v不能同時屬於集合V',甚至可以u和v都不屬於集合V‘。在V’中添加任何不屬於V‘元素后V’不再是獨立集,則V‘是極大獨立集。稱G的所有頂點獨立集中頂點個數最多的獨立集為最大獨立集。
對於任意圖G來說,這三個問題不存在多項式時間的解法。不過對於樹來說,卻很容易。目前有兩種解法,一種基於貪心思想,另一種基於樹形動態規划思想。
一:貪心法
基本算法:
以最小支配集為例,對於樹上的最小支配集問題,貪心策略是首先選擇一點為根,按照深度優先遍歷得到遍歷序列,按照所得序列的反向序列的順序進行貪心,對於一個既不屬於支配集也不與支配集中的點相連的點來說,如果他的父節點不屬於支配集,將其父節點加入支配集。
這里注意到貪心的策略中貪心的順序非常重要,按照深度優先遍歷得到遍歷序列的反向進行貪心,可以保證對於每個點來說,當期子樹都被處理過后才輪到該節點的處理,保證了貪心的正確性。
①以1號點深度優先搜索整棵樹,求出每個點在深度優先遍歷序列中的編號和每個點的父節點編號。
②按照深度優先序列的反向序列檢查,如果當前點既不屬於支配集也不與支配集中的點相連,且他的父節點不屬於支配集,將其父節點加入支配集,支配集中點的個數加1.標記當前節點、當前節點的父節點和當前節點的父節點的父節點,因為這些節點要么屬於支配集,要么與支配集中的點相連。
最小點覆蓋於最大獨立集與上面的做法相似。對於最小點覆蓋來說,貪心的策略是,如果當前點和當前點的父節點都不屬於頂點覆蓋集合,則將父節點加入到頂點覆蓋集合,並標記當前節點和其父節點都被覆蓋。對於最大獨立集來說,貪心策略是如果當前點沒有被覆蓋,則將當前節點加入到獨立集,並標記當前節點和其父節點都被覆蓋。
需要注意的是由於默認程序中根節點和其他節點的區別在於根節點的父節點是其自己,所以三種問題對根節點的處理不同。對於最小支配集和最大獨立集,需要檢查根節點是否滿足貪心條件,但是對於最小點覆蓋不可以檢查根節點。
具體實現:
采用鏈式前向星存儲整棵樹。對於DFS(),newpos[i]表示深度優先遍歷序列的第i個點是哪個點,now表示當前深度優先遍歷序列已經有多少個點了。select[]用於深度優先遍歷序列的判重,p[i]表示點i的父節點的編號。對於greedy(),s[i]如果為true,表示第i個點被覆蓋。set[i]表示點i屬於要求的點集。
首先是深度優先遍歷,得到遍歷序列。代碼如下:
int p[maxn]; bool select[maxn]; int newpos[maxn]; int now; int n,m; void DFS(int x) { newpos[now++]=x; int k; for(k=head[x];k!=-1;k=edge[k].next) { if(!select[edge[k].to]) { select[edge[k].to]=true; p[edge[k].to]=x; DFS(edge[k].to); } } }
對於最小支配集,貪心函數如下:
int greedy() { bool s[maxn]; bool set[maxn]={0}; int ans=0; int i; for(i=n-1;i>=0;i--) { int t=newpos[i]; if(!s[t]) { if(!set[p[t]]) { set[p[t]]=true; ans++; } s[t]=true; s[p[t]]=true; s[p[p[t]]]=true; } } return ans; }
對於最小點覆蓋,貪心函數如下:
int greedy() { bool s[maxn]={0}; bool set[maxn]={0}; int ans=0; int i; for(i=n-1;i>=1;i--) { int t=newpos[i]; if(!s[t]&&s[p[t]]) { set[p[t]]=true; ans++; s[t]=true; s[p[t]]=true; } } return ans; }
對於最大獨立集,貪心函數如下:
int greedy() { bool s[maxn]={0}; bool set[maxn]={0}; int ans=0; int i; for(i=n-1;i>=0;i--) { int t=newpos[i]; if(!s[t]) { set[t]=true; ans++; s[t]=true; s[p[t]]=true; } } return ans; }
使用樣例:
int main() { /*讀入圖信息*/ memset(select,0,sizeof(select)); now=0; select[1]=true; p[1]=1; DFS(1); printf("%d\n",greedy()); }
二.樹形動態規划法
仍以最小支配集為例
基本算法:
由於這是在樹上求最值的問題,顯然可以用樹形動態規划,只是狀態的設計比較復雜。為了保證動態規划的正確性,對於每個點設計了三種狀態,這三種狀態的意義如下:
①$dp[i][0]$:表示點i屬於支配集,並且以點i為根的子樹都被覆蓋了的情況下支配集中所包含的的最少點的個數。
②$dp[i][1]$:i不屬於支配集,且以i為根的子樹都被覆蓋,且i被其中不少於1個子節點覆蓋的情況下支配集中所包含最少點的個數。
③$dp[i][2]$:i不屬於支配集,且以i為根的子樹都被覆蓋,且i沒被子節點覆蓋的情況下支配集中所包含最少點的個數。
對於第一種狀態,dp[i][0]等於每個兒子節點的3種狀態(其兒子是否被覆蓋沒有關系)的最小值之和加1,即只要每個以i的兒子為根的子樹都被覆蓋,再加上當前點i,所需要的最少點的個數,方程如下:
$$dp[i][0]=1+\sum{min(dp[u][0],dp[u][1],dp[u][2])(p[u]=i)}$$
對於第二種狀態,如果點i沒有子節點,那么$dp[i][1]=INF;$否則,需要保證它的每個以i的兒子為根的子樹都被覆蓋,那么要取每個兒子節點的前兩種狀態的最小值之和,因為此時i點不屬於支配集,不能支配其子節點,所以子節點必須已經被支配,與子節點的第三種狀態無關。如果當前所選的狀態中,每個兒子都沒有被選擇進入支配集,即在每個兒子的前兩種狀態中,第一種狀態都不是所需點最少的,那么為了滿足第二種狀態的定義,需要重新選擇點i的一個兒子的狀態為第一種狀態,這時取花費最少的一個點,即取min(dp[u][0]-dp[u][1])的兒子節點u,強制取其第一種狀態,其他兒子節點都取第二種狀態,轉移方程為:
$if$( $i$沒有子節點) $dp[i][1]=INF;$
$else~dp[i][1]=Σmin(dp[u][0],dp[u][1])+inc$
其中對於$inc$有:
$if$(上面式子中的$\sum{min(dp[u][0],dp[u][1])}$中包含某個$dp[u][0]$)$inc=0;$
$else~inc=min(dp[u][0]-dp[u][1])$
對於第三種狀態,i不屬於支配集,且以i為根的子樹都被覆蓋,又i沒被子節點覆蓋,那么說明點i和點i的兒子節點都不屬於支配集,則點i的第三種狀態只與其兒子的第二種狀態有關,方程為
$dp[i][2]=\sum{dp[u][1]}$
對於最小的覆蓋問題,為每個點設計了兩種狀態,這兩種狀態的意義如下:
①$dp[i][0]$:表示點i屬於點覆蓋,並且以點i為根的子樹中所連接的邊都被覆蓋的情況下點覆蓋集中所包含最少點的個數。
②$dp[i][1]$:表示點i不屬於點覆蓋,並且以點i為根的子樹中所連接的邊都被覆蓋的情況下點覆蓋集中所包含最少點的個數。
對於第一種狀態dp[i][0],等於每個兒子節點的兩種狀態的最小值之和加1,方程如下:
$$dp[i][0]=1+\sum{min(dp[u][0],dp[u][1])(p[u]=i)}$$
對於第二種狀態dp[i][1],要求所有與i連接的邊都被覆蓋,但是i點不屬於點覆蓋,那么i點所有的子節點都必須屬於點覆蓋,即對於點i的第二種狀態與所有子節點的第一種狀態無關,在數值上等於所有子節點的第一種狀態之和。方程如下:
$dp[i][1]=\sum{dp[u][0]}$
對於最大獨立集問題,為每個節點設立兩種狀態,這兩種狀態的意義如下:
①$dp[i][0]$:表示點i屬於獨立集的情況下,最大獨立集中點的個數。
②$dp[i][1]$:表示點i不屬於獨立集的情況下,最大獨立集中點的個數。
對於第一種狀態$dp[i][0]$,由於點i屬於獨立集,他的子節點都不能屬於獨立集,所以只與第二種狀態有關。方程如下:
$$dp[i][0]=1+\sum{dp[u][1]}$$
對於第二種狀態,點i的子節點可以屬於獨立集,也可以不屬於獨立集,方程如下:
$$dp[i][1]=\sum{max(dp[u][0],dp[u][1])}$$
具體代碼如下:
u表示當前正在處理的節點,p表示u的父節點。
最小支配集:
void DP(int u,int p) { dp[u][2]=0; dp[u][0]=1; bool s=false; int sum=0,inc=INF; int k; for(k=head[u];k!=-1;k=edge[k].next) { int to=edge[k].to; if(to==p)continue; DP(to,u); dp[u][0]+=min(dp[to][0],min(dp[u][1],dp[u][2])); if(dp[to][0]<=dp[to][1]) { sum+=dp[to][0]; s=true; } else { sum+=dp[to][1]; inc=min(inc,dp[to][0]-dp[to][1]); } if(dp[to][1]!=INF&&dp[u][2]!=INF)dp[u][2]+=dp[to][1]; else dp[u][2]=INF; } if(inc==INF&&!s)dp[u][1]=INF; else { dp[u][1]=sum; if(!s)dp[u][1]+=inc; } }
最小點覆蓋:
void DP(int u,int p) { dp[u][0]=1; dp[u][1]=0; int k,to; for(k=head[u];k!=-1;k=edge[k].next) { to=edge[k].to; if(to==p)continue; DP(to,u); dp[u][0]+=min(dp[to][0],dp[to][1]); dp[u][1]+=dp[to][0]; } }
最大獨立集:
void DP(int u,int p) { dp[u][0]=1; dp[u][1]=0; int k,to; for(k=head[u];k!=-1;k=edge[k].next) { to=edge[k].to; if(to==p)continue; DP(to,u); dp[u][0]+=dp[u][1]; dp[u][1]+=max(dp[to][0],dp[to][1]); } }
由於使用的是樹狀動態規划,所以整個算法的時間復雜度是O(n)。
注:本博客轉自此處.
