c++之搜索算法

2 篇文章 0 订阅
2 篇文章 0 订阅

1.基本概念

搜索(Search)是各种数据结构中的一种常见运算。搜索是在给定的数据集合中,寻找符合特定条件的数据,用来搜索的这个特定条件称为键值。例如,在电话簿中,按照某人姓名查找他的电话,那么这个人的姓名就是键值。
按数据集合所含数据量的大小来分,搜索可分为内部搜索外部搜索。当数据量较小,可以直接将它载入内存中进行搜索,称这种搜索为内部搜索;当数据量较大,无法一次将它载入内存进行搜索处理,需要使用辅助存储来分批处理,称这种搜索为外部搜索。
下面,我们来介绍几个关于搜索的概念:
搜索表:由同一类型数据所组成的集合。
关键字:可惟一标识数据的数据项。
搜索:在搜索表中查找关键字值键值相同的数据。
成功搜索:在搜索表中查找关键字值键值相同的数据。
不成功搜索:在搜索表中未查找关键字值键值相同的数据。
最大搜索长度:在成功搜索中,关键字值与键值进行比较*的最大次数。
平均搜索长度(Average Search Length):在成功搜索中,关键字值与键值的平均比较次数。平均比较次数的计算公式为
ASL(n)=∑(i=1,…n)pi *ci
其中n 是数据量, pi是取第i个数据的概率,ci是搜索第 个数据所需的比较次数
为了讨论问题方便,在以下的讨论中,我们总假设搜索表为一维整型数组。

2.顺序搜索

顺序搜索是运用枚举法的思想。
顺序搜索法 (Linear Search)从搜索表的一端开始顺序扫描,依次将搜索表中的结点关键字和键值进行比较,若两者相等,则搜索成功;若扫描结束后,还没有找到与键值相等的关键字,则搜索失败。

#include<iostream>
#include<ctime>
using namespace std;
#define n 81//搜索数组的最大容量
//创建搜索数组
void Create(int* data)
{
	for (int i = 0; i < n; i++)
		data[i] = rand() % 150 + 1;
}
//输出搜索数组
void Display(int* data)
{
	int i, colNum = 0;
	for (i = 0; i < n; i++)
	{
		cout << data[i] << "\t";
		colNum++;
		if (colNum == 9)//每行按9列输出
		{
			colNum = 0;
			cout << endl;
		}
	}
	cout << endl;
}
//顺序搜索
void LinearSearch(int* data, int key)
{
	int flag = 0;//flag=0表示未搜索到key
	for (int i = 0; i < n; i++)
		if (data[i] == key)//顺序搜索,搜索到key
		{
			cout << "在第" << i << "个位置,找到" << key << endl;
			flag = 1;//搜索到key
		}
	if (flag == 0)//如果搜索失败
		cout << "没有找到" << key << endl;
}
//主函数
int main()
{
	int i, key, data[n];
	srand(time(NULL));//设置随机数发生器种子
	Create(data);//创建搜索数组
	cout << "搜索数组" << endl;
	Display(data);//输出搜索数组
	while (1)
	{
		cout << "请输入搜索值(1-150),输-1退出搜索: ";
		cin >> key;
		if (key == -1)
			break;
		LinearSearch(data, key);//调用顺序搜索函数
	}
	return 0;
}

在这里插入图片描述如果搜索数据没有重复,找到数据就中止搜索的话,最坏情况是未找到数据,这仍需要进行 n次比较,故最大搜索长度为n
假设搜索值在第i个位置,则比较次数ci=i+1,如果认为数据出现的位置是等可能的,即概率pi=1/n,故平均搜索长度为ASL=(n+1)/2

3.二分搜索

二分搜索法(Binary Search)只适用于搜索数据已被排序的情况。假设搜索数据是升序的,二分搜索法是将数据分成两等份,再比较键值与中间值的大小,如果键值小于中间值,可确定要搜索的数据为前半部分,否则为后半部分。如此进行下去,直到搜索成功或搜索不成功为止。

#include<iostream>
#include<iomanip>
using namespace std;
//输出数组
void Display(int *data,int n)
{	int i,colNum=0;
	for(i=0;i<n;i++)
	{	cout<<setw(4)<<data[i];
		colNum++;
		if(colNum==9)//每行按9列输出
		{	colNum=0;
			cout<<endl;
		}
	}
	cout<<endl;
}
//二分搜索
void BinarySearch(int *data,int key,int n)
{	int z,mid,y,flag;//左中右下标,标识变量
	z=0;y=n-1;
	if(key<data[z]||key>data[y])
	{	cout<<"键值超界,无法找到"<<key<<endl;
		return;//什么都不做
	}
	flag=0;//假设未搜索到key
	while(z<=y)
	{	mid=(z+y)/2;//两分
		if(key<data[mid])
		{	y=mid-1;
			if(z<=y)
				cout<<"搜索左半部分,下标"
					<<setw(2)<<z<<setw(2)<<"-"<<setw(2)<<y<<endl;
			else
				break;
		}
		else if(key>data[mid])
		{	z=mid+1;
			if(z<=y)
				cout<<"搜索右半部分,下标"
					<<setw(2)<<z<<setw(2)<<"-"<<setw(2)<<y<<endl;
			else
				break;
		}
		else
		{	cout<<"在第 "<<mid<<" 个位置,搜索到"<<key<<endl;
			flag=1;//搜索到key
			break;
		}
	}
	if(flag==0)
		cout<<"没有搜索到"<<key<<endl;
}
//主函数
int main()
{	int data[9]={2,3,5,8,9,11,12,16,18};//待搜索数组
	int key;//键值
	cout<<"搜索数组"<<endl;
	Display(data,9);//输出搜索数组
	//输入搜索的键值key
	while(1)
	{	cout<<"请输入搜索值,输-1退出搜索: ";
		cin>>key;
		if(key==-1)
			break;
		BinarySearch(data,key,9);//调用二分搜索函数
	}
	return 0;
}

在这里插入图片描述为了求二分搜索法的平均搜索长度,我们列出上述运行结果的下标搜索范围、中心下标和中心下标对应值,如下表所示。
在这里插入图片描述按由上至下的顺序,画中心下标对应值排序二叉树
在这里插入图片描述从上图可以看出,搜索结点11的过程就是沿根结点9到结点11的路径进行搜索,比较次数为3,恰好是结点11在树中的层数
对于含n个互异数据的升序序列进行二分搜索,有
ASL(n)≤(n+1)/n ⌈log2⁡( n+1)⌉-1。
对于 ,采用二分搜索的平均比较次数约9次,而采用顺序搜索平均比较次数约500次,可见二分搜索的效率是很高的。该算法的缺点是需事先对关键字进行排序,而排序也需要耗费时间,此算法仅适用于采用顺序存储有序线性表的搜索。

3.二叉搜索树

二叉搜索树就是排序二叉树。在树的数据结构段中,函数Create()是通过逐个结点插入的方式来创建排序二叉树的。因此,可以将此函数看作是二叉搜索树的插入函数Insert()。向二叉搜索树中插入一个新元素,应保持二叉树仍为二叉搜索树。

3.1二叉搜索树的搜索

二叉搜索树的搜索算法使用while循环,从根结点开始搜索,将键值key与当前结点值进行比较。若key等于该结点的值,则搜索以成功而终止;若key小于该结点的值,则继续搜索左子树;否则继续搜索右子树。只有搜索到达空子树时,搜索以失败而终止。
搜索函数的代码如下:

void Search(BinTree T,int key)
{	Node *p=T.root;//定义探测指针p并指向根结点   
	while(p)//当p不指向空值时
	{	if(key==p->data)//如果键值等于父结点值
		{	cout<<"找到"<<key<<endl;//输出找到信息
			return;//什么都不做
		}
		else if(key<p->data)//如果键值小于父结点值
			p=p->lChild;//搜索左子树
		else if(key>p->data)//如果键值大于父结点值
			p=p->rChild;//搜索右子树
	}
	cout<<"没有找到"<<key<<endl;//输出未找到信息
}

3.2二叉搜索树的删除

在二叉搜索树上删除一个结点,需保证删除后的二叉树仍然是二叉搜索树。为了讨论方便,假定被删除结点为p,其父结点为f。删除过程可按下述两种情况分别处理:
(1)如果被删除的结点 没有左子树,则只需把结点f指向p的指针,改为指向p的右子树,然后,再删除结点p。
在这里插入图片描述(2)如果被删除的结点 有左子树,则从结点p的左子树中选择结点值最大的结点 (它就是 的左子树中最右下角的结点,该结点 可能有左子树,但一定无右子树(如果有右子树,说明s不是最大,矛盾)),把结点s的值复制到结点p中,再将指向s的指针改为指向结点s的左子树,然后,再删除结点s。
在这里插入图片描述删除函数的代码如下:

