回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。 ----摘自百科
我的理解就是:
采用试探性的搜索原则,按优先条件向前进发,
能进则进,无路则退,退而再另辟蹊径,
直至得到所有有效的结果集,
也可能无解。
回溯法通常使用递归方式实现,配合恰当的剪枝对复杂度优化有奇效。
回溯法经典例题就是皇后问题,但是这里我们不以它为例,我们重点讨论如何有效的剪枝,通过编程求解下面四道排列组合题,希望大家能有所体会。
剪枝策略就是在搜索过程中利用过滤条件来剪去完全不用考虑(已经判断这条路走下去得不到最优解)的搜索路径,从而避免了一些不必要的搜索,大大优化了算法求解速度,还保证了结果的正确性。
应用到回溯算法中,我们就可以提前判断当前路径是否能产生结果集,如果否,就可以提前回溯。而这也叫做可行性剪枝(本文重点讨论)。
另外还有一种叫做最优性剪枝,每次记录当前得到的最优值,如果当前结点已经无法产生比当前最优解更优的解时,可以提前回溯,eg:分支限界算法。
Case 1:对数组1到n(无重复数)求全排列。
#include
#define NMAX 3 //取N=3
bool vis[NMAX+1]; //默认false
int arr[NMAX+1];
int count =0;
void backt(int t){ //回溯 t为层数(数的个数)
if(t>NMAX){ //达到N个数 一种结果
for(int j = 1; j <= NMAX ; j++)
printf("%d ",arr[j]);
count++;
printf("\n");
return;
}
for(int i = 1; i <= NMAX; i++){
if(!vis[i]){
arr[t]=i;
vis[i]=1;
backt(t+1); //纯回溯 无需剪枝优化
vis[i]=0;
}
}
}
int main(){
backt(1); //从一个数开始搜索到n个数
printf("当n = %d时,排列方式(如上)有 %d 种。\n",NMAX,count);
return 0;
}
Case 2: 对数组1到n(无重复数)求其中k个数组成的所有组合。(k<=n)
#include
#define NMAX 5 //取N=5
#define KMAX 2 //取K=2
int a[NMAX+1];
int arr[NMAX+1];
int count =0;
void backt(int s,int t){ //s为数起点 t为层数
if(t>KMAX){ //达到K个数 一种结果
for(int j=1;j<=KMAX;j++)
printf("%d ",arr[j]);
count++;
printf("\n");
return;
}
for(int i=s;i<=NMAX;i++){ //i=s !!!
arr[t]=i;
backt(i+1,t+1); //递归i,不允许回退
}
}
int main(){
backt(1,1);
printf("当n=%d,k=%d时,所有组合(如上)有%d种。\n",NMAX,KMAX,count);
return 0;
}
Case 3: 求有重复数的数组所有排列
#include
#include
#define NMAX 4 //N=4
bool vis[NMAX + 1];
int arr[NMAX + 1];
int count = 0;
void backt(int a[], int t) {
if (t > NMAX) { //达到N个数 一种结果
for (int j = 1; j <= NMAX; j++)
printf("%d ", arr[j]);
count++;
printf("\n");
return;
}
for (int i = 1; i <= NMAX; i++) {
if (!vis[i]) {
if (i != 1 && !vis[i - 1] && a[i] == a[i - 1]) { //数组a已排序 剪枝
continue; //return 会破坏vis
}
arr[t] = a[i];
vis[i] = 1;
backt(a, t + 1);
vis[i] = 0;
}
}
}
int main() {
int a[NMAX + 1] = {0, 1, 2, 2, 1}; //这里我是从a[1]开始的
std::sort(a + 1, a + 5); //方便剪枝 保证字典序
backt(a, 1);
printf("对数组1 2 2 1进行全排列(如上)");
printf("有 %d 种\n", count);
return 0;
}
Case 4: 对重复数的数组求其中k个数组成的所有组合。(k<=n)
#include
#include
#define NMAX 7 //N=7
#define KMAX 3 //K=3
bool vis[NMAX + 1];
int arr[NMAX + 1];
int count = 0;
void backt(int a[], int s, int t) {
if (t > KMAX) {
for (int j = 1; j <= KMAX; j++)
printf("%d ", arr[j]);
count++;
printf("\n");
return;
}
for (int i = s; i <= NMAX; i++) {
if (i > s && a[i] == a[i - 1]) { //去除非头重
continue;
}
arr[t] = a[i];
backt(a, i + 1, t + 1); //递归i,不允许回退
}
}
int main() {
int a[NMAX + 1] = {0, 1, 1, 2, 1, 2, 3, 4};
std::sort(a + 1, a + NMAX); //方便剪枝 保证字典序
backt(a, 1, 1);
printf("对数组1 1 2 1 2 3 4,求%d个数组成的所有组合(如上)", KMAX);
printf("有 %d 种\n", count);
return 0;
}
四道题码完了,至于不懂为什么这样剪枝的同学,希望大家可以自己动笔画画。模拟一下过程就很好懂了。
我当时看到这四道题时就懵了,查了网上资料大都是用字典序算法。但是当时我只学过回溯。所以我决定自己想,花了大把时间终于自己整出来了。整个过程也让我体会到的就是剪枝的过滤条件不好找。
想通过剪枝优化来提高算法高效性,又要保证结果正确性,还要保证剪枝的准确性。是非常难得的。而这也是这个剪枝优化算法的好坏评判标准。往往剪得不当,就会事而其反,得不偿失.......
所以加油吧,孰能生巧,大家可以看到上面四个问题是层层递进的。而我们要做的就是在基础上优化再优化....... 同样,对于随便一个问题也是如此,先暴力求解,然后在此基础上对问题分析,找寻更优。
Over~