1、概念
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
2、基本思想
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
3、用回溯法解题的一般步骤:
(1)针对所给问题,确定问题的解空间:
首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。
(2)确定结点的扩展搜索规则
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
4、算法框架
(1)问题框架
设问题的解是一个n维向量(a1,a2,………,an),约束条件是ai(i=1,2,3,…..,n)之间满足某种条件,记为f(ai)。
(3)递归的算法框架
回溯法是对解空间的深度优先搜索,在一般情况下使用递归函数来实现回溯法比较简单,其中i为搜索的深度,框架如下:
1: int a[n];
2: try(int i)
{
if(i>n) 输出结果;
else
{ for(j = 下界; j <= 上界; j=j+1) // 枚举i所有可能的路径
{ if( fun(j) ) // 满足限界函数和约束条件
{ a[i] = j;
… // 其他操作
try(i+1);
回溯前的清理工作(如a[i]置空值等);
}
}
}
}
(2)非递归回溯框架
1:int a[n],i;
2: 初始化数组a[];
3: i = 1;
4: while (i>0(有路可走) and (未达到目标)) // 还未回溯到头
5: {
6: if(i > n) // 搜索到叶结点
7: {
8: 搜索到一个解,输出;
9: }
10: else // 处理第i个元素
11: {
12: a[i]第一个可能的值;
13: while(a[i]在不满足约束条件且在搜索空间内)
14: {
15: a[i]下一个可能的值;
16: }
17: if(a[i]在搜索空间内)
18: {
19: 标识占用的资源;
20: i = i+1; // 扩展下一个结点
21: }
22: else
23: {
24: 清理所占的状态空间; // 回溯
25: i = i –1;
26: }
27: }
回溯法实战训练
一、递归复习
1、学生年龄
2、1*2*3*…*n
3、fibonacci数列的第n项。(1,1,2,3,5,8,11……)
4、汉诺塔问题
递归三要素: 自己调用自己,调用自已时问题规模会变小,有递归终止条件!!
递归关键技术: 找出递推公式。
【蟠桃记】
Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 14729 Accepted Submission(s): 11299
【Problem Description】
喜欢西游记的同学肯定都知道悟空偷吃蟠桃的故事,你们一定都觉得这猴子太闹腾了,其实你们是有所不知:悟空是在研究一个数学问题!
什么问题?他研究的问题是蟠桃一共有多少个!
不过,到最后,他还是没能解决这个难题,呵呵^-^
当时的情况是这样的:
第一天悟空吃掉桃子总数一半多一个,第二天又将剩下的桃子吃掉一半多一个,以后每天吃掉前一天剩下的一半多一个,到第n天准备吃的时候只剩下一个桃子。
聪明的你,请帮悟空算一下,他第一天开始吃的时候桃子一共有多少个呢?
【Input】
输入数据有多组,每组占一行,包含一个正整数n(1
/*
第一天悟空吃掉桃子总数一半多一个,第二天又将剩下的桃子吃掉一半多一个,
以后每天吃掉前一天剩下的一半多一个,到第n天准备吃的时候只剩下一个桃子。
聪明的你,请帮悟空算一下,他第一天开始吃的时候桃子一共有多少个呢?
【Input】
输入数据有多组,每组占一行,包含一个正整数n(1<n<30),表示只剩下一个桃子的时候是在第n天发生的。
【Output】
对于每组输入数据,输出第一天开始吃的时候桃子的总数,每个测试实例占一行。
【Sample Input】
2
4
【Sample Output】
4
22
处理递归一个非常重要的地方,
就是设置合理的退出递归的条件(递归终止条件),
明显最后一天是一个归触发条件,因为最后一天只有一个,
那么设最后一天: n=1 ,昨天,n=2,前天:n=3,以此类推。
则 f(n)=( f(n-1)+1 )*2
*/
#include<stdio.h>
int peach(int n)
{
if(n==1) return 1;
else return (peach(n-1)+1)*2;
}
int main()
{
int n;
while(scanf("%d", &n)!=EOF)
printf("%d\n", peach(n));
return 0;
}
二、递归回溯
【全排列】
输入整数n, 生成整数1~n的全排列,按字典序输出.(假设n<=10)
【测试数据:】
输入:
3
输出:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
/* 生成整数1~n的全排列,按字典序输出.(假设n<=10) 测试数据: 输入: 3 输出: 1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1 */
#include<stdio.h>
void printf_permutation(int n, int a[], int cur) //n个数的全排列,存在a中,正要写a[cur]在值
{ //a[0...cur-1]已经填好。
int i,j, ok=1;
if(cur==n){ //a[]中数据填满,则输出.
for(i=0; i<n ;i++) printf("%d ", a[i]);
printf("\n");
}
else
for(i=1; i<=n; i++) //从1~n依次测试它是否能填到a[cur]中
{
ok=1; //标志变量,表达整数i是否已经在前缀序列中了
for(j=0; j<cur; j++)
if(a[j]==i) ok=0;
if(ok) //整数i没有在前缀序列中
{
a[cur]=i; //把整数i加入前缀序列
printf_permutation(n, a, cur+1); //递归,向后测试,去填a[cur+1]在值
}
}
}
int main()
{
int n,a[10]={0};
scanf("%d",&n);
printf_permutation(n,a,0); //递归初始状态, 准备填a[0]的值
return 0;
}
/* 生成整数1~n的全排列,按字典序输出.(假设n<=10) 测试数据: 输入: 3 输出: 1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1 */
#include<stdio.h>
int a[10]={0}; //装答案的数组设置为全局变量
void P(int n, int cur) //n个数的全排列,存在a中,正要写a[cur]在值
{ //a[0...cur-1]已经填好。
int i,j, ok=1;
if(cur==n){ //a[]中数据填满,则输出.
for(i=0; i<n ;i++) printf("%d ", a[i]);
printf("\n");
}
else
for(i=1; i<=n; i++) //从1~n依次测试它是否能填到a[cur]中
{
ok=1; //标志变量,表达整数i是否已经在前缀序列中了
for(j=0; j<cur; j++)
if(a[j]==i) ok=0;
if(ok) //整数i没有在前缀序列中
{
a[cur]=i; //把整数i加入前缀序列
P(n, cur+1); //递归,向后测试,去填a[cur+1]在值
}
}
}
int main()
{
int n;
scanf("%d",&n);
P(n,0); //递归初始状态, 准备填a[0]的值
return 0;
}
【求幂集】
求含有n个元素的集合的幂集。(数据结构书,第6.7节 回溯法与树的遍历)
如:A={1,2,3},则A的幂集是:
p(A)={{1,2,3},{1,2},{1,3},{1},{2,3},{2},{3},{空集}};
【测试数据:】
输入:
3
1 2 3
输出:
{ 1 2 3 } // 左括号后面有一个空格,数据后面有一个空格
{ 1 2 }
{ 1 3}
{ 1 }
{ 2 3 }
{ 2 }
{ 3 }
{ }
/* 求含有n个元素的集合的幂集。(数据结构书,第6.7节 回溯法与树的遍历) 如:A={1,2,3},则A的幂集是: p(A)={{1,2,3},{1,2},{1,3},{1},{2,3},{2},{3},{空集}}; */
#include<stdio.h>
void Get_PowerSet( int n, int a[], int cur, int r[])
{ //集合A中n个元素存放在a[0...n-1]中,r[i]记录a[i]是否在子集中,
// 目前已经确定a[0...cur-1]是否加入,正要决定a[cur]加入否
int i;
if(cur==n){ //已经有n个数确定是否加入 ,则输出该子集
printf("{ ");
for(i=0; i<n; i++)
if(r[i]) printf("%d ", a[i]);
printf("}");
printf("\n");
}
else{
r[cur]=1; //确定a[cur]加入
Get_PowerSet(n,a,cur+1,r); // 确定下一个元素是否加入子集中
r[cur]=0; //确定a[cur]不加入
Get_PowerSet(n,a,cur+1,r);
}
}
int main()
{
int n, a[20],r[20]={0}, i;
scanf("%d", &n);
for(i=0; i<n; i++)
scanf("%d", &a[i]);
Get_PowerSet(n, a, 0, r); // 递归初始,n个元素的集合a,准备确定第0个元素是否加入
return 0;
}
【素数环】
输入正整数n,把整数1,2,3,…,n组成一个环,使得相邻两个整数之和均为素数。输出时从整数1开始排列。同一个环应恰好输出一次。n<=16 。
【测试数据】
输入:
6
输出:
1 4 3 2 5 6
1 6 5 2 3 4
【注意】如果用枚举法,n=16时,有16!=2*10(13)个排列,会超时。试试回溯法。
/* 【素数环】 输入正整数n,把整数1,2,3,…,n组成一个环,使得相邻两个整数之和均为素数。 输出时从整数1开始排列。同一个环应恰好输出一次。n<=16 。 【测试数据】 输入: 6 输出: 1 4 3 2 5 6 1 6 5 2 3 4 用回溯法解决 */
#include<stdio.h>
#include<math.h>
#include<memory.h>
int IsPrime(int x) //若x为素数返回1,否则返回0
{
int i,k;
k=sqrt(x);
for(i=2; i<=k; i++)
if(x%i==0) return 0;
return 1;
}
void PrimeCircle(int n, int a[], int cur, int used[])
{
//a[]装答案数据, cur表示当前正要装a[cur]的数据,
//used为标志数组, used[i]值为0或1, 表示整数i有没有被用过
int i;
if( cur==n+1 && IsPrime(a[n]+a[0]) )
{
for(i=1; i<=n; i++) printf("%d ", a[i]);
printf("\n");
}
else
for(i=2; i<=n; i++)
if( !used[i] && IsPrime(a[cur-1]+i) ) //整数i没有用过,且和为素数
{
a[cur]=i; //指定第cur个数值为i
used[i]=1; //标记整数i已经用过
PrimeCircle(n, a, cur+1, used); //继续向后探测
used[i]=0; //准备测试另一个分支(第cur个数不为i为i+1的情况)
}
}
int main()
{
int a[20]={1,1}, used[20]={0}; //数组0号单元不用。 a[1]第1个数默认为1,
int n;
scanf("%d", &n);
PrimeCircle(n, a, 2, used); //从第2个位置开始填数
return 0;
}
【n皇后问题】 (数据结构书第6.7节 有4皇后问题的例子)
【问题描述:】
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯•贝瑟尔于1848年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。计算机发明后,有多种方法可以解决此问题。
这是来源于国际象棋的一个问题。皇后可以沿着纵横和两条斜线4个方向相互捕捉。求出在一个n×n的棋盘上,放置n个不能互相捕捉的国际象棋“皇后”的所有布局。
/* 八皇后 */
#include<stdio.h>
int A[8][8];
int hy[8];
int liey[8];
int zudjy[2*8-1];
int fudjy[2*8-1];
void Output(int a[][8], int n)
{
int i,j;
printf("\n--------------------------------\n");
for(i=0; i<n ;i++)
{ for(j=0; j<n; j++)
printf("%4d", a[i][j]);
printf("\n");
}
}
int Peace(int a[][8], int n)
{ int i;
for(i=0; i<n; i++)
if(hy[i]>1||liey[i]>1) return 0;
for(i=0; i<2*n-1; i++)
if(zudjy[i]>1||fudjy[i]>1) return 0;
return 1;
}
void Queen(int i, int n)
{ int j;
if(i>n) Output(A,n);
else{
for(j=1; j<=n; j++)
{
A[i-1][j-1]=1;
hy[i-1]++; liey[j-1]++; zudjy[i-j+n-1]++; fudjy[i+j]++;
if( Peace(A,n)) Queen(i+1, n);
A[i-1][j-1]=0;
hy[i-1]--; liey[j-1]--; zudjy[i-j+n-1]--; fudjy[i+j]--;
}
}
}
int main()
{
Queen(1,8);
return 0;
}