最小支配集,最小點覆蓋,最大獨立集(貪心/DP)
最小支配集(minimal dominating set):對於圖G=(V,E)來說,設V'是圖G的一個支配集,則對於圖中的任意一個頂點u,要麼屬於集合V',要麼與V'中的頂點相連。
在V'中除去任何元素後V'不再是支配集,則支配集V'是極小支配集。稱G中所有支配集中頂點個數最少的支配集為最小支配集,最小支配集中的頂點個數稱為支配數。
最小點覆蓋(minimum point coverage):對於圖G=(V,E)來說,設V'是圖G的一個頂點覆蓋,則對於圖中任意一條邊(u,v),要麼屬於u要麼屬於集合V'。
在V'中除去任何元素後V'不再是頂點覆蓋,則V'是極小點覆蓋。稱G中所有頂點覆蓋中頂點個數最小的覆蓋為最小點覆蓋。
最大獨立集(minimum independent set):對於圖G=(V,E)來說,設V'是圖G的一個獨立集,則對於圖中任意一條邊(u,v),u和v不能同時屬於集合V',甚至可以u和v都不屬於集合V'。
在V'中新增任何不屬於V'元素後V'不再是獨立集,則V'是極大獨立集。稱G中所有頂點獨立集中頂點個數最多的獨立集為最大獨立集。
1.貪心求解:
int p[maxn]; //父節點編號 bool select[maxn]; //用於深度優先的判重 int newpos[maxn]; //newpos[i]表示深度優先序列的第i個點是哪個點, int now; //表示當前深度優先序列已經有幾個點了 int n,m; void dfs(int x) { newpos[now++]=x; for(int 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); } } } /* 最小支配集:貪心策略是首先選擇一點為根,按照深度優先遍歷得到遍歷序列,按照所得序列的反向序列的順序進行貪心, 對於一個既不屬於支配集也不與支配集中的點相連的點來說,如果它的父節點不屬於支配集,將其父節點加入支配集。 貪心策略中貪心的順序非常重要,按照深度優先遍歷得到遍歷序列的反方向進行貪心,可以保證對於每個點來說, 當其子樹都被處理過後才會輪到該節點的處理,保證了貪心的正確性。 (1).以1號點深度優先搜尋整棵樹,求出每個點在深度優先遍歷序列中的編號和每個點的父節點編號 (2).按照深度優先序列的反向序列檢查,如果當前點既不屬於支配集也不與支配集中的點相連, 且它的父節點不屬於支配集,將其父節點加入支配集,支配集中的點的個數加1.標記當前節點, 當前節點的父節點和當前節點的父節點的父節點,因為這些節點要麼屬於支配集(當前點的父節點), 要麼與支配集中的點相連(當前節點和當前節點的父節點的父節點)。 */ int greedy() { bool s[maxn]={0}; //如果s[i]是true,則表示s[i]被覆蓋 bool set[maxn]={0}; //set[i]表示點i屬於要求的集合 int ans=0; for(int 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; //不可以檢查根節點 for(int 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; for(int 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); return 0; }
2.樹形DP求解
仍以最小支配集為例
基本演算法:
由於這是在樹上求最值的問題,顯然可以用樹形動態規劃,只是狀態的設計比較複雜。為了保證動態規劃的正確性,對於每個點設計了三種狀態,這三種狀態的意義如下:
①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+Σmin(dp[u][0],dp[u][1],dp[u][2])(p[u]=i).
對於第二種狀態,如果點i沒有子節點,那麼dp[i][1]=INF;否則,需要保證它的每個以i的兒子為根的子樹都被覆蓋,那麼要取每個兒子節點的前兩種狀態的最小值之和,因為此時i點不屬於支配集,不能支配其子節點,所以子節點必須已經被支配,與子節點的第三種狀態無關。如果當前所選的狀態中,每個兒子都沒有被選擇進入支配集(有可能出現在求和時每一個v都取的是F[v][1],這也就意味著我們最後求出來的F[u][1]代表的解中u的子節點v都沒有被選擇去覆蓋別的點!,而這與我們對F[u][1]的定義是矛盾的。),即在每個兒子的前兩種狀態中,第一種狀態都不是所需點最少的,那麼為了滿足第二種狀態的定義,需要重新選擇點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(上面式子中的Σ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]=Σdp[u][1]
對於最小的覆蓋問題,為每個點設計了兩種狀態,這兩種狀態的意義如下:
①dp[i][0]:表示點i屬於點覆蓋,並且以點i為根的子樹中所連線的邊都被覆蓋的情況下點覆蓋集中所包含最少點的個數。
②dp[i][1]:表示點i不屬於點覆蓋,並且以點i為根的子樹中所連線的邊都被覆蓋的情況下點覆蓋集中所包含最少點的個數。
對於第一種狀態dp[i][0],等於每個兒子節點的兩種狀態的最小值之和加1,方程如下:
dp[i][0]=1+Σmin(dp[u][0],dp[u][1])(p[u]=i).
對於第二種狀態dp[i][1],要求所有與i連線的邊都被覆蓋,但是i點不屬於點覆蓋,那麼i點所有的子節點都必須屬於點覆蓋,即對於點i的第二種狀態與所有子節點的第一種狀態無關,在數值上等於所有子節點的第一種狀態之和。方程如下:
dp[i][1]=Σdp[u][0]
對於最大獨立集問題,為每個節點設立兩種狀態,這兩種狀態的意義如下:
①dp[i][0]:表示點i屬於獨立集的情況下,最大獨立集中點的個數。
②dp[i][1]:表示點i不屬於獨立集的情況下,最大獨立集中點的個數。
對於第一種狀態dp[i][0],由於點i屬於獨立集,他的子節點都不能屬於獨立集,所以只與第二種狀態有關。方程如下:
dp[i][0]=1+Σdp[u][1]
對於第二種狀態,點i的子節點可以屬於獨立集,也可以不屬於獨立集,方程如下:
dp[i][1]=Σ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]);
}
}
由於求的是每個點分別在幾種狀態下的最優值,所以需要比較dp[root][0],dp[root][1]的值,取較優的一個作為最終答案。
由於使用的是樹狀動態規劃,所以整個演算法的時間複雜度是O(n)。