[leetcode]回溯框架TypeScript版本

回溯框架

TypeScript | JavaScript | 回溯框架

排列组合子集
无重复 + 不可复选467778
可重复 + 不可复选479090
无重复 + 可复选全排列(元素无重可复选)3939

无论是排列、组合还是子集问题,简单说无非就是让你从序列 nums 中以给定规则取若干元素,主要有以下几种变体:

形式一、元素无重不可复选,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次,这也是最基本的形式

以组合为例,如果输入 nums = [2,3,6,7],和为 7 的组合应该只有 [7]

形式二、元素可重不可复选,即 nums 中的元素可以存在重复,每个元素最多只能被使用一次

以组合为例,如果输入 nums = [2,5,2,1,2],和为 7 的组合应该有两种 [2,2,2,1][5,2]

形式三、元素无重可复选,即 nums 中的元素都是唯一的,每个元素可以被使用若干次

以组合为例,如果输入 nums = [2,3,6,7],和为 7 的组合应该有两种 [2,2,3][7]

当然,也可以说有第四种形式,即元素可重可复选。但既然元素可复选,那又何必存在重复元素呢?元素去重之后就等同于形式三,所以这种情况不用考虑。

上面用组合问题举的例子,但排列、组合、子集问题都可以有这三种基本形式,所以共有 9 种变化。

除此之外,题目也可以再添加各种限制条件,比如让你求和为 target 且元素个数为 k 的组合,那这么一来又可以衍生出一堆变体,怪不得面试笔试中经常考到排列组合这种基本题型。

但无论形式怎么变化,其本质就是穷举所有解,而这些解呈现树形结构,所以合理使用回溯算法框架,稍改代码框架即可把这些问题一网打尽

子集(元素无重不可复选)

78. 子集

78. 子集

给你输入一个无重复元素的数组 nums,其中每个元素最多使用一次,请你返回 nums 的所有子集。

function subsets(nums: number[]): number[][] {
    // 设置结果集    
    const res: number[][] = [];

    // 记录回溯算法的递归路径
    const track: number[] = [];

    // 回溯算法核心函数,遍历子集问题的回溯树
    const helper = (nums: number[], start: number) => {
        // 前序位置,每个节点的值都是一个子集
        res.push(track.concat());

        // 回溯算法标准框架
        for (let i = start; i < nums.length; i++) {
            // 做出选择
            track.push(nums[i]);

            // 通过 start 参数控制树枝的遍历,避免产生重复的子集
            helper(nums, i + 1);

            // 撤销选择
            track.pop();
        }
    }

    // 开始主函数
    helper(nums, 0)

    // 返回结果
    return res;
};

组合(元素无重不可复选)

77. 组合

77. 组合

function combine(n: number, k: number): number[][] {
    // 设置结果集    
    const res: number[][] = [];

    // 记录回溯算法的递归路径
    const track: number[] = [];

    // 回溯算法核心函数,遍历子集问题的回溯树
    const helper = (start: number, n: number, k: number) => {
        // base case
        if (k == track.length) {
            // 遍历到了第 k 层,收集当前节点的值
            res.push(track.concat());
            return;
        }

        // 回溯算法标准框架
        for (let i = start; i <= n; i++) {
            // 做出选择
            track.push(i)

            // 通过 start 参数控制树枝的遍历,避免产生重复的子集
            helper(i + 1, n, k);

            // 撤销选择
            track.pop();
        }
    }

    // 开始主函数
    helper(1, n, k)

    // 返回结果
    return res;
};

子集/组合(元素可重不可复选)

90. 子集 II

90. 子集 II

子集/组合(元素可重不可复选)

给你一个整数数组 nums,其中可能包含重复元素,每个元素只能被选择一次,请你返回该数组所有可能的子集。

function subsetsWithDup(nums: number[]): number[][] {
    // 设置结果集    
    const res: number[][] = [];

    // 记录回溯算法的递归路径
    const track: number[] = [];

    // 回溯算法核心函数,遍历子集问题的回溯树
    const helper = (nums: number[], start: number) => {
        // 前序位置,每个节点的值都是一个子集
        res.push(track.concat());

        // 回溯算法标准框架
        for (let i = start; i < nums.length; i++) {
            // 剪枝逻辑,值相同的相邻树枝,只遍历第一条
            if (i > start && nums[i] == nums[i - 1]) {
                continue;
            }

            track.push(nums[i]);
            helper(nums, i + 1);
            track.pop();
        }
    }

    // 开始主函数
    // 先排序,让相同的元素靠在一起
    nums.sort()
    helper(nums, 0)

    // 返回结果
    return res;
};

子集/组合(元素无重可复选)

39. 组合总和

39. 组合总和

给你一个无重复元素的整数数组 candidates 和一个目标和 target,找出 candidates 中可以使数字和为目标数 target 的所有组合。candidates 中的每个数字可以无限制重复被选取。

