【算法学习笔记】1:回溯法中子集树与排列树(装载/最大团/n皇后/旅行商)

之前一直分不清哪种递归属于回溯法,当然回溯法也不一定用递归来做,上了算法课有了一点感悟,记录一下。这四道题是算法的作业,在OJ上可以测试通过,感觉解空间这个概念真的是很帮助思考的一个东西。

解空间

解空间就是所有解的可能取值构成的空间, 一个解往往包含了得到这个解的每一步,往往就是对应解空间树中一条从根节点到叶节点的路径。
子集树和排列树都是一种解空间,它们不是真实存在的数据结构,也就是说并不是真的有这样一棵树,只是抽象出的解空间树。

约束条件和限界条件

约束条件Constraint是问题中就限定好的条件,比如在装载问题中装入第i个物体后不能超过背包总容量时才考虑装入它,即搜索左子树的情况。
限界条件Bound是需要自己挖掘的一个界,可能是上界或者下界,当问题中的某个值在走向某棵子树时会超出这个界限时,就可以放弃这棵子树了。
用这两个条件可以对解空间树进行剪枝,这样回溯法才有别于枚举。

子集树

当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树称为子集树。子集树通常有2^n个叶结点(完全二叉树)。
这里写图片描述
回溯法搜索子集树的一般算法:

void backtrack(int i)
{
	if(i>t)
	{
		//找到了一个解,记录一下
		return ;
	}
	if(Constraint1() && Bound1())//左子树剪枝
	{
		x[i]=1;
		//考虑搜索左子树
		backtrack(i+1);
		//回退了!维护好
	}
	if(Constraint0() && Bound0())//右子树剪枝
	{
		x[i]=0;
		//考虑搜索右子树
		backtrack(i+1);
	}
return ;
}

一般来说,子集树是一棵完全二叉树,这是因为子集往往只能取或者不取原集合中的元素,也就是取=1(左子树),不取=0(右子树)。
因为树的根节点只有一个,但从第一个物体开始就要考虑选择还是不选择,所以选择序列数组应该开t+1大小的,然后从1~t层(根节点算0层)即是考虑这个物体选择/不选择的层。
要注意搜索完左子树,往往要维护好这个i节点,因为相当于回退到了上一层,再去考虑是否要搜索右子树。

排列树

当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。排列树通常有n!个叶节点。也就是说n个元素的每一个排列都是解空间中的一个元素。
这里写图片描述
回溯法搜索排列树的一般算法:

void backtrack(int i)
{
	if(i>t)
	{
		//找到了一个解,记录一下
		return ;
	}
	else
		for(int j=i;j<=t;j++)
		{
			swap(x[j],x[i]);
			if(Constraint() && Bound())//剪枝
				backtrack(i+1);
			swap(x[j],x[i]);
		}
return ;
}

对于i层及其后的所有层,都和第i层交换值然后再搜索交换后的下一层,并在回退后交换回来(维护好本层),这样能够搜索完全排列(可以想象一下2个节点的情况,3个节点的情况…)。
因为树的根节点只有一个,但从第一个位置开始就要考虑排这t个物品中的哪一个,所以选择序列数组应该开t+1大小的,然后从1~t层(根节点算0层)即是考虑这个位置放t个物品中的哪一个。

装载问题(子集树)

这里写图片描述

#include<iostream>
using namespace std;
int count=0;//全局计数
int t;//箱子数
int *w;//重量数组
int c1,c2;//两艘船载重量
int cw;//当前载重量
int bestw;//当前找到过的最优成功载重量
int *c;//当前的放入情况
int *b;//当前找到过的最优的放入情况
bool ok=false;

void backtrack(int i)//回溯法:在一棵子集树解空间中深度搜索
{
	if(i>t)//超出子集树的叶节点了
	{
		if(cw>bestw)//仅当这次搜索严格大于当前最优时
		{
			bestw=cw;//才做更新(这样能保证一样的值取按字典序最大者)
			for(int j=1;j<=t;j++)//更新最优选择序列
				b[j]=c[j];
			ok=true;//记录找到过成功的解
		}
		return ;//回退
	}
	//考虑搜索左子树(即放入第i个的情况)
	if(cw+w[i]<=c1)//约束条件剪枝:不能超过c1载重量才可以放入i物品(走左子树)
	{
		c[i]=1;//记录当前第i个装进来了
		cw+=w[i];//把当前载重量加进来
		backtrack(i+1);//然后搜索下一层(左子树)
		cw-=w[i];//搜索完以后,要退回到本层,所以把刚才加上的左子树的重量减掉
		//而c[i]是不需要动的,因为每次向下走一层时总是会按左走还是右走更新c[i]
		//也就是说目前c[i]里的值只是废的而已
	}
	//考虑搜索右子树(即不放入第i个的情况)
	int sumend=0;
	for(int j=i+1;j<=t;j++)
		sumend+=w[j];//sumend里存的是从第i+1个到第t个全部放进来的重量
	if(cw+sumend>bestw)//限界条件剪枝:如果剩下的全放进来都没法更重,就不用看右子树了
	{
		c[i]=0;//记录第i个物体不放入(从第i-1层到第i层走了右子树)
		backtrack(i+1);//然后搜索下一层(右子树)
	}
}