void Delete(BinTree T,int key)
{	Node *f,*p,*q,*s;
	p=T.root;f=NULL;//p指向根结点,f是p的双亲结点(直接前驱结点)
	//搜索值为key的结点p
	while(p&&p->data!=key)//当p不为空且p的值不等于键值时
	{	f=p;
		if(p->data>key)//如果p的值大于键值
			p=p->lChild;//p向左子树下移
		else//如果p的值不大于键值
			p=p->rChild;//p向右子树下移
	}
	if(p==NULL)//如果删除结点不存在
	{	cout<<"结点"<<key<<"不存在,无法删除"<<endl;
		return;//什么都不做
	}
	if(p->lChild==NULL)//如果删除结点没有左子树
	{	if(f==NULL)//如果删除结点为根结点
			T.root=p->rChild;//树根结点右移
		else if(f->lChild==p)//如果双亲结点的左孩子为删除结点
			f->lChild=p->rChild;
		else
			f->rChild=p->rChild;
		delete p;
	}
	else//如果删除结点有左子树
	{	q=p;s=p->lChild;
		while(s->rChild)
		{	q=s;
			s=s->rChild;
		}
		if(q==p)
			q->lChild=s->lChild;
		else
			q->rChild=s->lChild;
		p->data=s->data;
		delete s;
	}
}

3.3最大搜索长度与平均搜索长度

假设数组有n个元素,将它的元素逐一插入二叉搜索树,其树的最大高度可达到n
例如,将数组{9,8,5,3,2}中的元素,逐一插入二叉搜索树,可获得高5层的树,如下左图所示,称这种树为左单枝树。同样,由数组{2,3,5,8,9}可得到高达5层的右单枝树,如下右图所示。
在这里插入图片描述对于含n个互异结点值的二叉搜索树,有 ASL(n)≤log2⁡n+1/n。
顺序搜索与二分搜索是一种静态搜索,主要适用于顺序表结构,并且对表中结点仅做搜索操作,而不做插入和删除操作。动态搜索不仅要搜索结点,而且还要不断地插入和删除结点。当表采用顺序结构时,这需花费大量的时间用于结点的移动,效率很低。当采用二叉搜索树结构时,则不需要移动太多结点。
例题:在数据{9,3,12,2,5,11,16,8,18}中搜索3、4和11,若存在删除它,若不存在显示相关信息;再插入数据4,7,15,然后再搜索4。

#include<iostream>
using namespace std;
//二叉树结点定义
struct Node
{	int data;//数据域
	Node *lChild,*rChild;//指针域
};
//二叉树定义
struct BinTree
{	Node *root;};
//创建空二叉树
void Create(BinTree &T)
{	T.root=NULL;}
//判空
int IsEmpty(BinTree T)
{	if(T.root==NULL) return 1;
	else return 0;
}
//插入
void Insert(BinTree &T,int x)
{	Node *NewNode,*p;
	NewNode=new Node; //创建新结点
	NewNode->data=x;
	NewNode->lChild=NULL;
	NewNode->rChild=NULL;
	int flag=0;//新结点入树标识,flag=0表示未入树
	if(IsEmpty(T))//如果二叉树为空
		T.root=NewNode;
	else//如果二叉树非空
	{	p=T.root;//探测指针指向根结点
		while(!flag)//当新结点未入树时
		{	if(x<p->data)//如果新结点的值小于双亲结点的值
			{//进入左子树 
				if(p->lChild==NULL)//如果左子树为空		体会此处的选择结构
				{	p->lChild=NewNode;//新结点成为左子树
					flag=1;//新结点入树
				}
				else//如果左子树非空
					p=p->lChild;//探测指针向左下移
			}
			else//如果新结点的值大于双亲结点的值
			{  //进入右子树
				if(p->rChild==NULL)//如果右子树为空
				{	p->rChild=NewNode;//新结点成为右子树
					flag=1;//新结点入树
				}
				else
					p=p->rChild;//探测指针向右下移
			}
		}
	}
}
//前序遍历
void PreOrder(Node *p)
{	if(p!=NULL)
	{	cout<<p->data<<" ";//访问树根
		PreOrder(p->lChild);//遍历左子树
		PreOrder(p->rChild);//遍历右子树
	}
}
//中序遍历
void InOrder(Node *p)
{ if(p!=NULL)
	{	InOrder(p->lChild);//遍历左子树
		cout<<p->data<<" ";//访问树根
		InOrder(p->rChild);//遍历右子树
	}
}
//搜索
void Search(BinTree T,int key)
{	Node *p=T.root;//探测指针指向根结点
	while(p)//当p不指向空值时
	{		if(key==p->data)//如果键值等于父结点值
		{		cout<<"找到"<<key<<endl;//输出找到信息
			return;//什么都不做
		}
		else if(key<p->data)//如果键值小于父结点值
			p=p->lChild;//搜索左子树
		else if(key>p->data)//如果键值大于父结点值
			p=p->rChild;//搜索右子树
	}
	cout<<"没有找到"<<key<<endl;//输出未找到信息
}
//删除
void Delete(BinTree T,int key)
{	Node *f,*p,*q,*s;
	p=T.root;f=NULL;//p指向根结点,f是p的父结点(直接前驱结点)
	//搜索值为key的结点p
	while(p&&p->data!=key)//当p不为空且p的值不等于键值时
	{	f=p;
		if(p->data>key)//如果p的值大于键值
			p=p->lChild;//p向左子树下移
		else//如果p的值不大于键值
			p=p->rChild;//p向右子树下移
	}
	if(p==NULL)//如果删除结点不存在
	{	cout<<"结点"<<key<<"不存在,无法删除"<<endl;
		return;//什么都不做
	}
	if(p->lChild==NULL)//如果删除结点没有左子树
	{	if(f==NULL)//如果删除结点为根结点
			T.root=p->rChild;//树根结点右移
		else if(f->lChild==p)//如果父结点的左孩子为删除结点
			f->lChild=p->rChild;
		else
			f->rChild=p->rChild;
		delete p;
	}
	else//如果删除结点有左子树
	{	q=p;s=p->lChild;
		while(s->rChild)
		{	q=s;
			s=s->rChild;
		}
		if(q==p)
			q->lChild=s->lChild;
		else
			q->rChild=s->lChild;
		p->data=s->data;
		delete s;
	}
}
//主函数
int main()
{	int i,data[9]={9,3,12,2,5,11,16,8,18};
	BinTree T;//定义二叉树
	Create(T);//创建空二叉树
	for(i=0;i<9;i++)
		Insert(T,data[i]);//创建二叉搜索树	
	cout<<"前序遍历 ";PreOrder(T.root);cout<<endl;
	cout<<"中序遍历 ";InOrder(T.root);cout<<endl;
	Delete(T,3);Delete(T,4);Delete(T,11);//删除3,4,11
	cout<<"前序遍历 ";PreOrder(T.root);cout<<endl;
	cout<<"中序遍历 ";InOrder(T.root);cout<<endl;
	Insert(T,4);Insert(T,7);Insert(T,15);//插入4,7,15
	cout<<"前序遍历 ";PreOrder(T.root);cout<<endl;
	cout<<"中序遍历 ";InOrder(T.root);cout<<endl;
	Search(T,4);//搜索4
	return 0;
}

在这里插入图片描述

4.深度优先搜索DFS

常见的搜索问题有以下三种:
(1)在集合上,搜索某元素或元素所在位置。(顺序搜索,二分搜索,二叉搜索树)
(2)在解空间上,搜索满足某种约束条件的解或解的个数。
(3)在解空间上,搜索满足某种约束条件,且使目标函数取得最值的解或解的个数。
上述讲的是基于在集合上,搜索某元素或元素所在位置。现在来讲后两种。在讲DFS与BFS的时候我们都以具体的或者著名的实例来说明。

【著名例题】鸡兔同笼

鸡兔同笼,头3脚10,问鸡兔各多少?
数学模型:设鸡x0,兔x1,则x0+x1=3,2x0+4x1=10,真解x0=1,x1=2。
对于此例,解空间S={(x0,x1) | 0≤x0,x1≤3 },约束条件x0+x1=3,2x0+4x1=10, ,解(1,2)或解的个数为1。

一.按先x[0]后x[1]进行搜索解空间

按先x[0]后x[1]进行的搜索的逻辑结构如下图所示:
在这里插入图片描述

#include<iostream>
using namespace std;
int x[2];//解
int cnt=0;//统计搜索点个数

