回溯算法的浅析

本文介绍了回溯算法的基本概念,强调其作为暴力算法的特点,重点讲解了在解决组合问题和排列问题中的应用,以及如何通过优化减少无效递归,以提高效率。
摘要由CSDN通过智能技术生成

回溯算法的浅析

回溯算法的定义

回溯法,也称为回溯搜索法,它属于一种优先选择搜索的方法。在这种方法中,根据优先条件向前搜索,以达到特定目标。然而,当探索到某一步时,如果发现先前的选择并不是最优或无法达到目标,就会回溯到上一步重新选择。这种不断尝试并在失败时回溯的技术就是回溯法。在回溯法中,满足回溯条件的某个状态点称为“回溯点”。

「回溯是递归的副产品,只要有递归就会有回溯」

注意:回溯算法其实就是为暴力算法,并不能起到空间或时间上的优化,我们使用其的原因是因为某些问题只能通过回溯算法解决。

回溯算法的应用场景

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

回溯算法的代码模板

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

组合问题

力扣题目链接(opens new window)

给定两个整数 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循环,这显然是不现实的。

这就要有请回溯算法来解决这个问题了,虽然它也是暴力的算法,但是起码能够通过递归的方式来解决这些多层循环嵌套的问题。

我们将回溯算法解决的问题可以抽象为一个树状结构,如下:

image-20240322170053982

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开始的循环递归的过程就已经毫无意义。如下图所示:

77.组合4

为了减去这种情况,我们只需确定在变量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;
}
  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值