网上关于最大团问题的回溯解法,大多为递归回溯,近日老师布置一个作业,采用迭代回溯的框架解决最大团问题(MCP),且要求多组解,有一定难度。
文章目录
关于最大团
简单来说,团就是一个无向图 G = < V , E > G=<V,E> G=<V,E>的一个子集,一个团内的顶点都与团内其他任意顶点有连接,而最大团其实就是顶点数目最多的团。
回溯基本思想
显然,其解空间树为子集树,因此我们可以使用子集选取的思想,设当前拓展节点 Z Z Z位于解空间树的第 i i i层,在进入其左子树前,必须确认从顶点 i i i到已选入的顶点集中每一个顶点都有边相连;在进入右子树前,必须确认还有足够多的可选择顶点使得算法有可能在右子树中找到更大的团。
由此我们得到回溯时的约束函数与限界函数:
约束函数(constraint()
):准备深入左子树时,判断当前节点是否可以加入团顶点集合;
限界函数(bound()
):准备深入右子树时,判断是否仍有可能产生最优解。
在具体实现时,我们使用临界矩阵表示图
G
G
G,用整型数组x[]
来表示是否选择进入最大团。
迭代基本框架V1.0
约束函数constraint()
bool constraint(int k) {
for(int j=1; j<k; j++) {
if(x[j] && a[j][k]==0) {
return false;
}
}
return true;
}
对于待加入节点
k
k
k,如果存在某个已经在当前最大团集合的节点与其没有连接,那么就无法构成团,即返回false
,否则返回true
。
限界函数bound()
bool bound(int k) {
if(cn+n-k+1 > bestn) {
return true;
}
return false;
}
对于带考虑节点
k
k
k,如果最大团集合内当前节点数cn
加上剩下未考虑的节点数n-k+1
的值严格优于当前最大团集合的节点最优值,则返回true
,否则返回false
。
迭代函数MCP()
void MCP() {
int k=1;
while(1) {
while(k<=n && constraint(k)) { //顶点值合法且满足约束函数,选中,并深入左子树
cn++; //团内点的计数值++
x[k++] = 1; //选入
}
if(k>=n) { //对于图的一次遍历已经完成,记录结果
for(int i=1;i<=n;i++)bextx[i] = x[i]; //拷贝解向量
bestn = cn; //更新最优值
} else { //否则,表示当前顶点不能被选入,需要考虑其是否满足限界函数
x[k++]=0;
}
//如果满足限界函数,继续往后进入constraint的判断,但未选当前节点,表明是深入右子树
while(!bound(k)) { //如果不满足,则需要回溯
k--;
while(k && !x[k]) { //回溯到上一个有效的选择
k--;
}
if(k==0)return; //如果是0,则退出
cn--; //将其从最大团集合中取出
x[k++]=0; //否则,深入其右子树
}
}
}
这种迭代可以得出最大团的一组解,为什么说是一组解呢?我们可以看到在限界函数的定义种,当前团内节点数加上剩下的节点数与团的最优值相等时,是被剪掉了的,换句话说,不会去考虑相等的情况,因此只会有一组解。
试图得到多组解
bool bound(int k) {
if(cn+n-k+1 >= bestn) { //添加等于的情况
return true;
}
return false;
}
于是,我们试着在限界函数中添加等号,但如果是此种迭代框架,并不能达到输出多组解的目的,程序会产生死循环,为什么呢?我们以一个例子来说明:
这个无向图内有三个最大团,我们根据上述程序列出以下搜索时的数据表格:
k | 是否添加 | cn | x[k] | k | bound |
---|---|---|---|---|---|
1 | 是 | 1 | 1 | 2 | \ |
2 | 是 | 2 | 1 | 3 | \ |
3 | 否 | 2 | 0 | 4 | cn+n-k+1=2+5-4+1=4>bestn=0,故不回溯,深入右子树 |
4 | 否 | 2 | 0 | 5 | cn+n-k+1=2+5-5+1=3>0,故不回溯,深入右子树 |
5 | 是 | 3 | 1 | 6 | 遍历完成,记录最优值{1,2,5},bestn=3 |
6 | \ | 3 | \ | \ | 掉入判断bound,cn+n-k+1=3+5-6+1=3=bestn,故不回溯,深入右子树 |
6 | \ | 3 | \ | \ | 直接略过约束函数的判断,调入记录最优值的语句块,重复记录,导致死循环 |
如上图表格所示,其加入等号之后,会导致一直记录最优解,而造成死循环。故要想输出多组解不能直接更改限界函数。
上述代码其实是《计算机算法设计与分析习题解答》(第2版)王晓东著内的源码,大家有兴趣可以翻阅。
迭代基本框架V2.0
(得多组解)
我们需要解决记录解之后出现死循环的问题就需要在记录解之后,判断bound()
之前,修改掉其k
值,从而避免刚刚记录bestn
就卡住了;一种可行的想法是:在记录了解之后,先提前找到最后一个加入团的顶点,将其回溯,即将其从团集合中剔除,然后才进入bound()
的判断,这样既可以达到预期的效果。
迭代函数MCP()
主要变化在:if(k>n)
分支的处理上
void MCP() {
int k=1;
while(1) {
while(k<=n && constraint(k)) {
cn++;
x[k++]=1;
}
if(k>=n) { //记录解
if(cn>bestn) bestx.clear(); //一组更大的解,清空解向量集合
if(cn>=bestn){ //相等顶点数的解(更大的解也会落入此)
vector<int> chose;
for(int i=1;i<=n;i++){ //得到哪些点被选入团
if(x[i])chose.push_back(i);
}
bestx.push_back(chose); //加入到解向量
bestn = cn; //更新最优值
}
k=n; //在判断bound前,提前回溯
while(k && !x[k]) { //找到最后一个加入的节点
k--;
}
cn--; //移出团
x[k++]=0; //标记不选
} else {;
x[k++]=0;
}
while(!bound(k)) {
k--;
while(k && !x[k]) {
k--;
}
if(k==0)return;
cn--;
x[k++]=0;
}
}
}
输出结果
如上图,可以输出多组解。根据输出的调试语句可以看到其回溯的顺序与我们预想的一致。
完整代码如下
#include <iostream>
#include <vector>
using namespace std;
#define MAX 1000
int n,m;
int a[MAX+1][MAX+1];
int cn=0,bestn=0;
int x[MAX+1];//bestx[MAX+1];
vector< vector<int> > bestx;
bool constraint(int k) {
for(int j=1; j<k; j++) {
if(x[j] && a[j][k]==0) {
return false;
}
}
return true;
}
bool bound(int k) {
if(cn+n-k+1 >= bestn) {
return true;
}
return false;
}
void output() {
for(int i=0;i<bestx.size();i++){
for(int j=0;j<bestx[i].size();j++){
cout<<bestx[i][j]<<" ";
}
cout<<endl;
}
}
void MCP() {
int k=1;
while(1) {
while(k<=n && constraint(k)) {
cn++;
//cout<<"第"<<k<<"个满足约束条件cn="<<cn<<endl;
x[k++]=1;
}
if(k>=n) {
if(cn>bestn) bestx.clear();
if(cn>=bestn){
vector<int> chose;
for(int i=1;i<=n;i++){
if(x[i])chose.push_back(i);
}
cout<<endl;
bestx.push_back(chose);
bestn = cn;
}
k=n;
while(k && !x[k]) {
k--;
}
cn--;
x[k++]=0;
} else {
//cout<<"第"<<k<<"个不满足约束条件"<<endl;
x[k++]=0;
}
while(!bound(k)) {
//cout<<"第"<<k<<"个节点不满足限界cn="<<cn<<endl;
k--;
while(k && !x[k]) {
k--;
}
if(k==0)return;
cn--;
//cout<<"因为不满足约束也不满足限界而回溯至第"<<k<<"个节点cn="<<cn<<endl;
x[k++]=0;
}
}
}
int main() {
cout<<"请输入该图的顶点数V与边数E:"<<endl;
cin>>n>>m;
int u,v;
cout<<"请依次输入边的顶点u和v:"<<endl;
for(int i=1; i<=m; i++) {
cin>>u>>v;
a[u][v]=1;
a[v][u]=1;
}
MCP();
output();
}
注释掉的为调试信息。
时间复杂度
有 n n n个顶点需要考虑,每个节点都都有深入左右子树两种选择,而解空间树高为 n n n,故至多有 2 n 2^n 2n种可能情况,因此 T ( n ) = n ∗ 2 n T(n)=n*2^n T(n)=n∗2n,故时间复杂度为: O ( n 2 n ) O(n2^n) O(n2n)。