//输出搜索点坐标
void display(int *x)
{
	static int col=0;
	cout<<"("<<x[0]<<","<<x[1]<<")"<<" ";//输出搜索点坐标
	col++;//列数增1
	if(col==4)//如果一行输出4列
	{
		cout<<endl;//换行
		col=0;//更新col
	}
}
//按先纵后横进行搜索
void search()
{
	for(x[0]=0;x[0]<=3;x[0]++)//先x[0]
	{
		for(x[1]=0;x[1]<=3;x[1]++)//后x[1]
		{
			display(x);//输出搜索点坐标
			cnt++;//搜索点个数增1
		}
	}
}
//主函数
int main()
{
	search();
	cout<<"cnt="<<cnt<<endl;
	return 0;
}

在这里插入图片描述二.按先x[1]后x[0]进行搜索
按先x[1]后x[0]进行搜索的逻辑结构如下图所示:
在这里插入图片描述

#include<iostream>
using namespace std;
int x[2];//解
int cnt=0;//统计搜索点个数
//输出搜索点坐标
void display(int *x)
{
	static int col=0;
	cout<<"("<<x[0]<<","<<x[1]<<")"<<" ";//输出搜索点坐标
	col++;//列数增1
	if(col==4)//如果一行输出4列
	{
		cout<<endl;//换行
		col=0;//更新col
	}
}

//按先x[1]后x[0]进行搜索
void search()
{
	for(x[1]=0;x[1]<=3;x[1]++)
	{
		for(x[0]=0;x[0]<=3;x[0]++)
		{
			display(x);//输出搜索点坐标
			cnt++;//搜索点个数增1
		}
	}
}
//主函数
int main()
{
	search();
	cout<<"cnt="<<cnt<<endl;
	return 0;
}

在这里插入图片描述

上述两种对解空间的搜索,具有较好的数学背景,容易理解,也容易实现。但当解x的维数大于3时,这种搜索法的逻辑结构不易表示
而按以下树型结构进行搜索,可表示对高维x的搜索。
三.按深度优先进行搜索
深度优先搜索的逻辑结构如下图所示:
在这里插入图片描述
实现这种树型(解空间树)搜索,可以用一种特别的搜索函数来实现,只是这种函数是递归的,想通过程序代码,解释它可实现这种树型搜索是困难的,也无法说清楚。但是,从通过程序运行的结果,却可证实它的确可以实现这种树型搜索。

#include<iostream>
using namespace std;
#define n 4
int S[n]={0,1,2,3};//可重复解空间
int vis[n]={0};//访问标识
int x[2];//解
int cnt=0;//统计搜索点个数

//输出搜索点坐标
void display(int *x)
{
	static int col=0;
	cout<<"("<<x[0]<<","<<x[1]<<")"<<" ";
	col++;
	if(col==4)
	{
		cout<<endl;
		col=0;
	}
}
//按深度优先搜索
void search(int k)
{//k表示结点所处层(从0层开始计数)
	if(k==2)//如果结点层到达叶子结点层
	{
		display(x);//输出搜索点坐标
		cnt++;
		return;
	}
	for(int i=0;i<n;i++)//穷举可重复的解空间
	{
		x[k]=S[i];
		vis[i]=1;//置S[i]已被访问
		search(k+1);//递归调用
		vis[i]=0;//恢复S[i]未被访问
	}
}
//主函数
int main()
{
	search(0);//从0层开始搜索
	cout<<"cnt="<<cnt<<endl;
	return 0;
}

在这里插入图片描述
深度优先进行搜索,上述程序给出实现这种搜索的核心框架,它具有结构清晰,代码简单,易于记忆与复用,值得学习与借鉴。
但当解的维数较高时,递归深度会增大,内存消耗过多,代码涵义不易理解。
以后,我们将这种搜索称为深搜法,一般用DFS()表示该函数。
四.全域深搜法
从某点出发,按预设方向,对解空间进行搜索,是深搜法的另一种常用形式。譬如,我们从点(0,0)出发,按上、右、左、下的方向dir[4][2]={{0,1},{1,0},{-1,0},{0,-1}},搜索解空间。

#include<iostream>
using namespace std;
int dir[4][2]={{0,1},{1,0},{-1,0},{0,-1}};//搜索方向为上右左下
int vis[4][4]={0};//访问标识
//输出点
void Display(int x,int y)
{	static int col=0;
	cout<<" ("<<x<<","<<y<<") ";
	col++;
	if(col==8)
	{
		cout<<endl;
		col=0;
	}
}
//检验点的合理性
int Test(int x,int y)
{
	if(x<0||x>3||y<0||y>3)//如果点(x,y)超界
		return 0;
	if(vis[x][y]==1)//如果点(x,y)已访问
		return 0;
	return 1;
}
//深搜
void DFS(int x,int y,int k)
{
	Display(x,y);//输出(x,y)
	int mx,my;//(mx,my)为移动点
	if(k==16)//如果搜索完16个点
	{
		exit(0);//强制退出搜索
	}
	for(int i=0;i<4;i++)
	{
		mx=x+dir[i][0];my=y+dir[i][1];//从点(x,y)移动到点(mx,my)
		if(Test(mx,my))//如果点(mx,my)的合理性检验通过
		{
			vis[mx][my]=1;//置点(mx,my)已被访问
			DFS(mx,my,k+1);//再从点(mx,my)出发,深搜下一个点 
			vis[mx][my]=0;//恢复点(mx,my)未被访问 
		}
	}
}
//主函数
int main()
{	vis[0][0]=1;//置点(0,0)被访问
	DFS(0,0,1);//第1个点从(0,0)开始深搜
	return 0;
}

在这里插入图片描述
如果将搜索方向改变为
dir[4][2]={{0,1},{1,0},{0,-1},{-1,0}};//搜索方向为上右下左
搜索过程中,所遍历的格点如下图所示。
在这里插入图片描述

搜索路线如下图所示:

在这里插入图片描述
五.用深搜法求解鸡兔同笼问题
由于深搜法可以遍历解空间中的每个可能解,因此,可以用深搜法求解鸡兔同笼问题。当然,需要为检验可能解是否为真解,配置一个专门的检验函数Test()。

#include<iostream>
using namespace std;

int S[4] = { 0,1,2,3 };//可重复解空间
int vis[4] = { 0 };//访问标识
int x[2];//解
//输出真解
void Display(int* x)
{
	cout << "(" << x[0] << "," << x[1] << ")" << endl;
}
//检验可能解是否为真解
int Test(int* x)
{
	if (x[0] + x[1] != 3)//如果头不等于3
		return 0;
	if (2 * x[0] + 4 * x[1] != 10)//如果脚不等于10
		return 0;
	return 1;
}
//深搜法求解鸡兔同笼问题
void DFS(int k)
{
	//k表示结点层(从0开始计数)
	if (k == 2)//如果结点层到达叶子结点层
	{
		if (Test(x))//如果x满足检验条件
			Display(x);//输出真解
		return;//什么都不做
	}
	for (int i = 0; i < 4; i++)
	{
		x[k] = S[i];
		vis[i] = 1;//置S[i]已被访问
		DFS(k + 1);//深搜下一个结点
		vis[i] = 0;//恢复S[i]未被访问
	}
}
int main()
{
	DFS(0);//从0层结点开始深搜
	return 0;
}

在这里插入图片描述

用深搜法求解01背包问题(子集问题)