int main()
{
	while(cin>>t)
	{
		w=new int[t+1];//分配空间,从1开始计数
		c=new int[t+1];
		b=new int[t+1];
		int sum=0;//用于求总和
		for(int i=1;i<=t;i++)//读入物体重量
		{
			cin>>w[i];
			sum+=w[i];//加到总和里
		}
		cin>>c1>>c2;//读入两船载重量
		//输入检查:如果c1+c2都没有所有物体加和大,不可能装得下
		if(c1+c2<sum)
		{
			cout<<"Case "<<++count<<endl;
			cout<<"No"<<endl;
			//释放空间
			delete[] w;
			delete[] c;
			delete[] b;
			continue;//直接进入下次循环
		}
		//每次初始化
		cw=bestw=0;
		ok=false;
		//从第0层向第1层开始搜索
		backtrack(1);
		cout<<"Case "<<++count<<endl;
		if(true==ok && sum-bestw<=c2)//如果对于c1而言能找到ok的解,并且剩下的物体不超过c2容量
		{
			cout<<bestw<<" ";
			for(int i=1;i<=t;i++)
				cout<<b[i];//把左1右0的子树选择序列输出,也就是c1的物品选择情况
			cout<<endl;
		}
		else//否则,并不能放下
			cout<<"No"<<endl;
		//释放空间
		delete[] w;
		delete[] c;
		delete[] b;
	}
return 0;
}

这里写图片描述

最大团问题(子集树)

这里写图片描述

#include<iostream>
using namespace std;

int count=0;//全局计数
char **v;//用来存二维邻接方阵
int t;//方阵行列数
int nown,maxn;//当前节点数和当前找到过的最大节点数
bool *k;//选择序列:用来存搜索过程中的节点i是否放在了当前的图的节点集中

void backtrack(int i)//回溯法:在一棵子集树解空间中深度搜索
{
	if(i>t)//超过叶节点层,说明找到了一个解
	{
		if(nown>maxn)//看一下会不会更大
			maxn=nown;//更大就更新一下
		return ;//返回
	}
	//考虑搜索左子树(即第i个节点放入的情况)
	//约束条件剪枝:放入第i个节点后所得的图仍然是个团(完全图),才搜索左子树
	bool ok=true;
	for(int j=1;j<i;j++)//对i以前的每个节点j
		if(true==k[j] && '0'==v[i][j])//如果出现了i和在当前团中的节点j没有边的情况
		{
			ok=false;//那么就不必考虑左子树了,因为放入i会破坏当前的'团'的性质
			break;
		}
	if(true==ok)//如果可以搜索左子树,就搜索之
	{
		k[i]=true;//记录第i个放入了
		nown++;//团的规模+1
		backtrack(i+1);//向下一层进发考虑
		nown--;//当回退到本层时,因为后面还要考虑搜索右子树,团的规模要减回来(即此时还没放入第i个)
	}
	//考虑搜索右子树(即第i个节点不放入的情况)
	//边界条件剪枝:如果剩下的节点全都可以和当前构成团,都不会有更大的团,右子树就不必看了
	if(t-i+nown>maxn)//仅当可能有更大的团时才做
	{
		k[i]=false;//记录第i个不放入
		backtrack(i+1);//向下一层进发考虑
	}
return ;//左右子树都考虑完,该回退到上一层了
}


int main()
{
	while(cin>>t)
	{
		//不妨从1开始计数,分配1~t的方阵空间
		v=new char*[t+1];
		for(int i=1;i<=t;i++)
			v[i]=new char[t+1];
		//分配选择序列的空间
		k=new bool[t+1];
		//读入方阵
		for(int i=1;i<=t;i++)
			for(int j=1;j<=t;j++)
				cin>>v[i][j];
		/*
		for(int i=1;i<=t;i++)
		{
			for(int j=1;j<=t;j++)
				cout<<v[i][j]<<" ";
			cout<<endl;
		}*/
		
		//初始化
		nown=maxn=0;
		//从第0层向第1层开始搜索
		backtrack(1);
		cout<<"Case "<<++count<<": "<<maxn<<endl;
		//释放空间
		for(int i=1;i<=t;i++)
			delete[] v[i];
		delete[] v;
	}
return 0;
}

这里写图片描述

n皇后问题(排列树)

这里写图片描述

//约束①行不相同:直接让i号皇后就保持在第i行
//约束②列不相同:让t个x[i]的值都不相同,且都在1~t之间,因为只做swap()所以约束②也自然满足
//约束③不在同一斜线:abs(x[i]-x[j])!=abs(i-j)即斜率不为1或-1,该约束需要手动剪枝
#include<iostream>
#include<algorithm>//含有swap()
#include<cmath>//含有abs()
using namespace std;
int t;//每次输入的规模
int *x;//x[i]表示i号皇后放在第i行的第x[i]列,这样自然地约束了①
int total;//解的个数
int c=0;

