回溯的基本原理:
在问题的解空间中,按深度优先遍历策略,从根节点出发搜索解空间树。算法搜索至解空间的任意一个节点时,先判断该节点是否包含问题的解。如果肯定不包含,跳过对以该节点为根的子树的搜索,逐层向其祖先节点回溯,否则进入该子树,继续深度优先搜索。
回溯法解问题的所有解时,必须回溯到根节点,且根节点的所有子树都被搜索后才结束。回溯法解问题的一个解时,只要搜索到问题的一个解就可结束。
回溯的基本步骤:
1.定义问题的解空间
2.确定易于搜索的解空间结构
3.以深度优先搜索的策略搜索解空间,并在搜索过程中用剪枝函数避免无效搜索
典型的解空间:
1.子集树:从n个元素的集合S中找到S满足某种性质的子集,解空间是二叉树结构
2.排列树:确定n个元素满足某种性质的排列,解空间是n叉树
提高回溯法的效率:
1.约束函数:减去不满足约束的子树
2.限界函数:剪去得不到最优解的子树
回溯的基本结构:递归、非递归迭代
回溯的基本框架:
1.回溯法搜索子集树
void backtrack(int i){
{
if(i > n) output(x);
else{
for(int i=0; i<=1; i++){
x[t] = i;
if(constraint(x[t]) && bound(x)) backtrack(i+1);
}
}
}
2.回溯法搜索排列树
void backtrack(int t){
if(t > n) output(t);
else{
for(int i=t; i<=n; i++){
swap(x[t],x[i]);
if(constraint(t) && bound(t)) backtrack(i+1);
swap(x[t],x[i]);
}
}
}
回溯的几个应用:
1.n皇后问题
//n皇后问题
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#define NUM 8
//判断第t个皇后是否与1-(t-1)个皇后冲突
bool place(int x[NUM], int t){
int k = 0;
for(int k=1; k<t; k++){
if(x[k] == x[t] || abs(t-k) == abs(x[t]-x[k])){
return false;
}
}
return true;
}
//状态空间是:第1个皇后在第1行的所有列,第2个皇后在第2行的所有列。。。
void back(int i, int x[NUM]){
int j = 1;
if(i > NUM){
for(j=1; j<=NUM; j++){
printf(“%d “,x[j]);
}
printf(“\n”);
return;
}
for(j=1; j<=NUM; j++){
x[i] = j;
if(place(x,i)){
back(i+1,x);
}
}
}
int main(){
int x[NUM];
back(1,x);
system(“pause”);
return 0;
}
2.装载问题
//装载问题
#include<stdio.h>
#include<stdlib.h>
int cw = 0;
int bestcw = 0;
int total = 0;
int n;
int w[10];
void backtrack(int i){
if(i > n){
if(bestcw < cw){
bestcw = cw;
}
return;
}
//遍历左子树,1,包含w[i]
if(w[i] + cw <= total){
cw += w[i];
backtrack(i+1);
cw -= w[i];
}
//遍历右子树,0,不包含w[i]
//cw不变
backtrack(i+1);
}
int main(){
int i;
scanf(“%d”,&total);
scanf(“%d”,&n);
for(i=1; i<=n; i++){
scanf(“%d”,&w[i]);
}
backtrack(1);
printf(“%d\n”,bestcw);
system(“pause”);
return 0;
}
3.排列问题
//求1-n的排列
#include<stdio.h>
#include<stdlib.h>
#define NUM 4
int x[NUM];
void swap(int &a, int &b){
int c = a;
a = b;
b = c;
}
//回溯法遍历排列树,同时分治
void perm(int i){
int j;
if(i > NUM){
for(j=1; j<=NUM; j++){
printf(“%d “,x[j]);
}
printf(“\n”);
return;
}
for(j=i; j<=NUM; j++){
swap(x[i],x[j]);
perm(i+1);
swap(x[i],x[j]); //还原
}
}
int main(){
int i=1;
for(i=1; i<=NUM; i++){
x[i] = i;
}
perm(1);
system(“pause”);
return 0;
}
回溯法心得:
1、全局变量在回溯中,注意在backtrack(i+1)返回时,修改的恢复;若将变量或者数组放在数组中传递时,在backtrack(i+1)返回时,栈会恢复,不用自行恢复。
2、递归、回溯的思考过程中,思维不要太细,要抽象,否则很容易混乱
3、步骤、框架、模型,成体系
4、回溯的效率是o(2^n)或者o(n!),虽然回溯较通用,但是效率较低,尽可能剪枝,以提高效率。非递归的回溯能减少递归的o(n)的栈,但是比较难写。