已知3种物品(每种物品仅有1件),物品的编号、重量、价值如下表所示:
在这里插入图片描述现有一容量为C=25的背包,欲从3件物品中选取若干件装入背包,使装入背包的物品价值最大。试给出最大价值和它对应的装包方案。
在这里插入图片描述
2 解空间的逻辑结构
如下图所示,解空间树中第 层(从0层开始计数)的每个结点,表示对物品 做出的某种特定选择。左分支表示装入物品,右分支表示不装入物品。那么,从结点1到叶子结点的一条路径,就代表了一种装包方案。例如,从结点1到结点11,选择物品的方案是011,即不选物品1,选物品2,选物品3,对应的价值为0+30+25=55。
在这里插入图片描述
枚举法就是对这8个叶子结点,是否为可行解进行判断,放弃那些不可行解,如结点1,结点2,因为这种装包方案超出了背包容量;然后,在那些可行解中,找出具有最大价值的装包方案的结点,如结点11,一般地,我们将这种结点称为最优结点,它对应的装包方案011,称为最优解
如果把这棵解空间树看作是无向图,生成图中结点的次序,实际上是对图中结点的深度优先遍历。
3 深搜法的原理
从下图可以看出,对完全没有可能构成可行解的分枝,如结点1至结点4,结点1至结点5,没必要再生成叶子结点4和结点5,然后再对它们是否为可行解进行判断,可以在结点3,就放弃对这两个分枝的生成,这种技术称为“剪枝”。显然,使用“剪枝”,可以更高效地去寻找最优解。
深搜法就是这样一种带有“剪技”功能的枚举法,也可称它为改进的枚举法。深搜法的逻辑结构如下图所示。
用全局变量bestW,bestP=0,bestX表示最佳装包重量,最佳装包价值,最佳装包方案,具体的深搜过程如下。
(1) 创建结点1,置当前背包重量cw=0,价值cp=0,装包方案x=[0,0,0]。
(2) 创建结点2,取物品1,则当前背包重量cw=20,价值cp=20,装包方案x=[1,0,0]。
(3) 创建结点3,取物品2,则cw=20+15=35超出背包容量C=25,放弃创建,剪枝。
(4) 回溯到结点2,创建结点6,不取物品2,则cw=20,cp=20,x=[1,0,0]。
(5) 创建结点7,取物品3,则cw=20+10=30超出背包容量C=25,放弃创建,剪枝。
(6) 创建结点8,不取物品3,则cw=20,cp=20,x=[1,0,0],到达叶子结点。因cp=20>bestP=0,更新bestP=20,bestW=20,bestX=[1,0,0]。(叶子)
(7) 回溯到结点1,创建结点9,不取物品1,则cw=0,cp=0,x=[0,0,0]。
(8) 创建结点10,取物品2,则cw=15,cp=30,x=[0,1,0]。
(9) 创建结点11,取物品11,则cw=25,cp=55,x=[0,1,1],到达叶子结点。因cp=55>bestP=20,更新bestP=55,bestW=25,bestX=[0,1,1]。(叶子)
(10)回溯到结点10,不取物品3,创建结点12,则cw=15,cp=30,x=[0,1,0],到达叶子结点。因cp=30<bestP=55,对bestP,bestW,bestX不作更新。(叶子)
(11) 回溯到结点9,不取物品2,创建结点13,则cw=0,cp=0,x=[0,0,0]。
(12) 创建结点14,取物品3,则cw=10,cp=25,x=[0,0,1],到达叶子结点。因cp=25<bestP=55,对bestP,bestW,bestX不作更新。(叶子)
(13)回溯到结点13,不取物品3,创建结点15,则cw=0,cp=0,x=[0,0,0],到达叶子结点。因cp=0<bestP=55,对bestP,bestW,bestX不作更新。(叶子)
(14)**创建完全部叶子结点,结束深搜。**输出bestW,bestP,bestX。
在这里插入图片描述4.深搜法求解

#include<iostream>
using namespace std;

#define n 3//物品数
#define C 25//背包容量
int w[n]={20,15,10};//物品重量
int p[n]={20,30,25};//物品价值

int S[2]={1,0};//可重复的解空间
int vis[2]={0};//访问标识
int x[n];//装包方案
int bestP=0,bestW,bestX[n];//最佳价值,最佳重量和最佳装包方案

//输出最佳价值、重量和装包方案
void Display()
{
	int i;
	cout<<"最佳价值 "<<bestP<<" 最佳重量"<<bestW<<endl;
	cout<<"最佳装包方案 ";
	for(i=0;i<n;i++)
		cout<<bestX[i]<<" ";
	cout<<endl;
}
//带剪枝深搜法求解0-1背包问题
void DFS(int cw,int cp,int k)
{
	int i;
	//cw当前背包重量,cp当前背包价值,k当前结点的层(从0开始计数)
	if(k==n)//如果结点层到达叶子结点
	{
		if(cp>bestP)//如果当前背包价值大于最佳价值
		{
			bestP=cp;//更新最佳价值
			bestW=cw;//更新最佳重量
			for(i=0;i<n;i++)//更新最佳装包方案
				bestX[i]=x[i];
		}
		return;
	}
	for(i=0;i<2;i++)//穷举可重复的解空间
	{
		x[k]=S[i];
		vis[i]=1;
		if(cw+x[k]*w[k]<=C)
		{
				cw+=x[k]*w[k];//更新当前背包重量
				cp+=x[k]*p[k];//更新当前背包价值
				DFS(cw,cp,k+1);//回溯
				cw-=x[k]*w[k];//恢复背包重量
				cp-=x[k]*p[k];//恢复背包价值
				vis[i]=0;//恢复访问标识
		}
	}
}
//主函数
int main()
{
	DFS(0,0,0);//从当前背包重量为0,价值为0,结点层为0开始深搜
	Display();//输出最优解
	return 0;
}

在这里插入图片描述

用深搜法求解TSP问题(全排列问题)

TSP问题
旅行商从某城市出发,通过若干个城市一次且仅一次,最后仍回到原来出发的城市,问应如何选择行走路径,使总代价最小。
下图给出了一个城市数n=4的旅行商问题和它的代价矩阵(亦即邻接矩阵)。求它所对应的旅行商问题(Travelling Salesman Problem)。
在这里插入图片描述

在这里插入图片描述
2 解空间的逻辑结构
如下图所示,解空间树中任意一个分支,代表着一种全排列。例如,结点1到结点5这一分枝,表示全排列0123;分枝结点1到结点65,表示全排列3210。
在这里插入图片描述3 全排列法
枚举法就是从4!=24个叶子结点中,通过比较,寻找具有最小代价的叶子结点。很显然,可以利用全排列法解决此问题。

#include<iostream>
#include<algorithm>
#include<iomanip>
#define inf 10000//无穷大 
#define n 4//城市数
using namespace std;

int C[n][n]={{inf,3,6,7},{12,inf,2,8},{8,6,inf,2},{3,7,6,inf}};//代价矩阵
int x[n]={0,1,2,3};//初始解(说明:必须是正序的)
//输出代价矩阵
void Output()
{	int i,j;
	for(i=0;i<n;i++)
	{
		for(j=0;j<n;j++)
		{	if(i==j) cout<<setw(4)<<"inf";
			else cout<<setw(4)<<C[i][j];
		}
		cout<<endl;
	}
}
//输出路径数,路径,代价
void Display()
{	int i,c=0;
	static int k=1,col=0;
	//计算代价
	for(i=0;i<n-1;i++)
		c+=C[x[i]][x[i+1]];
	c+=C[x[n-1]][x[0]];
	//输出路径数,路径,代价
	cout<<setw(4)<<k<<":";
	for(i=0;i<n;i++)
		cout<<setw(2)<<x[i];
	cout<<setw(4)<<c;
	//每行输出3列
	col++;
	if(col==3)
	{	cout<<endl;
		col=0;
	}
	k++;
}
//主函数
int main()
{	cout<<"代价矩阵"<<endl;
	Output();//输出邻接矩阵
	cout<<"路径数,路径,代价"<<endl;
	do
	{
		Display();
	}while(next_permutation(x,x+4));
	return 0;
}

在这里插入图片描述4 不带剪枝的深搜法
利用不带剪枝的深搜法,列出所有路径,并从中找出某条(或所有条)代价最小路径,也是容易实现的。但它却未体现出深搜法的优势,从本质上讲,仍然是全排列法。

#include<iostream>
#include<iomanip>
#define inf 2147483647//无穷大 
#define n 4//城市数
using namespace std;

int C[n][n]={{inf,3,6,7},{12,inf,2,8},{8,6,inf,2},{3,7,6,inf}};//代价矩阵
int S[n]={0,1,2,3};//不可重复的解空间
int vis[n]={0};//访问标识
int x[n];//解
//输出代价矩阵
void Output()
{
	int i,j;
	for(i=0;i<n;i++)
	{
		for(j=0;j<n;j++)
		{
			if(i==j) cout<<setw(4)<<"inf";
			else cout<<setw(4)<<C[i][j];
		}
		cout<<endl;
	}
}
//输出路径数,路径,代价
void Display()
{
	int i,c=0;
	static int k=1,col=0;
	//计算代价
	for(i=0;i<n-1;i++)
		c+=C[x[i]][x[i+1]];
	c+=C[x[n-1]][x[0]];
	//输出路径数,路径,代价
	cout<<setw(4)<<k<<":";
	for(i=0;i<n;i++)
		cout<<setw(2)<<x[i];
	cout<<setw(4)<<c;
	//每行输出3列
	col++;
	if(col==3)
	{
		cout<<endl;
		col=0;
	}
	k++;
}
//深搜
void DFS(int k)
{
	if(k==n)//如果k达到叶子结点
	{
		Display();//输出路径及代价
		return;
	}
	for(int i=0;i<n;i++)//穷举不可重复的解空间 
	{
		if(vis[i]==0)//如果S[i]未访问
		{
			x[k]=S[i];//初选x=S[i] 
			vis[i]=1;//标识S[i]已访问
			DFS(k+1);//深搜
			vis[i]=0;//恢复访问标识
		}
	}
}
//主函数
int main()
{	cout<<"代价矩阵"<<endl;
	Output();//输出邻接矩阵
	cout<<"路径数,路径,代价"<<endl;
	DFS(0);//深搜
	return 0;
}

