回溯算法详解

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

  • 组合问题:N个数里面按一定规则找出k个数的集合

  • 切割问题:一个字符串按一定规则有几种切割方式

  • 子集问题:一个N个数的集合里有多少符合条件的子集

  • 排列问题:N个数按一定规则全排列,有几种排列方式

  • 棋盘问题:N皇后,解数独等等

因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度

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

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

回溯法四部曲(空参治国)

1、定义解空间(路径和结果)

2 、回溯函数的返回值以及参数

3、  回溯函数终止条件(一般在此加入result,且是array化的数组,终止条件多于path.length相关)

4、 单层搜索的过程(一般在上下是相同的结构,path.push  backTracing()  path.pop)

解题小技巧

  1. 组合问题
    1. 可以用startIndex表示开始下标,只要每个下标都有过那么就全部组合过了
    2. 分割字符串根组合问题同用startIndex解决,思路殊途同归。(终止条件用startIndex)
  2. 子集问题
    1. 唯一不同与组合就是,其是在叶子节点收集
  3. 排列问题
    1. 需要额外数组标识是否被占用

组合问题:N个数里面按一定规则找出k个数的集合

力扣

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

//  1. 确定解空间
let result = []
let step = []
//2. 确定参数  n遍历子元素的数量,k能遍历的深度
function backTracing(n, k, startindex){
    //  2. 确定回溯终止条件,这里是数组长度,确定是有一个解
    if (step.length === k) {
         result.push(Array.from(step))
        return
    }
    for (let i = startindex; i <= n; i++) {
        // 3. 确定处理节点
        step.push(i)
        backTracing(n, k, i + 1)
        step.pop()
    }
}



var combine = function (n, k) {
     result = []
     step = []
    backTracing(n, k, 1)
    return result
};

总结:

        回溯法首先要考虑树状结构,对于此类的组合问题,

  1. 注重i=startIndex这个技巧
  2. 注重终止条件与,step.length的关系
  3. 具体操作中回溯上下操作一致
  4. 回溯的startIndex=i+1

相关练习

力扣


排列问题:N个数按一定规则全排列,有几种排列方式

力扣

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:

输入:nums = [1]
输出:[[1]]

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function(nums) {
    const res = [], path = [];
    backtracking(nums, nums.length, []);
    return res;
    
    function backtracking(n, k, used) {
        if(path.length === k) {
            res.push(Array.from(path));
            return;
        }
        for (let i = 0; i < k; i++ ) {
            if(used[i]) continue;
            path.push(n[i]);
            used[i] = true; // 同支
            backtracking(n, k, used);
            path.pop();
            used[i] = false;
        }
    }
};

总结:

 排列问题与组合问题最大的区别是元素有顺序

在代码层面,不以sartIndex为限制,而以used[]来限制


子集问题:一个N个数的集合里有多少符合条件的子集 

力扣

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:

输入:nums = [0]
输出:[[],[0]]

//  1 定义解空间
let result = []
let step = []
// 2 定义回溯函数的参数和反回值

function backTracing(startIndex, nums) {
    result.push(Array.from(step))
    // 3 定义终止条件
    if (startIndex === nums.length) {
        return
    }
    // // 4 定义操作,遍历每一个元素

    for(let i=startIndex;i<nums.length;i++){
        step.push(nums[i])
        backTracing(++i, nums)
        step.pop()
    }
}
var subsets = function (nums) {
    result = []
    step = []
    backTracing(0, nums)
    console.log(result)
};
subsets([1, 2, 3])

总结 

与其他类型区别最大的就是,他是在每一个节点收集结果,而非在叶子节点


切割问题:一个字符串按一定规则有几种切割方式 

力扣

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回 s 所有可能的分割方案。

示例: 输入: "aab" 输出: [ ["aa","b"], ["a","a","b"] ]

代码

const isPalindrome = (s, l, r) => {
    for (let i = l, j = r; i < j; i++, j--) {
        if(s[i] !== s[j]) return false;
    }
    return true;
}

var partition = function(s) {
    const res = [], path = [], len = s.length;
    backtracking(0);
    return res;
    function backtracking(i) {
        if(i >= len) {
            res.push(Array.from(path));
            return;
        }
        for(let j = i; j < len; j++) {
            if(!isPalindrome(s, i, j)) continue;
            path.push(s.substr(i, j - i + 1));
            backtracking(j + 1);
            path.pop();
        }
    }
};

 总结:

类似多了条件判断的组合问题,还是有startIndex等


个人总结:

回溯算法不是很难,主要为两种

  1. 以每个节点为结果集的类型(子集问题)
  2. 以叶子节点为结果集的类型(排列问题,组合问题,切割问题)

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值