这是一个很典型的NP问题.很长一段时间一直在想如何解决它,直到那天看了一位前人推荐的文档,得到一些启发才顺利的解决了这个困扰我多天的问题.
典型描述:给定一个图G,要求G的最大团(团是指G的一个完全子图,该子图不包含在任何其他的完全子图当中。最大团指其中包含顶点最多的团).
该命题可以有很多变种,例如1002,1654的放机器人,实质上都是求最大团的问题,当然,由于问题的特殊性,他们或许还可以用其他更高效的算法来解.毕竟,问题抽象,解法一般后其实现难度和复杂度也会增大.
解决该问题的一般算法该是搜索.设其包含顶点a1,a2,a3,a4,a5·····an。从a1开始搜索,依次搜索所有与其相邻的点······典型的指数时间搜索。
那天看到一篇文章,专门论述了这个问题。它不是采用我们惯用的从a1开始搜索的方法,而是反了过来,从an开始走,当然,走的时候还是只向前走(a5开始,就只搜a6,a7······),这样做有什么好处呢?实际上类似于动态规划,这样我们每做的一次搜索都可以为后面的搜索所用,而我们先前的搜索方法则基本上每一次搜索都会从头开始去寻找最有解。并且,我们注意到这末一个结论:如果max【i】表示搜索a1得到的解,则max【i-1】=max【i】+1或者max【i】。这个结论是很明显的。如果ai~an的点可能形成的最大完全图含Max【i】个接点,则a(i-1)~an能形成的完全子图最多比前一个多1个顶点。这些给我们的搜索提供了一个很强的约束条件。
这里提供ZJU1492的解答源代码加以具体说明(TLE 2次 ,AC一次 3.09s):
#include<stdio.h>
#include<string.h>
int joint[50][50];
int Size;
int MAX;
int DP[50];
bool find;
int reachEnd(int start,int sets[])//返回-1表示邻接顶点集已经为空集,否则返回第一个公共顶点。
{
int lp;
for(lp=start ; lp<Size ; lp++)
if(sets[lp])
return lp;
return -1;
}
//Visit[]表示邻接数组,start表示搜索起点,切记只可向大的方向搜。Depth表示搜索深度。可以看到我们在函数中又定义了2
个和Visit【】一样内容的数组,这主要考虑到数组传递的指针,直接用Visit【】会直接改变joint邻接数组的内容,搜索之后状态无法还原。而之所以是两个,因为SET要保存信息以便下次回溯。
Sets则是用来进行回溯时传递参数。
void DFS(int Visit[], int start ,int depth )
{
int loop;
int first;
int sets[50],SET[50];
memcpy(sets,Visit,Size*4);
memcpy(SET,Visit,Size*4);
if((first=reachEnd(start,sets))==-1)
{
if(depth > MAX){
MAX=depth;
find=true;
}
return ;
}
while( first != -1){
if(depth+Size-start <= MAX)//不可能找到最有解
return ;
if(depth+DP[first] <=MAX)//不可能找到最有解
return;
sets[first]=0;
SET[first]=0;//从邻接顶点集中清除first顶点。
for(loop=first+1;loop<Size;loop++) //合并邻接顶点集
if(SET[loop]==1 && joint[first][loop]==1)
sets[loop]=1;
else
sets[loop]=0;
DFS(sets,first,depth+1);
if(find)
return ;
first=reachEnd(first,SET);//更新接点
}
}
int main()
{
int loop,lp;
int Visit[50];
while(scanf("%d",&Size)!=EOF && Size!=0)
{
for(loop=0;loop<Size;loop++)
for(lp=0;lp<Size;lp++)
scanf("%d",joint[loop]+lp);
MAX=0;
for(loop=Size-1 ; loop>=0 ; loop--){
find=false;
memcpy(Visit,joint[loop],Size*4);
DFS(Visit,loop,1);
DP[loop]=MAX;
}
printf("%d/n",DP[0]);
}
return 0;
}
图我们仍然用邻接数组表示。最大的不同是DFS部分,可以看到,我们没有用普通的DFS一次就完成整个搜索的策略,而是对an,a(n-1),······a2,a1分开调用DFS,这样方便我们记录每次搜索的结果。
看到DFS部分,reachEnd函数用来判定公共顶点集是否为空集,是空集,表明已经达到解状态树的叶接点,可以记录结果了。Find表示此次搜索已经得到一个优化解,可以返回,结束这次的搜索了。否则,继续搜索。DFS部分的解释参见程序部分。
到这里,这道题就大部分解决了。我们注意到这种算法的要点:
从小到大,小处的结论用来给更大范围的搜索提供强剪枝条件。这和我们递归的至顶而下的做法可谓背道而驰。这种算法将递归和小处规划紧密结合,让我们看到算法的精妙之处。
独立集:
独立集是指图的顶点集的一个子集,该子集的导出子图不含边.如果一个独立集不是任何一个独立集的子集, 那么称这个独立集是一个极大独立集.一个图中包含顶点数目最多的独立集称为最大独立集。最大独立集一定是极大独立集,但是极大独立集不一定是最大的独立集。
支配集:
与独立集相对应的就是支配集,支配集也是图顶点集的一个子集,设S 是图G 的一个支配集,则对于图中的任意一个顶点u,要么属于集合s, 要么与s 中的顶点相邻。 在s中除去任何元素后s不再是支配集,则支配集s是极小支配集。称G的所有支配集中顶点个数最 少的支配集为最小支配集,最小支配集中的顶点个数成为支配数。
最小点的覆盖:
最小点的覆盖也是图的顶点集的一个子集,如果我们选中一个点,则称这个点将以他为端点的所有边都覆盖了。将图中所有的边都覆盖所用顶点数最少,这个集合就是最小的点的覆盖。
最大团:
图G的顶点的子集,设D是最大团,则D中任意两点相邻。若u,v是最大团,则u,v有边相连,其补图u,v没有边相连,所以图G的最大团=其补图的最大独立集。
一些性质:
最大独立集+最小覆盖集=V
最大团=补图的最大独立集
最小覆盖集=最大匹配