最小支配集(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中所有顶点独立集中顶点个数最多的独立集为最大独立集。
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;
}
仍以最小支配集为例
基本算法:
由于这是在树上求最值的问题,显然可以用树形动态规划,只是状态的设计比较复杂。为了保证动态规划的正确性,对于每个点设计了三种状态,这三种状态的意义如下:
①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)。