在这里插入图片描述
如果仅需输出具有最小代价的路径,程序如下:

#include<iostream>
#include<iomanip>
#define inf 10000//无穷大 
#define n 4//城市数
using namespace std;

int C[n][n]={{inf,3,6,7},{12,inf,2,8},{8,6,inf,2},{3,7,6,inf}};//代价矩阵
int S[n]={0,1,2,3};//不可重复的解空间
int vis[n]={0};//访问标识
int x[n];//解
int bestC=inf;//最小代价

//输出代价矩阵
void Output()
{	int i,j;
	for(i=0;i<n;i++)
	{	for(j=0;j<n;j++)
		{	if(i==j) cout<<setw(4)<<"inf";
			else cout<<setw(4)<<C[i][j];
		}
		cout<<endl;
	}
}
//输出路径数,路径,代价
void Display()
{	int i;
	static int k=1;
	//输出路径数,路径,代价
	cout<<setw(4)<<k<<":";
	for(i=0;i<n;i++)
		cout<<setw(2)<<x[i];
	cout<<setw(4)<<bestC<<endl;
	k++;
}
//深搜
void DFS(int k)
{
	if(k==n)//如果k达到叶子结点
	{
		int c=0;//计算代价
		for(int i=0;i<n-1;i++)
			c+=C[x[i]][x[i+1]];
		c+=C[x[n-1]][x[0]];
		if(c<=bestC)
		{	bestC=c;//更新最小代价
			Display();//输出路径及代价
		}
		return;
	}
	for(int i=0;i<n;i++)//穷举不可重复的解空间 
	{
		if(vis[i]==0)//如果S[i]未访问
		{	x[k]=S[i];//初选x=S[i] 
			vis[i]=1;//标识S[i]已访问
			DFS(k+1);//深搜
			vis[i]=0;//恢复访问标识
		}
	}
}
//主函数
int main()
{	cout<<"代价矩阵"<<endl;
	Output();//输出邻接矩阵
	cout<<"路径数,路径,代价"<<endl;
	DFS(0);//深搜
	return 0;
}

在这里插入图片描述如果仅需输出一条具有最小代价的路径,程序可作以下改动:

#include<iostream>
#include<iomanip>
#define inf 10000//无穷大 
#define n 4//城市数
using namespace std;

int C[n][n]={{inf,3,6,7},{12,inf,2,8},{8,6,inf,2},{3,7,6,inf}};//代价矩阵
int S[n]={0,1,2,3};//不可重复的解空间
int vis[n]={0};//访问标识
int x[n];//解
int bestX[n],bestC=inf;//最佳路径与最小代价
//输出代价矩阵
void Output()
{
	int i,j;
	for(i=0;i<n;i++)
	{
		for(j=0;j<n;j++)
		{
			if(i==j) cout<<setw(4)<<"inf";
			else cout<<setw(4)<<C[i][j];
		}
		cout<<endl;
	}
}
void Save()//保留具有最小代价的最佳路径
{
	int i,c=0;//计算代价
	for(i=0;i<n-1;i++)
		c+=C[x[i]][x[i+1]];
	c+=C[x[n-1]][x[0]];
	if(c<bestC)//如果当前路径的代价小于最小代价
	{
		bestC=c;//更新最小代价
		for(i=0;i<n;i++)
			bestX[i]=x[i];//更新最佳路径
	}
}
//输出
void Display()
{
	cout<<"最佳路径: ";
	for(int i=0;i<n;i++)
		cout<<setw(2)<<bestX[i];
	cout<<" 最小代价: "<<bestC<<endl;
}
//深搜
void DFS(int k)
{
	if(k==n)//如果k达到叶子结点
	{
		Save();
		return;
	}
	for(int i=0;i<n;i++)//穷举不可重复的解空间 
	{
		if(vis[i]==0)//如果S[i]未访问
		{	x[k]=S[i];//初选x=S[i] 
			vis[i]=1;//标识S[i]已访问
			DFS(k+1);//深搜
			vis[i]=0;//恢复访问标识
		}
	}
}
//主函数
int main()
{	cout<<"代价矩阵"<<endl;
	Output();//输出邻接矩阵
	DFS(0);//深搜
	Display();//输出最佳路径与最小代价
	return 0;
}

在这里插入图片描述
上述程序有一个共同的缺点,在生成路径的中间过程中,未实施“剪枝”操作,而是待路径生成完成之后,才从中“筛选”出最佳路径与最小代价。对于大规模的城市数n,在生成路径的中间过程,就实施“剪枝”操作,可极大地提高寻找最优解的效率。
可以说,不具有“剪枝”功能的深搜,对大规模问题,是没有多大价值的。
5 带剪枝的深搜法
从下图可以看出,从结点1到结点5这一分枝,其代价cost=3+2+2+3=10。再看从结点1到结点7这一分枝,从结点1到结点6,代价cost=3+8=11已超过10,该分枝不可能产生具有最小代价,可放弃对叶子结点的创建,即实施“剪枝”。
带剪枝功能的深搜法,就是将无法获得最小代价的分枝,提前放弃对它的创建,提高搜索具有最小代价分枝的速度。
实施“剪枝”后的解空间树,如图所示。
在这里插入图片描述
带剪枝的深搜法的原理与实现程序

#include<iostream>
#include<iomanip>
using namespace std;
#define inf 0x7fffffff//正无穷大
#define n 4//城市数

int C[n][n]={{inf,3,6,7},{12,inf,2,8},{8,6,inf,2},{3,7,6,inf}};//代价矩阵
int S[n]={0,1,2,3};//不可重复的解空间
int x[n]={0};//解
int bestX[n], bestC=inf;//最佳路径与最佳代价

//输出代价矩阵
void Output()
{	cout<<"代价矩阵"<<endl;
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			if(i==j) cout<<setw(4)<<"inf";
			else cout<<setw(4)<<C[i][j];
		}
		cout<<endl;
	}
}
//输出从某城市出发的最佳路径与最小代价
void Display(int i)
{	cout<<"从城市"<<i<<"出发的最佳路径 ";
	for(int j=0;j<n;j++)
		cout<<bestX[j]<<" ";
	cout<<bestX[0]<<" ";
	cout<<"最小代价 "<<bestC<<endl;
}
//带剪枝的深搜
void DFS(int k,int cc,int vis[n])
{
	int i,j;
	if(k==n)//如果k到达叶子节点层
	{
		if(cc+C[x[n-1]][x[0]]<=bestC)
		{
			cc+=C[x[n-1]][x[0]];
			cout<<"当前最佳路径 ";
	 		for(i=0;i<n;i++)
				cout<<x[i]<<" "; 
			cout<<"代价 "<<cc<<endl;
			//更新最佳路径与最小代价
	 		for(i=0;i<n;i++)
				bestX[i]=x[i];
			bestC=cc;
		}
		else
		{
			cc+=C[x[n-1]][x[0]];
			cout<<"当前路径 ";
			for(i=0;i<n;i++) 
				cout<<x[i]<<" ";
			cout<<"代价 "<<cc<<" 剪枝"<<endl;
		}
	}
	for(i=0;i<n;i++)
	{
		if(vis[i]==0)//如果S[i]未被访问
		{
			x[k]=S[i];
			vis[i]=1;//置S[i]被访问
			//如果当前代价+边(x[k-1],x[k])代价小于最佳代价
			if(cc+C[x[k-1]][x[k]]<bestC) 
			{
				cc+=C[x[k-1]][x[k]];//更新当前代价
				cout<<"当前路径 ";
				for(j=0;j<=k; j++)
					cout<<x[j]<<" ";
				cout<<"代价 "<<cc<<endl;
				DFS(k+1,cc,vis);//深搜
				cc-=C[x[k-1]][x[k]]; 
				vis[i]=0;
			}
			else//如果当前代价+边(x[k-1],x[k])代价大于或等于最佳代价
			{
				cout<<"当前路径 ";
				for(j=0;j<=k;j++) 
					cout<<x[j]<<" ";
				cout<<"代价 "<<cc+C[x[k-1]][x[k]]<<" 剪枝"<<endl;
				vis[i]=0;//恢复S[i]未被访问,即剪枝 
			}
		}
	}
}
//主函数
int main()
{	Output();//输出代价矩阵
	int cc,vis[n]={0};
	x[0]=0;vis[0]=1;cc=0;//设出发城市为0
	DFS(1,cc,vis);//从k=1层开始深搜
	Display(0);//输出从城市0出发的最径路径与最小代价
	return 0;
}

在这里插入图片描述
欲实现上图所示的剪枝过程,只需将出发城市分别取0,1,2,3即可。完整的程序代码如下,因程序运行结果太长,忽略输出结果,读者可自行运行程序进行检验。

