严格来说,搜索也是一种暴力枚举策略,传统的枚举需要固定for循环的层数,但是这样不能随意增减枚举层数,本文将介绍一种新的利用递归的方式枚举每个可能的选项,如果合法就继续下一个,如果所有选项都不合法就退回并尝试更换上一个的选项,继续枚举。这种方式就是回溯算法,常用深度优先搜索实现:
先来看一道模板题:
排列数字
思路:
想象有n个空位,需将n个数插入其中:_ _ _ _ _ _ _ _ _ _ _
我们用a表示空位,用step表示正在处理的空位,用book标记使用过的数(将book定义为全局,使用过的改成1,未使用的则默认为0)
很容易就能想到如下代码:
if(book[i]==0) // 即未使用过
{
a[step]==i;
book[i]==1;
}
那么下一个空位该如何处理呢?
首先我们将上面的代码封装成一个函数dfs:
void dfs(int step)
{
for(int i=i;i<=n;i++)// 空位上可选择的数为1到n
{
if(book[i]==0)
{
a[step]==i;
book[i]==1;
}
}
}
那么处理下一个空位的方法就是递归 dfs(step+1):
void dfs(int step)
{
for(int i=i;i<=n;i++)
{
if(book[i]==0)
{
a[step]==i;
book[i]==1;
dfs(step+1);
book[i]==0; // 这是重要的一步,收回已经使用的数也就是第step个数
}
}
}
book[i]=0非常重要,它的作用是收回刚才的数,因为当递归结束也即所有的空位都被占满时,我们就需要让出最后的空位来寻找新的排列方法,如果不回收这个数,那么就不能进行下一次的插入
递归的进入比较容易理解,但是递归的回溯是我们无法看到的。因此递归究竟是如何完成的,成为了理解递归的一大难点:
递归程序在回退完成前,return会使得计算机继续依次执行上一层函数调用后的代码,而返回上一层还能继续枚举的原因是计算机运行函数时为每个子函数都分配了专门存储递归函数信息的栈空间,其中包括了每层函数各个局部变量的值。
还有最后一个问题,就是什么时候该输出一个满足要求的序列呢?
答案就是当step==n+1时,此时前n的空位都已经填满,只需输出即可。
该函数如下
void dfs(int step)
{
if(step==n+1)
{
for(int i=1;i<=n;i++)
cout<<a[i]<<' ';
cout<<endl;
return; // 返回以前的一步
}
for(int i=i;i<=n;i++)
{
if(book[i]==0)
{
a[step]==i;
book[i]==1;
dfs(step+1);
book[i]==0;
}
}
}
完整代码如下
#include<iostream>
using namespace std;
const int N=100;
int a[N],book[N],n;
void dfs(int step)
{
if(step==n+1)
{
for(int i=1;i<=n;i++)
cout<<a[i]<<' ';
cout<<endl;
return;
}
for(int i=1;i<=n;i++)
{
if(book[i]==0)
{
a[step]=i;
book[i]=1;
dfs(step+1);
book[i]=0;
}
}
}
int main()
{
cin>>n;
dfs(1);
return 0;
}
由此可得深搜的基本模型:
void dfs(int step)
{
if(所有空被填完)
{
记录答案/判断最优解
return;
}
for(枚举选项)
if(合法)
{
记录现场
dfs(step+1);
恢复现场
}
}
下面再来看一道经典题目:
八皇后
思路:
考虑到每一行、列只能放一个,对于行,我们可以遍历,对于列,我们可以递归,最后判断是否在同一条斜线上即可,斜线的判断是本题的难点,考虑到它们的斜率都为1,所以不同的斜线截距必然不同,我们可以套用初中的直线方程:y=x+b → b=y-x(对角) y=-x+b→b=y+x(反对角)但是考虑到数组名不能为负,而且截距只是为了区分并无实际意义,所以对角的截距我们需要加上一个较大的数,代码:
#include<bits/stdc++.h>
using namespace std;
//column列 diagonal对角 antidiagonal反对角
//y=x+b;b=y-x dg
//y=-x+b;b=y+x adg
const int N=15;
int a[N][N];
int col[N],dg[N*2],adg[N*2];
int res,n;
void dfs(int u) // 从第u行开始
{
if(u==n+1)
{ if(res<3)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
if(a[i][j]==1)
cout<<j<<' ';
}
cout<<endl;
}
res++;
}
for(int i=1;i<=n;i++) //记录列
if(!col[i]&&!dg[i-u+n]&&!adg[u+i])
{
a[u][i]=1;col[i]=1;dg[i-u+n]=1;adg[u+i]=1;
dfs(u+1);
a[u][i]=0;col[i]=0;dg[i-u+n]=0;adg[u+i]=0;
}
}
int main()
{
cin>>n;
dfs(1);
cout<<res<<endl;
return 0;
}
另外再拓展一下:
直接上代码:
#include<iostream>
using namespace std;
const int N=100;
int a[N],book[N],res;
void dfs(int step)
{
if(step==10)
{
if(a[1]*100+a[2]*10+a[3]+a[4]*100+a[5]*10+a[6]==a[7]*100+a[8]*10+a[9])
{
printf("%d%d%d+%d%d%d=%d%d%d\n",a[1],a[2],a[3],a[4],a[5],a[6],a[7],a[8],a[9]);
res++;
return;
}
}
for(int i=1;i<10;i++)
{
if(book[i]==0)
{
a[step]=i;
book[i]=1;
dfs(step+1);
book[i]=0;
}
}
}
int main()
{
dfs(1);
cout<<res/2<<endl; //会出现a+b=c||b+a=c的重复情况
return 0;
}