function combinationSum(candidates: number[], target: number): number[][] {
    // 设置结果集    
    const res: number[][] = [];

    // 记录回溯算法的递归路径
    const track: number[] = [];

    // 记录 track 中的路径和
    let trackSum = 0;

    // 回溯算法核心函数,遍历子集问题的回溯树
    const helper = (nums: number[], start: number, target: number) => {
        // base case,找到目标和,记录结果
        if (trackSum == target) {
            res.push(track.concat());
            return;
        }

        // base case,超过目标和,停止向下遍历
        if (trackSum > target) {
            return;
        }

        // 回溯算法标准框架
        for (let i = start; i < nums.length; i++) {
            // 选择 nums[i]
            trackSum += nums[i];
            track.push(nums[i]);

            // 递归遍历下一层回溯树
            // 同一元素可重复使用,注意参数
            helper(nums, i, target);

            // 撤销选择 nums[i]
            trackSum -= nums[i];
            track.pop();
        }
    }

    // -----------------开始主函数-------------

    if (candidates.length == 0) {
        return res;
    }
    helper(candidates, 0, target);
    return res;
};

排列(元素无重不可复选)

46. 全排列

46. 全排列

function permute(nums: number[]): number[][] {
    // 1. 设置结果集
    const result: number[][] = [];

    // 2. 回溯
    const recursion = (path, set) => {
        // 2.1 设置回溯终止条件
        if (path.length === nums.length) {

            // 2.1.1 推入结果集
            result.push(path.concat());

            // 2.1.2 终止递归
            return;
        }

        // 2.2 遍历数组
        for (let i = 0; i < nums.length; i++) {

            // 2.2.1 必须是不存在 set 中的坐标
            if (!set.has(i)) {

                // 2.2.2 本地递归条件(用完记得删除)
                path.push(nums[i]);
                set.add(i);

                // 2.2.3 进一步递归
                recursion(path, set);

                // 2.2.4 回溯:撤回 2.2.2 的操作
                path.pop();
                set.delete(i);
            }
        }
    };
    recursion([], new Set());

    // 3. 返回结果
    return result;
};

排列(元素可重不可复选)

47. 全排列 II

47. 全排列 II

function permuteUnique(nums: number[]): number[][] {
    // 设置结果集    
    const res: number[][] = [];

    // 记录回溯算法的递归路径
    const track: number[] = [];

    // 已经使用的
    const used: boolean[] = new Array(nums.length).fill(false);

    // 回溯算法核心函数,遍历子集问题的回溯树
    const helper = (nums: number[]) => {
        // base case
        if (track.length == nums.length) {
            res.push(track.concat());
            return;
        }

        // 回溯算法标准框架
        for (let i = 0; i < nums.length; i++) {
            if (used[i]) {
                continue;
            }
            // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue;
            }

            // 做出选择
            track.push(nums[i]);
            used[i] = true;


            helper(nums);

            // 撤销选择
            track.pop();
            used[i] = false;
        }
    }

    // 开始主函数
    // 先排序,让相同的元素靠在一起
    nums.sort()
    helper(nums)

    // 返回结果
    return res;
};

排列(元素无重可复选)

全排列(元素无重可复选)

还是全排列问题,这一次在nums 数组中的元素无重复且可复选的情况下,会有哪些排列?

比如输入 nums = [1,2,3],那么这种条件下的全排列共有 3^3 = 27 种:

[
  [1,1,1],[1,1,2],[1,1,3],[1,2,1],[1,2,2],[1,2,3],[1,3,1],[1,3,2],[1,3,3],
  [2,1,1],[2,1,2],[2,1,3],[2,2,1],[2,2,2],[2,2,3],[2,3,1],[2,3,2],[2,3,3],
  [3,1,1],[3,1,2],[3,1,3],[3,2,1],[3,2,2],[3,2,3],[3,3,1],[3,3,2],[3,3,3]
]

标准的全排列算法利用 used 数组进行剪枝,避免重复使用同一个元素。如果允许重复使用元素的话,直接放飞自我,去除所有 used 数组的剪枝逻辑就行了

function permuteRepeat(nums: number[]): number[][] {
    // 设置结果集    
    const res: number[][] = [];

    // 记录回溯算法的递归路径
    const track: number[] = [];

    // 回溯算法核心函数,遍历子集问题的回溯树
    const helper = (nums: number[]) => {
        // base case 到达叶子节点
        if (track.length == nums.length) {
            res.push(track.concat());
            return;
        }

        // 回溯算法标准框架
        for (let i = 0; i < nums.length; i++) {
            // 做出选择
            track.push(nums[i]);

            // 进入下一层回溯树
            helper(nums);

            // 撤销选择
            track.pop();
        }
    }

    // 开始主函数
    helper(nums)

    // 返回结果
    return res;
};

40. 组合总和 II

40. 组合总和 II

