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)≤log2n+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问题的过程来看,它是在到达叶子结子层时,才逐一生成了全部的路径,然后对每一条路径是否为最佳路径进行判断,并未实施任何剪枝操作。
由此可见,广搜法寻找最佳路径的效率,就比深搜法差一些,另外,广搜的程序代码也要复杂一些。