详解深度优先搜索与回溯

严格来说,搜索也是一种暴力枚举策略,传统的枚举需要固定for循环的层数,但是这样不能随意增减枚举层数,本文将介绍一种新的利用递归的方式枚举每个可能的选项,如果合法就继续下一个,如果所有选项都不合法就退回并尝试更换上一个的选项,继续枚举。这种方式就是回溯算法,常用深度优先搜索实现:

先来看一道模板题:

排列数字

给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。

现在,请你按照字典序将所有的排列方法输出。

输入格式:共一行,包含一个整数 n。

输出格式:按字典序输出所有排列方案,每个方案占一行。

数据范围:1≤n≤7

思路:

想象有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<

完整代码如下
 
#include
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<>n;
    dfs(1);
    return 0;
}

由此可得深搜的基本模型:

void dfs(int step)
{
	if(所有空被填完)
	{
		记录答案/判断最优解 
		return; 
	}
	for(枚举选项) 
		if(合法) 
		{
			记录现场 
			dfs(step+1);
			恢复现场 
		}
}

下面再来看一道经典题目:


八皇后

一个如下的6×6 的跳棋棋盘,有六个棋子被放置在棋盘上,使得每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。

详解深度优先搜索与回溯_第1张图片

上面的布局可以用序列 2 4 6 1 3 5 来描述,第 i 个数字表示在第 i 行的相应位置有一个棋子,如下:

行号1 2 3 4 5 6

列号 2 4 6 1 3 5

这只是棋子放置的一个解。请编一个程序找出所有棋子放置的解。
并把它们以上面的序列方法输出,解按字典顺序排列。
请输出前 3 个解。最后一行是解的总个数。

输入格式:一行一个正整数 n,表示棋盘是 n×n 大小的。

输出格式:前三行为前三个解,每个解的两个数字之间用一个空格隔开。第四行只有一个数字,表示解的总数。

 思路:

考虑到每一行、列只能放一个,对于行,我们可以遍历,对于列,我们可以递归,最后判断是否在同一条斜线上即可,斜线的判断是本题的难点,考虑到它们的斜率都为1,所以不同的斜线截距必然不同,我们可以套用初中的直线方程:y=x+b → b=y-x(对角)     y=-x+b→b=y+x(反对角)但是考虑到数组名不能为负,而且截距只是为了区分并无实际意义,所以对角的截距我们需要加上一个较大的数,代码:

#include
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<>n;
	dfs(1);
	cout<

另外再拓展一下:
_  _  _ + _  _  _ = _  _  _
将数字1到9填进空位,每个数只能用一次,问一共有几种组合?

直接上代码:


#include
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<

你可能感兴趣的:(深度优先,算法,c语言,c++,蓝桥杯)