function combinationSum2(candidates: number[], target: number): number[][] {
    // 设置结果集    
    const res: number[][] = [];

    // 记录回溯算法的递归路径
    const track: number[] = [];

    // 记录 track 中的路径和
    let trackSum = 0;

    // 回溯算法核心函数,遍历子集问题的回溯树
    const helper = (nums: number[], start: number, target: number) => {
        // base case,找到目标和,记录结果
        if (trackSum == target) {
            res.push(track.concat());
            return;
        }

        // base case,超过目标和,停止向下遍历
        if (trackSum > target) {
            return;
        }

        // 回溯算法标准框架
        for (let i = start; i < nums.length; i++) {
            // 剪枝逻辑,值相同的树枝,只遍历第一条
            if (i > start && nums[i] == nums[i - 1]) {
                continue;
            }

            // 做选择
            trackSum += nums[i];
            track.push(nums[i]);

            // 递归遍历下一层回溯树
            // 同一元素不可重复使用,注意参数
            helper(nums, i + 1, target);

            // 撤销选择 nums[i]
            trackSum -= nums[i];
            track.pop();
        }
    }

    // -----------------开始主函数-------------

    if (candidates.length == 0) {
        return res;
    }

    // 先排序,让相同的元素靠在一起
    candidates.sort()
    helper(candidates, 0, target);
    return res;
};

216. 组合总和 III

216. 组合总和 III

function combinationSum3(k: number, n: number): number[][] {
    // 设置结果集    
    const res: number[][] = [];

    // 记录回溯算法的递归路径
    const track: number[] = [];

    // 记录 track 中的路径和
    let trackSum: number = 0;

    // 回溯算法核心函数,遍历子集问题的回溯树
    const helper = (start: number, n: number, k: number) => {
        // base case,找到目标和,记录结果
        if (k == track.length && trackSum == n) {
            // 遍历到了第 k 层,trackSun == n,收集
            res.push(track.concat());
            return;
        }

        // base case,超过目标和,停止向下遍历
        if (trackSum > n) {
            return;
        }

        // 回溯算法标准框架
        for (let i = start; i <= 9; i++) {
            // 选择  
            trackSum += i;
            track.push(i);

            // 递归遍历下一层回溯树
            helper(i + 1, n, k);

            // 撤销选择 nums[i]
            trackSum -= i;
            track.pop();
        }
    }

    // -----------------开始主函数-------------
    helper(1, n, k);
    return res;
};

小结

形式一

元素无重不可复选,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次backtrack 核心代码如下:

/* 组合/子集问题回溯算法框架 */
function backtrack( nums,  start) {
    // 回溯算法标准框架
    for (let i = start; i < nums.length; i++) {
        // 做选择
        track.push(nums[i]);
        // 注意参数
        backtrack(nums, i + 1);
        // 撤销选择
        track.removeLast();
    }
}

/* 排列问题回溯算法框架 */
function backtrack( nums) {
    for (let i = 0; i < nums.length; i++) {
        // 剪枝逻辑
        if (used[i]) {
            continue;
        }
        // 做选择
        used[i] = true;
        track.push(nums[i]);

        backtrack(nums);
        // 撤销选择
        track.removeLast();
        used[i] = false;
    }
}

形式二

元素可重不可复选,即 nums 中的元素可以存在重复,每个元素最多只能被使用一次,其关键在于排序和剪枝,backtrack 核心代码如下:

nums.sort();
/* 组合/子集问题回溯算法框架 */
function backtrack( nums, let start) {
    // 回溯算法标准框架
    for (let i = start; i < nums.length; i++) {
        // 剪枝逻辑,跳过值相同的相邻树枝
        if (i > start && nums[i] == nums[i - 1]) {
            continue;
        }
        // 做选择
        track.push(nums[i]);
        // 注意参数
        backtrack(nums, i + 1);
        // 撤销选择
        track.removeLast();
    }
}


nums.sort();
/* 排列问题回溯算法框架 */
function backtrack( nums) {
    for (let i = 0; i < nums.length; i++) {
        // 剪枝逻辑
        if (used[i]) {
            continue;
        }
        // 剪枝逻辑,固定相同的元素在排列中的相对位置
        if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
            continue;
        }
        // 做选择
        used[i] = true;
        track.push(nums[i]);

        backtrack(nums);
        // 撤销选择
        track.removeLast();
        used[i] = false;
    }
}

形式三

元素无重可复选,即 nums 中的元素都是唯一的,每个元素可以被使用若干次,只要删掉去重逻辑即可,backtrack 核心代码如下:

/* 组合/子集问题回溯算法框架 */
function backtrack( nums, let start) {
    // 回溯算法标准框架
    for (let i = start; i < nums.length; i++) {
        // 做选择
        track.push(nums[i]);
        // 注意参数
        backtrack(nums, i);
        // 撤销选择
        track.removeLast();
    }
}


/* 排列问题回溯算法框架 */
function backtrack( nums) {
    for (let i = 0; i < nums.length; i++) {
        // 做选择
        track.push(nums[i]);
        backtrack(nums);
        // 撤销选择
        track.removeLast();
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值