回溯算法的浅析
回溯算法的定义
回溯法,也称为回溯搜索法,它属于一种优先选择搜索的方法。在这种方法中,根据优先条件向前搜索,以达到特定目标。然而,当探索到某一步时,如果发现先前的选择并不是最优或无法达到目标,就会回溯到上一步重新选择。这种不断尝试并在失败时回溯的技术就是回溯法。在回溯法中,满足回溯条件的某个状态点称为“回溯点”。
「回溯是递归的副产品,只要有递归就会有回溯」
注意:回溯算法其实就是为暴力算法,并不能起到空间或时间上的优化,我们使用其的原因是因为某些问题只能通过回溯算法解决。
回溯算法的应用场景
回溯法,一般可以解决如下几种问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
回溯算法的代码模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合问题
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
根据题意我们不难想到,解决该问题最简单的方法其实是使用for循环。
例如:
题目中的k=2,n=4时,用两个for循环即可解决
for(int i=0;i<=n;i++){
for(int j=i+1;j<=n;j++){
printf("%d %d",i,j);
}
}
如果k=4 ,n=100时,我们就用4个for循环
for(int i=0;i<=n;i++){
for(int j=i+1;j<=n;j++){
for(int b=j+1;b<=n;b++){
for(int m=b+1;m<=n;m++){
printf("%d %d %d %d",i,j,b,m);
}
}
}
}
通过以上例子我们不难看出,循环的次数是与k的值相同,那么如果当k=50时,我们就要写上一共整整50个for循环,这显然是不现实的。
这就要有请回溯算法来解决这个问题了,虽然它也是暴力的算法,但是起码能够通过递归的方式来解决这些多层循环嵌套的问题。
我们将回溯算法解决的问题可以抽象为一个树状结构,如下:
c语言实现的代码如下:
int pathtop;
int* path;
int anstop;
int** ans;
void dfs(int n,int k,int start){
if(pathtop==k){
//当path中的元素个数为k后,就将内容存储到数组中
int* temp = (int*)malloc(sizeof(int)*k);
for(int i=0;i<k;i++){
temp[i]=path[i];
}
ans[anstop++]=temp;
return;
}
else{
for(int i=start;i<=n;i++){
path[pathtop++]=i;
dfs(n,k,i+1);
pathtop--; //进行退回,便于其他元素的进入
}
}
}
int** combine(int n, int k, int* returnSize, int** returnColumnSizes) {
path = (int*)malloc(sizeof(int)*k);
ans = (int**)malloc(sizeof(int*)*5000);
pathtop=anstop=0;//在主函数内赋值
dfs(n,k,1);
*returnSize=anstop;//给行数复制
*returnColumnSizes=(int*)malloc(sizeof(int)*(*returnSize));
for(int i=0;i<anstop;i++){
(*returnColumnSizes)[i]=k;//将每行存储的个数设置为k
}
return ans;
}
组合问题的优化
前面我们说到,回溯算法是一种暴力算法,但是有时候仍然能进行剪枝的操作对算法进行一定程度上的优化。
拿n=4 k=4
来举例,我们可以一眼的看出,只有1 2 3 4
这种组合情况。但由于我们的代码内容如下:
for(int i=start;i<=n;i++){
path[pathtop++]=i;
dfs(n,k,i+1);
pathtop--;
}
即当我们读取到第一层for
循环之后,从2
开始的循环递归的过程就已经毫无意义。如下图所示:
为了减去这种情况,我们只需确定在变量start
后面的数字长度大于等于k
,即表达为j <= n- (k - pathTop) + 1
。
以下时修改后的代码:
int* path;
int pathTop;
int** ans;
int ansTop;
void backtracking(int n, int k,int startIndex) {
//当path中元素个数为k个时,我们需要将path数组放入ans二维数组中
if(pathTop == k) {
//path数组为我们动态申请,若直接将其地址放入二维数组,path数组中的值会随着我们回溯而逐渐变化
//因此创建新的数组存储path中的值
int* temp = (int*)malloc(sizeof(int) * k);
int i;
for(i = 0; i < k; i++) {
temp[i] = path[i];
}
ans[ansTop++] = temp;
return ;
}
int j;
for(j = startIndex; j <= n- (k - pathTop) + 1;j++) {
//将当前结点放入path数组
path[pathTop++] = j;
//进行递归
backtracking(n, k, j + 1);
//进行回溯,将数组最上层结点弹出
pathTop--;
}
}
int** combine(int n, int k, int* returnSize, int** returnColumnSizes){
//path数组存储符合条件的结果
path = (int*)malloc(sizeof(int) * k);
//ans二维数组存储符合条件的结果数组的集合。(数组足够大,避免极端情况)
ans = (int**)malloc(sizeof(int*) * 10000);
pathTop = ansTop = 0;
//回溯算法
backtracking(n, k, 1);
//最后的返回大小为ans数组大小
*returnSize = ansTop;
//returnColumnSizes数组存储ans二维数组对应下标中一维数组的长度(都为k)
*returnColumnSizes = (int*)malloc(sizeof(int) *(*returnSize));
int i;
for(i = 0; i < *returnSize; i++) {
(*returnColumnSizes)[i] = k;
}
//返回ans二维数组
return ans;
}