#include<iostream>
#include<iomanip>
using namespace std;
#define inf 0x7fffffff//正无穷大
#define n 4//城市数

int C[n][n]={{inf,3,6,7},{12,inf,2,8},{8,6,inf,2},{3,7,6,inf}};//代价矩阵
int S[n]={0,1,2,3};//不可重复的解空间
int x[n]={0};//解
int bestX[n], bestC=inf;//最佳路径与最佳代价

//访问标识数组初始化
void Initial(int vis[n])
{	for(int i=0;i<n;i++)
		vis[i]=0;
}
//输出代价矩阵
void Output()
{	cout<<"代价矩阵"<<endl;
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			if(i==j) cout<<setw(4)<<"inf";
			else cout<<setw(4)<<C[i][j];
		}
		cout<<endl;
	}
}
//输出从某城市出发的最佳路径与最小代价
void Display(int i)
{
	cout<<"从城市"<<i<<"出发的最佳路径 ";
	for(int j=0;j<n;j++)
		cout<<bestX[j]<<" ";
	cout<<bestX[0]<<" ";
	cout<<"最小代价 "<<bestC<<endl;
}
//带剪枝的深搜
void DFS(int k,int cc,int vis[n])
{
	int i,j;
	if(k==n)//如果k到达叶子节点层
	{
		if(cc+C[x[n-1]][x[0]]<=bestC)
		{
			cc+=C[x[n-1]][x[0]];
			cout<<"当前最佳路径 ";
	 		for(i=0;i<n;i++)
				cout<<x[i]<<" "; 
			cout<<"代价 "<<cc<<endl;
			//更新最佳路径与最小代价
	 		for(i=0;i<n;i++)
				bestX[i]=x[i];
			bestC=cc;
		}
		else
		{
			cc+=C[x[n-1]][x[0]];
			cout<<"当前路径 ";
			for(i=0;i<n;i++) 
				cout<<x[i]<<" ";
			cout<<"代价 "<<cc<<" 剪枝"<<endl;
		}
	}
	for(i=0;i<n;i++)
	{
		if(vis[i]==0)//如果S[i]未被访问
		{
			x[k]=S[i];
			vis[i]=1;//置S[i]被访问
			//如果当前代价+边(x[k-1],x[k])代价小于最佳代价
			if(cc+C[x[k-1]][x[k]]<bestC) 
			{
				cc+=C[x[k-1]][x[k]];//更新当前代价
				cout<<"当前路径 ";
				for(j=0;j<=k; j++)
					cout<<x[j]<<" ";
				cout<<"代价 "<<cc<<endl;
				DFS(k+1,cc,vis);//深搜
				vis[i]=0;//恢复S[i]未被访问
				cc-=C[x[k-1]][x[k]];//恢复当前代价
			}
			else//如果当前代价+边(x[k-1],x[k])代价大于或等于最佳代价
			{
				cout<<"当前路径 ";
				for(j=0;j<=k;j++) 
					cout<<x[j]<<" ";
				cout<<"代价 "<<cc+C[x[k-1]][x[k]]<<" 剪枝"<<endl;
				vis[i]=0;//恢复S[i]未被访问,即剪枝
			}
		}
	}
}
//主函数
int main()
{	Output();//输出代价矩阵
	int i,cc,vis[n];
	for(i=0;i<n;i++)
	{
		Initial(vis);//对访问标识数组初始化
		x[0]=i;//出发城市取i
		cc=0;//当前代价置0
		vis[i]=1;//城市i已被访问
		DFS(1,cc,vis);//从k=1层开始深搜
		Display(i);//输出从城市i出发的最径路径与最小代价
	}
	return 0;
}

用带剪枝的深搜法解TSP问题
弄明白了带剪枝深搜法的原理和程序,容易给出求解TSP问题标准程序。

#include<iostream>
#include<iomanip>
using namespace std;
#define inf 0x7fffffff//正无穷大
#define n 4//城市数

int C[n][n] = { {inf,3,6,7},{12,inf,2,8},{8,6,inf,2},{3,7,6,inf} };//代价矩阵
int S[n] = { 0,1,2,3 };//不可重复的解空间
int x[n] = { 0 };//解
int bestX[n], bestC = inf;//最佳路径与最佳代价
//访问标识数组初始化
void Initial(int vis[n])
{
	for (int i = 0; i < n; i++)
		vis[i] = 0;
}
//输出代价矩阵
void Output()
{
	cout << "代价矩阵" << endl;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			if (i == j) cout << setw(4) << "inf";
			else cout << setw(4) << C[i][j];
		}
		cout << endl;
	}
}
//输出从某城市出发的最佳路径与最小代价
void Display(int i)
{
	cout << "从城市" << i << "出发的最佳路径 ";
	for (int j = 0; j < n; j++)
		cout << bestX[j] << " ";
	cout << bestX[0] << " ";
	cout << "最小代价 " << bestC << endl;
}
//带剪枝的深搜
void DFS(int k, int cc, int vis[n])
{
	int i;
	if (k == n)//如果k到达叶子节点层
	{
		if (cc + C[x[n - 1]][x[0]] <= bestC)
		{
			cc += C[x[n - 1]][x[0]];
			//更新最佳路径与最小代价
			for (i = 0; i < n; i++)
				bestX[i] = x[i];
			bestC = cc;
			return;
		}
	}
	for (i = 0; i < n; i++)//不重复地遍历解空间
	{
		if (vis[i] == 0)//如果S[i]未被访问
		{
			x[k] = S[i];
			vis[i] = 1;//置S[i]被访问
			//如果当前代价+边(x[k-1],x[k])代价小于最佳代价
			if (cc + C[x[k - 1]][x[k]] < bestC)
			{
				cc += C[x[k - 1]][x[k]];//更新当前代价
				DFS(k + 1, cc, vis);//深搜
				vis[i] = 0;//恢复S[i]未被访问
				cc -= C[x[k - 1]][x[k]];//恢复当前代价
			}
			else//如果当前代价+边(x[k-1],x[k])代价大于或等于最佳代价
			{
				vis[i] = 0;//恢复S[i]未被访问,即剪枝
			}
		}
	}
}
//主函数
int main()
{
	Output();//输出代价矩阵
	int i, cc, vis[n];
	for (i = 0; i < n; i++)
	{
		Initial(vis);//对访问标识数组初始化
		x[0] = i;//出发城市取i
		cc = 0;//当前代价置0
		vis[i] = 1;//城市i已被访问
		DFS(1, cc, vis);//从k=1层开始深搜
		Display(i);//输出从城市i出发的最径路径与最小代价
	}
	return 0;
}

在这里插入图片描述

5.广度优先搜索BFS

【著名例题】鸡兔同笼

鸡兔同笼,头3脚10,问鸡兔各多少?
数学模型:设鸡x0,兔x1,则x0+x1=3,2x0+4x1=10,真解x0=1,x1=2。
对于此例,解空间S={(x0,x1) | 0≤x0,x1≤3 },约束条件x0+x1=3,2x0+4x1=10, ,解(1,2)或解的个数为1。
一 对解空间的广度优先搜索
广度优先搜索的逻辑结构如下图所示:
在这里插入图片描述

#include<iostream>
#include<queue>
using namespace std;
struct Node//结点定义
{	int x[2];//可能解
	int k;//结点层
};
queue<Node>Q;//定义结点队列 
//输出
void Display(Node node)
{	static int col=0;
	cout<<"("<<node.x[0]<<","<<node.x[1]<<")"<<" ";
	col++;
	if(col==4)//每行按4列输出
	{	cout<<endl;
		col=0;
	}
}
//广搜法
void BFS()
{	int i,j;
	Node node,head;//扩展结点,队头结点
	for(i=0;i<2;i++)
		node.x[i]=0;//可能解置空
	node.k=0;//结点层取0
	Q.push(node);//结点1入队
	while(!Q.empty())//当队列Q非空时
	{
		head=Q.front();//获取队头结点
		Q.pop();//队头结点出队
		if(head.k==2)//如果队头结点所处层为2
			Display(head);//输出该结点
		if(head.k<2)//如果结点所在层未达到2
		{//扩展搜索树结点
			for(i=0;i<4;i++)
			{
				for(j=0;j<2;j++)
				{
					if(j==head.k)//相等用i填充,不相等用head.x[j]填充
						node.x[j]=i;//更新结点的x[head.k]
					else
						node.x[j]=head.x[j];//让结点的x[j]与head的x[j]一致
				}
				node.k=head.k+1;//结点层增1
				Q.push(node);//新结点入队
			}
		}
	}
}
//主函数
int main()
{
	BFS();//广搜法
	return 0;
}

