回溯算法:全排列问题
前言
在学习C++时,我们一定会学到回溯算法。但回溯算法的细节方面,总是不那么容易掌握。这篇文章,就是为各位扫盲的。相信大家一定会得到些许帮助。
一、回溯是什么?
回溯,顾名思义,就是“一条路走到黑”,直到“撞破南墙”才回头一种类似于枚举的算法。不断试探,直到发现此方案不可行就尝试其它的方案。
二、典题:组合的输出
1.题目
排列与组合是常用的数学方法,其中组合就是从n个元素中抽出r个元素(不分顺序且r≤n),我们可以简单地将n个元素理解为自然数1,2,…,n,从中任取r个数。
现要求你用递归的方法输出所有组合。例如n=5,r=3,所有组合为:
1 2 3 ,1 2 4 ,1 2 5,1 3 4,1 3 5,1 4 5,2 3 4,2 3 5,2 4 5,3 4 5
2.分析
可以把这颗搜索树理解成一棵r叉树。如下图(图有省略):
而递归程序所做的,就是将本身与上面的每层的每个节点对应起来。如下图:
如何用程序来表达呢?如下(会做解释):
#include <bits/stdc++.h>
using namespace std;
int r,n,a[20];
bool init(int n){//判断元素n是否在数组a[]里
for(int i = 0;i < r;i++) if(a[i] == n) return 1;
return 0;
}
void print(){//
for(int i = 0;i < r;i++) cout<<a[i]<<" ";
cout<<endl;
}
void fill(int t){//已经填了t个元素
if(t == r){//填完了
print();
return;
}
int b[20];
for(int i = 0;i < 20;i++) b[i] = a[i];//备份
for(int i = 1;i <= n;i++){
if(!init(i)){//每一个元素需且只出现一次
a[t] = i;fill(t + 1);//将i填至a[t],然后尝试已经填t+1个元素的情况
}
}
for(int i = 0;i < 20;i++) a[i] = b[i];//恢复现场
}
int main(){
cin>>n>>r;
fill(0);//最开始一个元素都没填,所以是0
return 0;
}
6.19更:由此我们可以得出一个回溯程序框架(在上面也同样适用):
void dfs(int t){
if(满足成功的条件){
打印方案;
return;
}else{
for(遍历尝试的范围){
temp = 以前的方案;
if(当前的方案可以){
将它填进去;
dfs(t + 1);
}
方案 = temp;
}
}
3.注意点
相信大家看到这里,都知道回溯的原理。但有一个细节,大家可能没注意。我拿吃饭举例:
小明一天吃早餐,午餐,晚餐三餐。
早餐可吃:皮蛋瘦肉粥、油条配烧饼、肉包子。
午餐可吃:杂酱面,抄手,可乐鸡饭。
晚餐可吃:牛排,牛百叶汤、牛肉面。
现在对小明一天吃的东西进行暴力搜索,那么我们应:A.把早餐吃的东西换来换去。B.先固定早餐,然后依次选。
答案为B。我们显然需要“给早餐一个机会”。切忌只变动第一项,却不见后面的动静。希望大家注意。