一.回溯法概念
回溯法也称试探法,可以把它看成一个在约束条件下对解空间树(几乎所有回溯法的解都可以化成一个n叉树)进行深度优先(先纵向后横向)查找的过程,并在查找过程中剪去那些不满足条件的分支。当用回溯法搜索解空间树的时候,如果发现某一个节点不满足约束条件或者不是最优解的时候,就该放弃对该节点子树的查找(该节点下面的子树不再进行考虑,这也是比暴力枚举优化的地方),返回其祖先节点,并对下一个兄弟节点进行考查,直到找出一个解。
二.算法框架
1.递归框架(如有发懵,请看经典问题讲解并在回过头来看框架)
search(int i)
{
if(i>n)
//输出结果
else
{
for(j=下界;j<=上界;++j)//横向搜索节点
{
if(P(i))//满足约束条件
{
x[i]=j;//将满足条件订单结果保存
search(i+1);//搜索下一个结点
//回溯清理
}
}
}
}
2.非递归框架
int x[n],i=1;//解空间以及循环层数
while(i&&(未达到目标))
{
if(i>n)
//搜索到叶节点,搜素到一个节点,输出
else
{
获取x[i]可能的值
while(!P(x[i])&&x[i]在搜索空间内)
x[i]下一个可能的值
if(x[i]在搜索空间内)
{
标识占用的资源
i++;
}else
{
清理所占用的状态空间
i--;
}
}
}
三.回溯法经典例题
1.装载问题:有一艘轮船重量为c=10,有n=5个集装箱要装进轮船,其中集装箱的重量wi分别为w={7,2,6,5,4}。请找出一种方案,将轮船尽可能装满,即在装载体积不受限制的情况下,轮船上所装载的集装箱的重量尽可能的接近于10。
具体可运行代码如下,运行结果是00101
#include<bits/stdc++.h>
using namespace std;
int c=10;//轮船的最大重量
int w[5]={7,2,6,5,4};//集装箱重量
int n=5;//集装箱个数
int nowc=0,maxc=0;//nowc表示当前的值,maxc表示当前的最优值
int i=0;//记录递归的层数,也是作为了一个终止条件
int maxx[5],x[5];//最优解 值为1表示该集装箱被装入 值为0表示该集装箱未被装入
int r=24;//剩余的重量
void search(int i)
{
if(i>n-1)//表示到达树的底部:此时需要收集解并结束递归
{
if(nowc>maxc)
{
maxc=nowc;//赋值当前的值
for(int j=0;j<n;++j)
{
maxx[j]=x[j];
}
}
return;
}
r=r-w[i];//开始搜索第i层,同时减少剩余用量
if(nowc+w[i]<=c)//能装下,也就是左树
{
x[i]=1;//当前这个集装箱可以放进去
nowc=nowc+w[i];//当前的重量
search(i+1);//搜索下一层
nowc=nowc-w[i];//回溯,减去上一次深度搜索的集装箱
}
if(nowc+r>maxc)//剩余的总重量加上当前的重量是否大于当前最优的
{
x[i]=0;//如果大于说明不能装
search(i+1);//继续深度搜索
//此时没有装,不用回溯,不用去减掉之前装的
}
r=r+w[i];
}
int main()
{
search(i);
for(int i=0;i<n;++i)
{
cout<<maxx[i]<<endl;
}
return 0;
}
算法分析
根据选择树推导,一共有n层,每层相加:1+2+……+2^n=O(2^n)
2.n皇后问题:经典的n皇后问题是指在一个n*n的棋盘上,根据国际象棋的规则,任意两个皇后不能处在同一行,同一列,同一斜线上,请给出满足条件的所有方案。
问题分析:首先是棋盘存储的问题,我们不采用二维数组,而是采用一维数组queenx来表示数组,queenx[i]=k,其中i表示第i行,k表示的是列。先逐行遍历,每一行的每一列逐个试探,如果可以摆放继续下一行,如果不可以摆放那么回溯到上一行。
int queenx[4];//queenx[i]是指摆放在第i行第queenx[i]列
int sum=0;
bool ok(int i)
{
for(int j=0;j<i;++j)
{
if(queenx[i]==queenx[j]||abs(queenx[i]-queenx[j])==i-j)//防止在同一列和同一条斜线,同一条斜线是防止在斜率为1也就是(y1-y2)/(x1-x2)==1的线上
return false;//此位置不能放皇后
}
return true;
}
void queen(int i)
{
if(i>3)
{
for(int j=0;j<4;++j)//此处的4可以换成n
cout<<queenx[j]<<" ";//输出每个皇后的列
cout<<endl;
sum++;//计算结果数量
}
for(int j=0;j<4;++j)
{
queenx[i]=j;//行数是i列数是queenx[i]也就是j
if(ok(i))
queen(i+1);
}
}
int main()
{
queen(0);
return 0;
}
算法分析:
由于每次都是按照每一行的每一个位置开始四列的摆放,那么每个节点对应n个扩展,那么解空间树的节点个数为
1+n+n^2+n^3+……+n^n=(-1)/(n-1)<=2/n=(n>=2)
由ok函数可以发现,每选择一个函数都要进行是否在同一列同一斜线上面判断,那么判断次数为:
1+2+……+n=(n+1)*n/2
那么皇后问题的实际时间复杂度为O(),由于存在剪枝操作 ,实际运行时要远小于这个时间复杂度。
3.“0-1背包”问题:已知有n件物品,物品i的重量为Wi,价值为Pi。现从中选取一部分物品装入一个背包内,背包最多可容纳的总重量是m,如何选择才能使物品的总价值最大,如果限定每种物品只能选择0个或者1个,那么该类问题被称为0-1背包问题。
int m=50;//背包最多可容纳总重量是50
int w[3]={10,20,30};//背包的重量
int p[3]={60,100,120};//背包的价值
int bagp,maxp;//当前背包价值和当前为止背包的最大价值
int bagw,maxw;//当前背包的重量和最大价值背包的重量
int r=280;//可用的物品价值
int maxValue[3],xValue[3];
void bag(int i)
{
if(i>2)//到了最后一层
{
if(bagp>maxp)//当前值大于最优价值可以被记录在里面
{
maxp=bagp;
for(int j=0;j<3;++j)
{
maxValue[j]=xValue[j];//记录最优值
}
}
return;
}
r=r-p[i];//剩余价值减少
if(bagw+w[i]<=m)
{
xValue[i]=1;//装进这个物品
bagw=bagw+w[i];
bagp=bagp+p[i];
bag(i+1);
bagw=bagw-w[i];
bagp=bagp-p[i];
}
if(bagp+r>maxp)//搜索最优 也就是右子树
{
xValue[i]=0;
bag(i+1);
}
r=r+p[i];//在递归函数结束的位置,设置在第i层搜索结束之后恢复r的值
}
int main()
{
bag(0);
for(int i=0;i<3;++i)
{
cout<<maxValue[i]<<endl;
}
return 0;
}
算法分析:由于背包问题与装载问题类似,都是有两个叉的一个代表有一个代表无的树,所以时间复杂度同装载问题都是
4.TSP(旅行商)问题:有若干个城市,任何两个城市之间的距离都是确定的,现要求某个旅行商从某个城市出发必须经过每个城市且只能在每个城市逗留一次,最后回到原出发城市,问如何事先确定好一条路线使其旅行的费用最少。
可运行代码:
#include<bits/stdc++.h>
using namespace std;
int n=4;//城市个数
int d[4][4]={0,2,6,7,2,0,4,4,6,4,0,2,7,4,2,0};//距离矩阵
string city[4]={"A","B","C","D"};//城市名称
string nowx[5],maxx[5];//当前路径,最优路径
int nowDistance,minDistance=50;//当前路径距离,最短路径距离
int cityCode(string c)
{
for(int i=0;i<4;++i)
{
if(city[i]==c)
return i;//返回城市的序号
}
}
//判断c是否出现在nowx[0~i-1]中
bool ok(string c,int i)
{
for(int j=0;j<i;++j)
{
if(c==nowx[j])//这条路径之前出现过这个城市
return false;
}
return true;
}
void tsp(int i)
{
if(i>2)//找到了可行解 这个数一般被设置为n-1
{
int t;
t=cityCode(nowx[2]);//获取最后一个编码
nowDistance=nowDistance+d[t][0];//加上最后那条回到原点的路径长度
if(nowDistance<minDistance)
{
minDistance=nowDistance;//修改最优解
for(int j=0;j<5;++j)
{
maxx[j]=nowx[j];//记录最优解
}
}
nowDistance=nowDistance-d[t][0];//记录之后将路径的距离减回
return;
}
for(int j=1;j<4;++j)
{
if(ok(city[j],i))
{
nowx[i]=city[j];
int t;
if(i==0)
t=0;
else t=cityCode(nowx[i-1]);
nowDistance=nowDistance+d[t][j];//从已知的解的上一个点到这个点的长度加上
tsp(i+1);
nowDistance=nowDistance-d[t][j];//回溯恢复
nowx[i]="";//回溯恢复
}
}
}
int main()
{
tsp(0);
cout<<"A"<<endl;
for(int j=0;j<3;++j)
cout<<maxx[j]<<endl;
cout<<"A"<<endl;
return 0;
}
运行结果:
算法分析:根据斯特林公式,对解空间树进行计算,O((n-1)!),如果有小伙伴想看详细推导,请在下方留言,我再补充哈~