在这里插入图片描述
函数BFS()给出了按广度优先搜索的核心框架,值得学习和复用。尽管它比按深度优先搜索的代码复杂一些,但它是迭代算法,通过阅读代码,我们可以清晰地写出它的运行结果,这正是迭代算法的优越之处。
二 全域广搜法
若从解空间的某点出发,按预设方向,对解空间进行全域搜索,解空间如下图所示:
在这里插入图片描述
如果从左下角出发,按搜索方向dir[4][2]={{0,1},{0,-1},{-1,0},{1,0}},即按上下左右进行搜索。以下程序可以实现对整个解空间的搜索,以后,我们把这种搜索称之为全域广搜法

#include<iostream>
#include<queue>
using namespace std;
int vis[4][4] = { 0 };//解空间点的访问标识
int dir[4][2] = { {0,1},{0,-1},{-1,0},{1,0} };//搜索方向为上下左右
struct Node//定义结点
{
	int x, y;
};
queue<Node>Q;//定义结点队列 
//对移动结点进行合理性检验
int Test(Node mp)
{
	if (mp.x < 0 || mp.x >= 4 || mp.y < 0 || mp.y >= 4)//如果移动结点超界
		return 0;
	if (vis[mp.x][mp.y] == 1)//如果移动结点已被访问
		return 0;
	return 1;
}
//输出结点
void Display(Node p)
{
	static int col = 0;
	cout << "(" << p.x << "," << p.y << ")" << " ";
	col++;
	if (col == 4)
	{
		cout << endl;
		col = 0;
	}
}
//广搜
void BFS(int x, int y)
{
	Node p, mp;//定义p为当前结点,mp为移动结点
	p.x = x; p.y = y;//使p=(x,y)
	Q.push(p);//p入队
	while (!Q.empty())//当队列Q非空时
	{
		p = Q.front();//获取队头结点
		Q.pop();//队头结点出队
		Display(p);//输出队头结点
		for (int i = 0; i < 4; i++)//按预设方向生成移动结点
		{
			mp.x = p.x + dir[i][0];//获取移动结点的横坐标
			mp.y = p.y + dir[i][1];//获取移动结点的纵坐标
			if (Test(mp))//如果移动结点mp通过检验
			{
				vis[mp.x][mp.y] = 1;//置结点mp被访问
				Q.push(mp);//mp入队
			}
		}
	}
}
//主函数
int main()
{
	vis[0][0] = 1;//置结点(0,0)被访问
	BFS(0, 0);//从结点(0,0)开始广搜
	return 0;
}

在这里插入图片描述
按这种搜索方式,生成坐标点的次序是,由左下角至右上角、由上至下、取平行直线上的非负整数点,具有典型的广度优先遍历的风格。
x+y=i  ( i=0,1,2,3,4,5,6 )
在这里插入图片描述
三 用广搜法求解鸡兔同笼问题
在标准BFS代码上增加一个判断条件并配置一个专门的检验函数Test()。
由于广搜法可以遍历解空间中的每个可能解,因此,可以用广搜法求解鸡兔同笼问题。当然,需要为检验可能解是否为真解,配置一个专门的检验函数Test()。

#include<iostream>
#include<queue>
using namespace std;
struct Node//搜索树的结点定义
{
	int x[2];//可能解
	int k;//结点所处的层次
};
queue<Node>Q;//定义结点队列
//对可能解进行检验
int Test(Node node)
{
	if(node.x[0]+node.x[1]!=3)//如果头不等于3
		return 0;//返0
	if(2*node.x[0]+4*node.x[1]!=10)//如果脚不等于10
		return 0;//返0
	return 1;
}
//输出解
void Display(Node node)
{
	cout<<"("<<node.x[0]<<","<<node.x[1]<<")"<<endl;
}
//广搜
void BFS()
{
	int i,j;
	Node node,head;//扩展结点,队头结点
	for(i=0;i<2;i++)
		node.x[i]=0;//置可能解为空值
	node.k=0;//结点层取0
	Q.push(node);//结点1入队
	while(!Q.empty())//当队列Q非空时
	{
		head=Q.front();//获取队头结点
		Q.pop();//队头结点出队
		if(head.k==2)//如果队头结点所处层为2
		{
			if(Test(head))//如果队头结点是真解
				Display(head);//输出真解
		}
		if(head.k<2)//如果结点所在层未达到2
		{//扩展搜索树结点
			for(i=0;i<4;i++)
			{
				for(j=0;j<2;j++)
				{
					if(j==head.k)
						node.x[j]=i;//更新结点的x[head.k]
					else
						node.x[j]=head.x[j];//让新结点的x[j]与head的x[j]一致
				}
				node.k=head.k+1;//结点层增1
				Q.push(node);//新结点入队
			}
		}
	}
}
//主函数
int main()
{
	BFS();//广搜
	return 0;
}

在这里插入图片描述
以上求解鸡兔同笼问题的方法,本质上与枚举法一样,它对已生成的所有可能解结点。

广搜法求解0-1背包问题(子集问题)

已知3种物品(每种物品仅有1件),物品的编号、重量、价值如下表所示:
在这里插入图片描述现有一容量为C=25的背包,欲从3件物品中选取若干件装入背包,使装入背包的物品价值最大。试给出最大价值和它对应的装包方案。
在这里插入图片描述
2 解空间的逻辑结构
如下图所示,解空间树中第 层(从0层开始计数)的每个结点,表示对物品 做出的某种特定选择。左分支表示装入物品,右分支表示不装入物品。那么,从结点1到叶子结点的一条路径,就代表了一种装包方案。例如,从结点1到结点11,选择物品的方案是011,即不选物品1,选物品2,选物品3,对应的价值为0+30+25=55。

3 广搜树与搜索过程
采用广度优先搜索策略,广搜树的生成过程如图下图所示。

在这里插入图片描述4 程序实现

#include<iostream>
#include<queue>
using namespace std;
#define n 3//物品数
#define C 25//背包容量
int w[n]={20,15,10};//物品重量
int p[n]={20,30,25};//物品价值
//搜索树的结点定义
struct Node
{
	int x[n];
	int w;//结点的当前背包重量
	int p;//结点的当前背包价值
	int k;//结点所处的层次(从0开始计数)
};
int bestP=0,bestW,bestX[n];//最佳装包价值,重量和方案
void BFS();//广搜函数声明
void Display();//输出最佳装包价值,重量和方案函数声明
//主函数
int main()
{
	BFS();//广搜法求解0-1背包问题
	Display();//输出最佳价值,重量和装包方案
	return 0;
}
//广搜法求解01背包问题
void BFS()
{
	int i;
	Node Root,Front,lChild,rChild;
	queue<Node>Q;//创建结点顺序队列
	//使根结点x=[0,0,0],w=0,p=0,k=0
	for(i=0;i<n;i++)
		Root.x[i]=0;
	Root.w=0;Root.p=0;Root.k=0;
	Q.push(Root);//结点1入队
	while(!Q.empty())
	{
		Front=Q.front();//获取队头结点
		Q.pop();//队头结点出队
		//寻找最佳价值和对应的装包方案
		if(Front.p>bestP)
		{
			bestP=Front.p;
			bestW=Front.w;
			for(i=0;i<n;i++)
				bestX[i]=Front.x[i];
		}
		//扩展搜索树结点
		if(Front.k<n)//如果层数未达到n
		{
			if(Front.w+w[Front.k]<=C)//如果物品Front.k不超过背包容量
			{
				//扩展左孩子结点
				//对lChild的x赋初值
				for(i=0;i<n;i++)
				{
					if(i==Front.k) lChild.x[i]=1;//取i号物品
					else  lChild.x[i]=Front.x[i];
				}
				lChild.w=Front.w+w[Front.k];//更新当前背包重量
				lChild.p=Front.p+p[Front.k];//更新当前背包重量
				lChild.k=Front.k+1;//结点层增1
				Q.push(lChild);//左孩子入队
			}
			//总扩展右孩子结点
			for(i=0;i<n;i++)
			{
				if(i==Front.k)  rChild.x[i]=0;//不取i号物品
				else  rChild.x[i]=Front.x[i];
			}
			rChild.w=Front.w;
			rChild.p=Front.p;
			rChild.k=Front.k+1;
			Q.push(rChild);//右孩子入队
		}
	}
}
//输出最佳装包价值,重量和方案
void Display()
{
	int i;
	cout<<"最佳价值 "<<bestP<<" 最佳重量 "<<bestW<<endl;
	cout<<"装包方案 ";
	for(i=0;i<n;i++)
		cout<<bestX[i]<<" ";
	cout<<endl;
}

在这里插入图片描述

广搜法求解TSP问题(全排列问题)

