【数据结构与算法】带你玩透回溯法

看完你一定会有收获的,相信我!

回溯法

递归算法中非常经典的思想:回溯法。这样的算法思想通常都应用在一类问题上,这类问题叫做树型问题

用回溯算法解决问题的一般步骤:

1、 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。

2 、确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。

3 、以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。

简单来说,回溯法可以理解成为通过选择不同的岔路口,来寻找目的地,一个岔路口一个岔路口的去尝试找到目的地,如果走错了路的话,继续返回到上一个岔路口的另外一条路,直到找到目的地。

算法模板

可以按照3个步骤来思考这类的问题:

  1. 「路径」:记录做出的选择。
  2. 「选择列表/状态变量」:通常而言,用数组存储可以选择的操作,或用状态变量记录当前操作位置。
  3. 「结束条件」:一般而言,就是递归的结束点,也就是搜索的结束点。
result = []

function backtrack(路径, 选择列表) {
    if('满足结束条件') {
        // 这里就是对答案做更新,依据实际题目出发
        result.push(路径)
        return
    } else {
        for(let i = 0; i < 选择列表.length; i++) {
            // 对一个选择列表做相应的选择
            
            做选择
            
            backtrack(路径, 选择列表)
            
            // 既然是回溯算法,那么在一次分岔路做完选择后
            // 需要回退我们之前做的操作
            
            撤销选择
        }
    }
}

回溯算法题的解题思路

使用刻意练习的方法训练回溯算法:

1、画出递归树,找到状态变量。即函数中的变量。

2、找出递归出口。

3、找出选择列表 / 表达式

4、进行剪枝操作

5、根据选择,递归调用。

6、撤销选择

案例分析

字母大小写全排列

给定一个字符串S,通过将字符串S中的每个字母转变大小写,我们可以获得一个新的字符串。返回所有可能得到的字符串集合。

输入:S = “a1b2”

输出:[“a1b2”, “a1B2”, “A1b2”, “A1B2”]

输入:S = “3z4”

输出:[“3z4”, “3Z4”]

输入:S = “12345”

输出:[“12345”]

解题思路:

每个字母有状态,大写小写;对于数字,直接跳过。

  • 遇到数字,没有产生新分支,直接往后搜索。
  • 遇到字母,需要搜索两次,1、大写 2、小写
  • 递归出口:搜索到最后,则加入结果数组中。我们需要维护一个Index,指向当前的字符。

按照刻意练习的整理:

1、递归树,状态变量:index 和 当前合成字符串。

image-20210314142149172

2、递归出口:index === len

3、选择列表:数字直接往下搜索;字母则分大小写搜索两次

4、剪枝:暂时不需要

5、撤销操作:不用撤销。

var letterCasePermutation = function (S) {
  const result = [];
  var backfind = function (str, index) {
    if (index === S.length) {
      return result.push(str);
    }
    let current = S[index];
    if ((current >= 'A' && current <= 'Z') || (current >= 'a' && current <= "z")) {
      //字母
      let low = current.toLowerCase();
      let upper = current.toUpperCase();
      backfind(str + low, index + 1);
      backfind(str + upper, index + 1);
    } else {
      //数字 直接添加
      backfind(str + current, index + 1);
    }
  }
  backfind("", 0);
  return result;
};
子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: nums = [1,2,3]

输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]

解题思路:

每个数字有两种状态,选与不选。我们可以从头进行搜素,记录状态变量index和当前新数组。

当index === len,递归结束,存入结果数组中。

对每个数字进行两次操作:

1、选择该数字,进行搜索。

2、不选择该数字,进行搜索。

注意需要在选择后,撤销操作。

从刻意练习的思路捋一遍:

1、递归树,状态变量,当前数字下标、当前新数组。

2、递归出口:index===len

3、选择列表:每个数字要或不要

  • 要,数字存入新数组,进行搜索。
  • 不要,直接index+1搜索。

注意,选择数字后需要撤销操作。

4、剪枝:暂无。

5、撤销操作,pop

image-20210314143259514

var subsets = function (nums) {
  const result = [];
  var dfs = function (index, list) {
    if (index === nums.length) {
      return result.push(list.slice());
    }
    list.push(nums[index]);
    dfs(index + 1, list);
    //撤销操作
    list.pop();
    dfs(index + 1, list);//不加入当前数字。
  }
  dfs(0, []);
  return result;
};

以下是我在网上看到一套不错的回溯算法题集,如果你还在刻意找的话,可以看看这里。

类型题目链接
子集,组合子集子集 II组合组合总和组合总和 II
全排列全排列全排列 II字符串的全排列字母大小写全排列
搜索解数独单词搜索N皇后分割回文串二进制手表

附上每道题 使用刻意练习思路的题解:
子集2 题解
组合 题解
组合总和 题解
组合总和2 题解
全排列 题解
全排列2 题解
字符串的全排列 题解
分割回文串 题解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值