首先引用下《算法竞赛入门经典(第二版)》的一句话:“回溯法是初学者学习暴力破解法的第一个障碍,学习时间短则数天,长则数月甚至一年以上”
所以我在讲回溯法之前先讲几个例子,然后层层深入(一定要耐下心,慢慢看哦)
1.枚举排列
枚举1-10的所有排列(按字典序从小到大的顺序)
肯定是递归了,但是要怎么做呢?
是不是聪明的你已经想到了,就是每次把一个数放到标记数组中,如果有相同的数字就返回,没有就递归,等满了打印出来就成了。
int a[100] = { 0 }; //标记数组,记录这个数有没有被访问
int n; //枚举个数(从1开始到n)
int sum[100]; //记录顺序
void dfs(int sn) //sn是记录到第几个数了
{
if(sn == n) //因为这个是排列问题,不像其他递归一样多了或少了就退出了,等他们相等就直接ok。
{
int i = 1;
for(;i <= n;i++)
printf("%d ",sum[i]);
printf("\n");
return;
}
int i = 1;
for(;i <= n;i++)
{
if(a[i] == 1)
continue;
a[i] = 1;
int j = 1;
for(;j <= n;j++)
{
if(sum[j] == 0)
{
sum[j] = i;
break;
}
}
dfs(sn + 1); //递归下一个数
sum[j] = 0; //这其实有回溯
a[i] = 0; //这个回溯
}
}
这个题比较简单,只要记住有枚举排列这个东西,递归的时候没有终止条件(因为数字多少固定,且都要出现),其他就不多说了。
2.子集生成
方法很多,我讲一个最简单的:子集肯定是有和没有两种情况,分别递归就好了
void dfs(int n,int b[10],int count)
{
if(count == n) //这时,所有的情况都有了
{
for(int i = 0;i < n;i++)
{
printf("%d",b[i]);
}
printf("\n");
return;
}
b[count] = 1;
dfs(n,b,count + 1);
b[count] = 0;
dfs(n,b,count + 1);
}
这个题,就是枚举所有情况,但其实对每个数来说,也就两种,有和没有。对有和没有分别递归就ok了。
下面入正题了,讲一下八皇后:
为了让咋们多思考思考,别一上来就直接知道最优方案也只知道这个方案,我还是把书上的话引用一下,这也是为什么我要将上面那个几个题的原因。你会一个题,那么你永远会的只是这个题,但是你如果学到了方法,你得到的,就是海洋!
引自《算法竞赛入门经典(第二版)》
“分析:
最简单的思路是吧问题转化为“从64个格子中选一个子集”,使得“子集中恰好有8个棋子,而且不同行,不同列,不同对角线”。这正是子集枚举问题,然而,64给格子子集有2^64个,太大了,这并不是一个很好的模型。
第二个思路是把问题转化为“从64个格子中选8个格子”,这时一个组合生成问题,根据组合数学,有c8 64 种方案,比第一种方案优秀,但仍不够好。”
思考后,发现每行只能放一个。第一行可以选8个位置,第二行7,以此类推:最多8!
还有1个问题,这个每个该怎么存,来为后面产生影响?
最简单的,建立一个8 * 8的二维数组,一个结点准备递归时,不能走的全部标记。。但是这个太麻烦。。
说说我的方法吧,我列了一个c[8]的数组,用来存竖行,因为每次是向下的,所以横行肯定不会打架。
斜着的怎么存呢?如图
就像蓝色的和红色的那样,因为有两个对角线,所以我建立了两个标记数组。
int sum = 0;
int c[8] = { 0 }; //列的标记数组
int d[15] = { 0 }; //标记数组1,就是我那个红色的
int ff[15] = { 0 }; //标记数组2,就是蓝色的
void dfs(int h) //h是行号
{ //排列嘛,我说过,基本不用判断终止条件
int i = 0;
for(;i < 8;i++)
{
int h1 = h;
int i1 = i;
int t;
while(1)
{
if(h1 == 0)
{t = i1;break;}
if(i1 == 0)
{t = h1 + 7;break;}
i1--;
h1--;
}
h1 = h;
i1 = i;
int tt;
while(1)
{
if(h1 == 0)
{tt = i1;break;}
if(i1 == 7)
{tt = h1 + i1;break;}
i1++;
h1--;
}
if(c[i] == 1)
continue;
if(d[t] == 1)
continue;
if(ff[tt] == 1)
continue;
if(h == 7)
{
sum++;
return;
}
c[i] = 1;
d[t] = 1;
ff[tt] = 1;
dfs(h + 1);
c[i] = 0; //下面都是回溯
d[t] = 0;
ff[tt] = 0;
}
return;
}
最后再讲一道题(这个我感觉比8皇后稍微难点)
在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C。
Input
输入含有多组测试数据。
每组数据的第一行是两个正整数,n k,用一个空格隔开,表示了将在一个n*n的矩阵内描述棋盘,以及摆放棋子的数目。 n <= 8 , k <= n
当为-1 -1时表示输入结束。
随后的n行描述了棋盘的形状:每行有n个字符,其中 # 表示棋盘区域, . 表示空白区域(数据保证不出现多余的空白行或者空白列)。
Output
对于每一组数据,给出一行输出,输出摆放的方案数目C (数据保证C<2^31)。
Sample Input
2 1 #. .# 4 4 ...# ..#. .#.. #... -1 -1
Sample Output
2 1
同理,也是递归,但是这个题有几个问题:
1.不是全排列,数目不知道
2.一行可能没有!(这个很关键)
思路和八皇后差不多,复杂度也差不多,从n个格子里边选出k个格子也是每行遍历,然后递归,然后,我上面写的第二个问题出现了,假设有2个棋子,4行,怎么让第一行和第4行有,2和3没有?
这个困扰了我很长时间,但是,还是想出来了,给后面再加一个递归!(请仔细看代码,上面的标注是详解)
#include "iostream"
#include "cstdio"
#include "cstdlib"
#include "cstring"
using namespace std;
int c[8][8]; //棋盘
int sum; //总数
int n; //每次大小
int k; //总共需要放多少棋子
int ss[8];
void dfs(int a,int m)
{
if(m == k) //这个一定要写在前面,因为万一越界了,这个其实还是好的
{
sum++;
return;
}
if(a == n) //越界就return
{
return;
}
//别小瞧下面这句话,就是这句话,我的运行时间从47s变到16s----》我下面详细讲原因
if((n - a + 1) < (k - m)) //就是说假设有3个棋子,但是只有2层,肯定也return。
{
//printf("(n - a + 1) < (k - m)");
return;
}
int i = 0;
for(i = 0;i < n;i++)
{
if(ss[i] == 1 || c[a][i]) //如果越界就走了
continue;
ss[i] = 1;
dfs(a + 1,m + 1);
ss[i] = 0;
}
//前方高能
dfs(a + 1,m); //这句话是整段代码的精髓,这样上面运行结束了,就可以运行这句话
//相当于跳过上面的一行,或者更上面的一行和这行结合,或者重新开始
return;
}
int main()
{
while(1)
{
memset(ss,0,sizeof(c));
memset(c,0,sizeof(c));
sum = 0;
scanf("%d%d",&n,&k);
if(n == -1 && k == -1)
break;
getchar();
int i,j;
for(i = 0;i < n;i++)
for(j = 0;j < n;j++)
{
char a = getchar();
while(a == ' ' || a == '\n')
a = getchar();
if(a == '#')
c[i][j] = 0;
if(a == '.')
c[i][j] = 1;
}
dfs(0,0);
printf("%d\n",sum);
}
return 0;
}
整句话的精髓那个我想你看懂了,但是我写的那行你可能没看懂,我下面来讲讲,一听就会。。
有一个东西叫解答树:(我用8皇后来举例)
(图画的太丑了~.~)
就是说这个树展示的是从什么都没有到逐步生成到生成完整解的过程
把一个当成一个结点,这个的最底层有二分一的叶子,倒数第二层也有好多好多,相对的,上面叶子的就相对少多了。
而我,正是通过那一步舍弃了很多很多的叶子,从而让时间变得很快。
是不是很神奇,试试呗^_^