TSP问题
旅行商从某城市出发,通过若干个城市一次且仅一次,最后仍回到原来出发的城市,问应如何选择行走路径,使总代价最小。
下图给出了一个城市数n=4的旅行商问题和它的代价矩阵(亦即邻接矩阵)。求它所对应的旅行商问题(Travelling Salesman Problem)。
在这里插入图片描述

在这里插入图片描述2、解空间的逻辑结构
如下图所示,解空间树中任意一个分支,代表着一种全排列。例如,分枝结点1到结点5,表示全排列0123;分枝结点1到结点65,表示全排列3210。
在这里插入图片描述

3、广搜法生成解空间
用广搜法可生成下图所示的解空间。

#include<iostream>
#include<iomanip>
#include<queue>
using namespace std;
int S[4]={0,1,2,3};//解空间
int vis[4];//维数等于S中最大值3+1
int x[4];//解(即路径) 

//结点定义
struct Node
{	int x[4];//解(即路径)
	int vis[4];//访问标识
	int k;//结点所在层
};
queue<Node>Q;//定义结点队列
//输出
void Display(Node node)
{
	static int k=1,col=0;//k结点编号,col输出控制
	cout<<setw(5)<<k<<":";
	for(int i=0;i<4;i++)
		cout<<setw(3)<<node.x[i];
	col++;
	if(col==2)
	{
		cout<<endl;
		col=0;
	}
	k++;
}
//广搜
void BFS()
{
	int i,j;
	Node node;
	//创建第0层结点
	for(i=0;i<4;i++)
	{	node.x[i]=0;//解为空
		node.vis[i]=0;//访问标识为空
	}
	node.k=0;//结点层 
	Q.push(node);//入队
	//创建第1层结点
	for(i=0;i<4;i++)
	{	node=Q.front();
		node.x[0]=S[i];
		node.vis[S[i]]=1;
		node.k++;
		Q.push(node);
	}
	Q.pop();//结点1出队 
	while(!Q.empty())
	{
		node=Q.front();
		if(node.k==4)//如果到达叶子结点层
		{
			Display(node);//输出
		}
		else//如果未到达叶子结点层
		{
			for(i=0;i<4;i++)//扩展该结点下的分枝结点
			{
				node=Q.front();
				j=node.k;
				if(node.x[i]==S[i]||node.vis[S[i]]==1)
					continue;
				else
				{	node.x[j]=S[i];
					node.vis[S[i]]=1;
				}
				node.k=j+1;//更新结点层
				Q.push(node);//扩展结点入队
			}
		}
		Q.pop();//队头结点出队
	}
}
//主函数
int main()
{	BFS();
	return 0;
}

在这里插入图片描述
4TSP问题求解
结点定义中增加当前路径代价cc,对上述程序略加修改,可给出TSP问题的全部路径。

#include<iostream>
#include<iomanip>
#include<queue>
#define inf 0x7fffffff//无穷大
using namespace std;

int S[4] = { 0,1,2,3 };//城市 
int C[4][4] = { {inf,3,6,7},{12,inf,2,8},{8,6,inf,2},{3,7,6,inf} };//代价矩阵
//结点定义
struct Node
{
	int x[4];//路径
	int vis[4];//访问标识
	int k;//结点所在层
	int cc;//当前路径的代价
};
queue<Node>Q;//定义结点顺序队列
//输出
void Display(Node node)
{
	static int k = 1, col = 0;//k结点编号,col输出控制
	cout << setw(5) << k << ":";//结点编号
	for (int i = 0; i < 4; i++)
		cout << setw(3) << node.x[i];//路径
	cout << setw(4) << node.cc;//代价
	col++;
	if (col == 2)//控制每行输出2列
	{
		cout << endl;
		col = 0;
	}
	k++;
}
//广搜
void BFS()
{
	int i, j;
	Node node;
	//创建第0层结点
	node.k = 0;
	node.cc = 0;
	for (i = 0; i < 4; i++)
	{
		node.x[i] = 0;//路径均为0
		node.vis[i] = 0;//访问数组均为0
	}
	Q.push(node);//入队
	//创建第1层结点
	for (i = 0; i < 4; i++)
	{
		node = Q.front();
		node.x[0] = S[i];
		node.vis[S[i]] = 1;
		node.k++;
		node.cc = 0;
		Q.push(node);
	}
	Q.pop();//根结点出队
	while (!Q.empty())
	{
		node = Q.front();
		if (node.k == 4)//如果结点层到达叶子结点层
		{
			node.cc += C[node.x[3]][node.x[0]];//更新当前路径的总代价
			Display(node);//输出
		}
		else//如果结点层未到达叶子结点层
		{
			for (i = 0; i < 4; i++)//扩展该结点下的分枝结点
			{
				node = Q.front();
				j = node.k;
				if (node.x[i] == S[i] || node.vis[S[i]] == 1)
					continue;
				else
				{
					node.x[j] = S[i];
					node.vis[S[i]] = 1;
				}
				node.cc += C[node.x[j - 1]][node.x[j]];//更新当前路径代价
				node.k = j + 1;//更新结点层
				Q.push(node);//扩展结点入队
			}
		}
		Q.pop();//队头结点出队
	}
}
//主函数
int main()
{
	BFS();
	return 0;
}

在这里插入图片描述
如果仅找出全部具有最小代价的路径,即所有的哈密顿环,需配置一个存放具有最小代价的结点表,保存具有最小代价的结点。

#include<iostream>
#include<iomanip>
#include<queue>
#define inf 0x7fffffff//无穷大 
using namespace std;
int S[4]={0,1,2,3};//城市 
int C[4][4]={{inf,3,6,7},{12,inf,2,8},{8,6,inf,2},{3,7,6,inf}};//代价矩阵
//结点定义
struct Node
{
	int x[4];//路径
	int vis[4];//访问标识
	int k;//结点所在层
	int cc;//当前路径的代价
};
//顺序表定义
struct List
{
	Node t[24];//预置结点顺序表的长度为24 
	int m;//顺序表长度
};
queue<Node>Q;//定义结点队列
List L;//定义结点顺序表

//结点进入顺序表L,保存具有最小总代价的结点
void Enlist(Node node)
{
	static int minCC=inf;//预设最小总代价初值
	if(node.cc<minCC)
	{
		minCC=node.cc;//更新总代价
		L.m=0;//表长置0
		L.t[L.m]=node;//将结点node存入结点表
		L.m++;//表长置1
	}
	else if(node.cc==minCC)
	{
		L.t[L.m]=node;
		L.m++;
	}
}
//输出顺序表
void Display()
{
	cout<<setw(10)<<"路径"<<setw(8)<<"代价"<<endl;//输出标题
	for(int i=0;i<L.m;i++)
	{
		for(int j=0;j<4;j++)
			cout<<setw(3)<<L.t[i].x[j];
		cout<<setw(5)<<L.t[i].cc<<endl;
	}
}
//广搜
void BFS()
{
	int i,j;
	Node node;
	//创建第0层结点
	for(i=0;i<4;i++)
	{	node.x[i]=0;//路径均为0
		node.vis[i]=0;//访问数组均为0
	}
	node.k=0;
	node.cc=0;
	Q.push(node);//入队
	//创建第1层结点
	for(i=0;i<4;i++)
	{	node=Q.front();
		node.x[0]=S[i];
		node.vis[S[i]]=1;
		node.k++;
		node.cc=0;
		Q.push(node);
	}
	Q.pop();//结点1出队
	while(!Q.empty())
	{
		node=Q.front();
		if(node.k==4)//如果结点层到达叶子结点层
		{
			node.cc+=C[node.x[3]][node.x[0]];//更新当前路径的总代价
			Enlist(node);//结点入顺序表
		}
		else//如果结点层未到达叶子结点层
		{
			for(i=0;i<4;i++)//扩展该结点下的分枝结点
			{
				node=Q.front();
				j=node.k;
				if(node.x[i]==S[i]||node.vis[S[i]]==1)
					continue;
				else
				{	node.x[j]=S[i];
					node.vis[S[i]]=1;
				}
				node.cc+=C[node.x[j-1]][node.x[j]];//更新当前路径代价
				node.k=j+1;//更新结点层
				Q.push(node);//扩展结点入队
			}
		}
		Q.pop();//队头结点出队
	}
}
//主函数
int main()
{
	BFS();
	Display();
	return 0;
}

在这里插入图片描述广搜法求解TSP问题的过程来看,它是在到达叶子结子层时,才逐一生成了全部的路径,然后对每一条路径是否为最佳路径进行判断,并未实施任何剪枝操作。
由此可见,广搜法寻找最佳路径的效率,就比深搜法差一些,另外,广搜的程序代码也要复杂一些。

  • 9
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@菜鸟一枚

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值