回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
(1)针对所给问题,确定问题的解空间:首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。
(2)确定结点的扩展搜索规则
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
转化规则:其实八皇后问题可以推广为更一般的n皇后摆放问题:这时棋盘的大小变为n×n,而皇后个数也变成n。当且仅当 n = 1 或 n ≥ 4 时问题有解。令一个一维数组a[n]保存所得解,其中a[i] 表示把第i个皇后放在第i行的列数(注意i的值都是从0开始计算的),下面就八皇后问题的约束条件。
(1)因为所有的皇后都不能放在同一列,因此任意两个a[0].....a[7]的值不能存在相同的两个值。
(2)所有的皇后都不能在对角线上,那么该如何检测两个皇后是否在同一个对角线上?我们将棋盘的方格成一个二维数组,如下:
#include
#include
#include
#include
using namespace std;
//位置冲突算法
bool isMeet(int a[], int n)//a[]位置数组,n皇后个数
{
for (int i = 2; i <= n; ++i)//i:位置
for (int j = 1; j < i; ++j)//j:位置
if ((a[i] == a[j]) || (abs(a[i] - a[j]) == i - j))//1:在一行;2:在对角线上
return false; //冲突
return true;//不冲突
}
//八皇后:枚举算法
//主函数
int main()
{
int a[9] = { 0 }; //用于记录皇后位置:(第0行0列我们不用)。如:a[3] = 4;表示第3列第4行位置有皇后
int count = 0; //用于计数
for (a[1] = 1; a[1] <= 8; ++a[1])
for (a[2] = 1; a[2] <= 8; ++a[2])
for (a[3] = 1; a[3] <= 8; ++a[3])
for (a[4] = 1; a[4] <= 8; ++a[4])
for (a[5] = 1; a[5] <= 8; ++a[5])
for (a[6] = 1; a[6] <= 8; ++a[6])
for (a[7] = 1; a[7] <= 8; ++a[7])
for (a[8] = 1; a[8] <= 8; ++a[8])
{
if (!isMeet(a, 8))//如果冲突,则继续枚举
continue;
else
++count;
}
cout << count << endl;
system("pause");
return 0;
}
见leecode,
#include
#include
#include
#include
using namespace std;
int a[9] = { 0 };
int n = 8, cnt = 0;
//位置冲突算法
bool isConflict(int a[], int n)//a[]位置数组,n皇后个数
{
int i = 0, j = 0;
for (i = 2; i <= n; ++i)//i:位置
for (j = 1; j <= i - 1; ++j)//j:位置
if ((a[i] == a[j]) || (abs(a[i] - a[j]) == i - j))//1:在一行;2:在对角线上
return false; //冲突
return true;//不冲突
}
//八皇后问题:回溯算法(递归版)
void Queens8(int k) //参数k:递归摆放第k个皇后
{
int i = 0;
if (k > n) //k>n:即k>8表示最后一个皇后摆放完毕
{
printf("第%d种情况:", ++cnt);
for (i = 1; i <= n; ++i)
printf("%d ", a[i]);//打印情况
printf("\n");
}
else //8个皇后未全部摆放完毕
{
for (i = 1; i <= n; ++i)//摆放第k个皇后时(转下一行)
{ //依次从列顶端开始搜索,一直到列底端,直到找到合适位置,如果未找到,自动返回上层递归(回溯)
a[k] = i;
if (isConflict(a, k))
Queens8(k + 1);//不冲突,递归摆放下一个皇后
}
}
return;
}
//主函数
int main()
{
Queens8(1);//参数1:表示摆放第1个皇后
system("pause");
return 0;
}
在leetcode中,那些要求列举所有情况,或者说所有情况都要探讨一下的的例题,一般都可以考虑回溯法。
当遇到一个可以用到回溯法的时候需要按照如下步骤进行:
1. 确定问题的一个解空间树, 这个解空间树至少包含一个你需要的那个解, 否则这个树就完全没有意义了
2. 组织好这棵树, 弄明白这棵树的每一个节点代表什么, 每一个分支代表什么
3. 从这棵树的根节点不断的向下深搜, 当遇到不合适的节点的时候直接跳过以这个节点为根的子树
4. 当搜索到了叶子节点的时候就回溯
5. 不断的重复这个3, 4步骤
附加: 根据具体的问题可以定义限界条件, 最优值条件, 根据这两个条件可以剪枝了
以下是在leetcode中收集的典型例子。
39题,翻译题目:
给定一组候选集(C)和一个目标值T,在C的所有组合中,找出所有总和等于T的组合。
候选数组C中同一个数可以被选择多次(不限次数)。分析:
典型的回溯法应用。
对数组里面的每个数,用递归的方式相加,每次递归将和sum与target作比较,若相等则加入结果vector,sum>target则舍弃,并返回false,若sum
第一种sum=target的情况下,在加入结果vector后回溯(此时不应再累加),要将当前一种结果最后加入的元素pop_back(),并继续对后面的元素进行递归;
第二种sum>target的情况下,则需要将当前结果的最后加入的元素pop_back(),并继续对后面的元素进行递归。
第三种sum
注意元素可以重复,所以下一次递归总是从当前递归元素开始。
40题,翻译题目:
给定一组候选集(C)和一个目标值T,在C的所有组合中,找出所有总和等于T的组合。
候选数组C中每个数字只能使用一次。
分析:
典型的回溯法应用。
这道题跟上一道题基本一模一样,唯一区别就是每个数只能用一次,因此代码上只需要改一点点就行,即下一层递归
不能再从当前数开始,而要从下一个数开始了。
41题,翻译题目:
找到有k个数组成的所有可能组合,加起来等于数n。
k个数取值1到9,每个数只能使用一次。确保在集合中的数字按顺序排列。
注:本博文为EbowTang原创,后续可能继续更新本文。如果转载,请务必复制本条信息!
原文地址:http://blog.csdn.net/ebowtang/article/details/38145433
原作者博客:http://blog.csdn.net/ebowtang
参考资源:
【1】写的很不错,很清楚,http://www.2cto.com/kf/201405/302318.html