bool Place(int i)//关于③的可行性约束:看从i以前的(算是对后续而言暂时摆好了的皇后)是否都符合③
{
	for(int k=1;k<i;k++)//对i以前的每个(它们之间肯定不用检查了,因为之前的层已经检查过了)
		if(abs(x[i]-x[k])==abs(i-k))//如果有和i号皇后在同一斜线的
			return false;//说明i号皇后列在这里不可行,返回false
return true;//否则,i号皇后的列在这里是可行的
}


void backtrack(int i)//回溯法:在一棵排列树解空间中深度优先搜索
{
	if(i>t)//说明超过了叶节点,即找到了一个可行解
		total++;//记录多了这个解
	else//否则,还要继续往下搜索
		for(int j=i;j<=t;j++)//对它和它后面行上的所有皇后
		{
			swap(x[j],x[i]);//两个皇后列列交换,这样这个位置(这一层)上就有了前面层的基础上的所有可能列
			if(true==Place(i))//手动剪枝,去掉不满足约束③的
				backtrack(i+1);//向下一层进发考虑
			swap(x[j],x[i]);//回退到这一层时,需要把列号交换回来
		}
return ;//返回上一层
}


int main()
{
	while(cin>>t)
	{
		x=new int[t+1];//分配t+1个空间,即从1开始标号
		for(int i=1;i<=t;i++)
			x[i]=i;//初始化,第i个皇后放在第i行,这是一种符合①②的摆法
		//初始化
		total=0;
		backtrack(1);//从第1个皇后,即第1行开始搜索排列树解空间
		cout<<"Case "<<++c<<": n="<<t<<endl;
		cout<<"Total: "<<total<<endl<<endl;
		delete[] x;
	}
return 0;
}

这里写图片描述

旅行商问题(排列树)

这里写图片描述

#include<iostream>
#include<algorithm>
using namespace std;

int coun=0;//全局计数
int t;//矩阵维度
int **a;//邻接矩阵
int *x;//x数组用来作排列树层次,存矩阵的游标
int nowf;//当前费用
int minf;//当前已经找到的最小费用

bool Place(int i,int j)//可行性条件
{
	if(a[x[i-1]][x[j]]!=-1)//如果从i-1能进入j
		if(-1==minf || nowf+a[x[i-1]][x[j]]<minf)//限界条件:剪枝
			return true;
return false;
}

void backtrace(int i)//回溯法遍历排列树解空间
{
	if(i==t)//如果到了最后一层,那么要看看能不能回来(上一层并没有加上至第t层的值,更没有考虑回到第1层的事)
	{
		if(a[x[t-1]][x[t]]!=-1 && a[x[t]][x[1]]!=-1)//如果从第n-1层能到n层,并且从第n层能到第1层
			if(-1==minf || nowf+a[x[t-1]][x[t]]+a[x[t]][x[1]]<minf)//如果还没找到过最小,或者当前比之前的还小
				minf=nowf+a[x[t-1]][x[t]]+a[x[t]][x[1]];//更新最小
	}
	else//如果没到最后一层
	{
		for(int j=i;j<=t;j++)//对于i及其后的x里存的所有游标
			if(true==Place(i,j))//如果满足可行性条件
			{
				swap(x[j],x[i]);//交换,这样这个位置(这一层)上就有了前面层的基础上的所有可能
				nowf+=a[x[i-1]][x[i]];//因为这次选择了x[i-1]->x[i]这条路,把它加进来
				backtrace(i+1);//搜索下一层
				nowf-=a[x[i-1]][x[i]];//回退以后减回来
				swap(x[j],x[i]);//回退之后交换回来
			}
	}
return ;//返回上一层
}


int main()
{
	while(cin>>t)
	{
		//开空间:下标从1开始计数
		a=new int*[t+1];
		for(int i=1;i<=t;i++)
			a[i]=new int[t+1];
		x=new int[t+1];
		//初始化
		nowf=0;
		minf=-1;
		for(int i=1;i<=t;i++)
			for(int j=1;j<=t;j++)
				cin>>a[i][j];

		//就以第1个为起始点即可,毕竟要形成一个环,以谁开始都一样
		for(int j=1;j<=t;j++)
			x[j]=j;//先1,2,3,4,..这样排列
		//回溯法遍历排列树解空间
		backtrace(2);//从1->2层开始考虑

		//输出
		cout<<"Case "<<++coun<<endl;
		if(-1==minf)
			cout<<"No loop"<<endl;
		else
			cout<<minf<<endl;
			
		//释放空间
		delete[] x;
		for(int i=1;i<=t;i++)
			delete[] a[i];
		delete[] a;
	}
return 0;
}

这里写图片描述

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值