回溯法(dfs)的总结

回溯法(dfs)的总结

我不是专业搞算法的,肯定不如各路大佬总结的好,程序效率也没有大佬高。但自己总结下来至少用着比较顺手,也希望能帮到你!

就希望自己别在回溯题上磨半天了;

1. 无重复有顺序的子集

序列里面没有重复的值,要求按照原来的顺序。因为要按照原有顺序,用过的就再不能用了,所以每次idx的位置是遍历位置i+1

见图:

在这里插入图片描述

无重复有顺序样例:剑指 Offer II 079. 所有子集

class Solution {
private:
    void dfs (int idx, vector<int> tmp,vector<int> &nums, int &size, vector<vector<int>> &res) {
        // 终止条件不需要写,会在下面的遍历自动终止
        for (int i = idx; i < size; ++i) { // 无重复:即不用在进入递归dfs前判断该数是否已经使用过
            tmp.emplace_back(nums[i]);
            res.emplace_back(tmp);
            dfs(i + 1, tmp, nums, size, res); // 有顺序:即按顺序用过的就不能再用了,每次递归时传入的idx是i + 1
            tmp.pop_back();
        }
    }
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<vector<int>> res;
        res.emplace_back();
        int size = nums.size();
        dfs(0, {}, nums, size, res);
        return res;
    }
};

带层数的:如剑指 Offer II 080. 含有 k 个元素的组合

class Solution {
private:
    void dfs (int idx, vector<int> tmp, int dep, int &targetDep, vector<int> &nums, int size, vector<vector<int>> &res) {
        if (dep == targetDep) { // 该题终止条件为遍历深度(组合元素个数)
            res.emplace_back(tmp);
            return;
        }
        for (int i = idx; i < size; ++i) { // 无重复:即不用在递归dfs前判断是否与前一数重复
            tmp.emplace_back(nums[i]);
            dfs(i + 1, tmp, dep + 1, targetDep, nums, size, res); // 有顺序:即按顺序用过的就不能再用了,每次递归时传入的idx是i + 1
            tmp.pop_back();
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        vector<vector<int>> res;
        vector<int> nums(n);
        for (int i = 1; i <= n; ++i) nums[i-1] = i; // 方便套模板
        dfs(0, {}, 0, k, nums, n, res);
        return res;
    }
};

2. 无重复无顺序的子集 / 全排列

**无重复:**数组里面没有重复的值,也就是进入dfs之前不需要检查当前值是否已用过;

**无顺序:**就是说每层递归,除了当前正在用的数(拿来充tmp的数)都可以继续使用,无论在该层是否用过;这里有个简单的技巧:每次递归前把不可用数(当前正在用的nums[i])和当层遍历的起始头部(即nums[idx])进行交换,如此一来就保证了每一层idx之后的数都是可用的,只需继续遍历即可。具体的解释和过程见下图:

求所有子列的话需要开tmp记录,某长度的全排列在某一层更新即可。

在这里插入图片描述

例程:剑指 Offer II 083. 没有重复元素集合的全排列

class Solution {
private:
    void dfs (int idx, vector<int> tmp, vector<int> &nums, vector<vector<int>> &res) {
        if (idx == nums.size()) {
            res.push_back(tmp);
            return;
        }

        for (int i = idx; i < nums.size(); ++i) { // 无重复:即不用在递归dfs前判断是否与前一数重复
            tmp.push_back(nums[i]);
            swap(nums[idx], nums[i]);
            dfs(idx + 1, tmp, nums, res); // 无顺序:即按顺序用过的还能接着用,每次把不能用的nums[i]换到遍历起始的idx头部之后,递归idx + 1
            swap(nums[idx], nums[i]);
            tmp.pop_back();
        }
    }
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> res;
        dfs(0, {}, nums, res);
        return res;
    }
};

3. 有重复有顺序的子集

因为同一层相同数字会引出相同的枝,我们将数组排序后遇到与上一个相同的就跳过不进入dfs即可,当然也可用无序set记录使用过的元素。

注:去重是不进dfs递归,而不是进了递归再去重

在这里插入图片描述

样例:剑指 Offer II 082. 含有重复元素集合的组合(与上图无关)

虽然没有说有顺序,但看样例是要去除不同顺序但同元素的组合的(如[1, 1, 2]和[1, 2, 1]就算同一个),所以我们直接按有顺序,只返回固定顺序的组合。

class Solution {
private:
    void dfs (int idx, int sum, int &target, vector<int> tmp, vector<int> &nums, int &size, vector<vector<int>> &res) {
        if (sum > target) {
            return;
        } else if (sum == target) {
            res.emplace_back(tmp);
            return;
        }

        for (int i = idx; i < size; ++i) {
            if (i > idx && nums[i] == nums[i - 1]) { // 有重复,就得在这里把重复的丢掉,别进循环,但最通用的还是在这里用无序set存储遍历过的数字,具体见下一节
                continue;
            }

            sum += nums[i];
            tmp.emplace_back(nums[i]);
            dfs(i + 1, sum, target, tmp, nums, size, res); // 有顺序:即按顺序用过的就不能再用了,每次递归时传入的idx是i + 1
            tmp.pop_back();
            sum -= nums[i];
        }
    }
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        int candidatesSum = accumulate(candidates.begin(), candidates.end(), 0);
        if (candidatesSum < target) return {};
        else if (candidatesSum == target) return {candidates};

        sort(candidates.begin(), candidates.end()); // 排序方便我们后续去重,当然无序set去重才更通用
        int size = candidates.size();
        vector<vector<int>> res;
        dfs(0, 0, target, {}, candidates, size, res);
        return res;
    }
};

注:这道题升级一下,不要求顺序了,比如[1, 1, 2, 3],要返回的是[1, 2]、[2, 1] 和 [3]。

我们只需要一点点改动:

dfs(i + 1, sum, target, tmp, nums, size, res);

// 将上面那行替换为下面这三行:

swap(nums[i], nums[idx]);
dfs(idx + 1, sum, target, tmp, nums, size, res); // 无顺序:即按顺序用过的还能接着用,每次把不能用的nums[i]换到遍历起始的idx头部之后,递归idx + 1
swap(nums[i], nums[idx]);

4. 有重复无顺序的子集 / 全排列

跟上一个一致,哈希表去重不进dfs就好。

注:这里注意!!!!!由于无顺序要求,有swap操作,即便排好序也有可能在交换后不满足sorted,导致重复数字无法被排除!!!

在这里插入图片描述

样例:剑指 Offer II 084. 含有重复元素集合的全排列

class Solution {
private:
    void dfs (int idx, vector<int> tmp, vector<int> &nums, int &size, vector<vector<int>> &res) {
        if (idx == size) {
            res.emplace_back(tmp);
            return;
        }
        unordered_set<int> st;
        for (int i = idx; i < size; ++i) {
            if (st.count(nums[i])) { // 有重复,需要记录用过的元素,用过则跳过不继续递归
                continue;
            }
            st.emplace(nums[i]);
            tmp.emplace_back(nums[i]);
            swap(nums[idx], nums[i]);
            dfs(idx + 1, tmp, nums, size, res); // 无顺序:即按顺序用过的还能接着用,每次把不能用的nums[i]换到遍历起始的idx头部之后,递归idx + 1
            swap(nums[idx], nums[i]);
            tmp.pop_back();
        }
    }
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<vector<int>> res;
        sort(nums.begin(), nums.end());
        int size = nums.size();
        dfs(0, {}, nums, size, res);
        return res;
    }
};

5. 无重复有顺序可无限选

元素能无限选,肯定得有终止条件,idx每次保持在i不变,以让当前分支走完。

图解:

在这里插入图片描述

如:剑指 Offer II 081. 允许重复选择元素的组合

这里有点特殊,因为虽然没有顺序要求,但是所选数字数量相同视为一样的组合。相当于有顺序,也就是我们只拿固定顺序的组合就好。

class Solution {
private:
    void dfs (int idx, int sum, int &target, vector<int> tmp, vector<int> &nums, int &size, vector<vector<int>> &res) {
        if (sum > target) { // 终止条件
            return;
        } else if (sum == target) {
            res.emplace_back(tmp);
            return;
        }
        for (int i = idx; i < size; i++) { // 无重复,在进入dfs前不用去重
            int num = nums[i];
            sum += num;
            tmp.emplace_back(num);
            dfs (i, sum, target, tmp, nums, size, res); // 可无限选,标志就是传入idx在单根上保持i不变,可以一直用1、1、1、1、……直到终止条件成立再继续遍历下一个i
            tmp.pop_back();
            sum -= num;
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>> res;
        int size = candidates.size();
        dfs(0, 0, target, {}, candidates, size, res);
        return res;
    }
};

6. 无重复无顺序可无限选

我们把剑指 Offer II 081. 允许重复选择元素的组合升级一下:不同顺序也视为不同的组合,就是这种情况了。

图解:

在这里插入图片描述

class Solution {
private:
    void dfs(int idx, int sum, int &target, vector<int> tmp, vector<int> &nums, int &size, vector<vector<int>> &res) {
        if (sum > target) {
            return;
        } else if (sum == target) {
            res.emplace_back(tmp);
            return;
        }
        for (int i = idx; i < size; i++) { // 无重复,在进入dfs前不用去重
            int num = nums[i];
            sum += num;
            tmp.emplace_back(num);
            swap(nums[i], nums[idx]); // 无顺序要求,标志就是交换,相当于我每次都把idx位的元素置成当前遍历到的i位置元素
            dfs(i, sum, target, tmp, nums, size, res); // 可无限选,标志就是传入idx在单根上不变,可以一直用1、1、1、1、……直到终止
            swap(nums[i], nums[idx]);
            tmp.pop_back();
            sum -= num;
        }
    }

public:
    vector<vector<int>> combinationSum(vector<int> &candidates, int target) {
        vector<vector<int>> res;
        int size = candidates.size();
        dfs(0, 0, target, {}, candidates, size, res);
        return res;
    }
};

注: 这里把传入的数到底是哪一个搞清楚。其实可以把dfs传入的数换成idx再改一下swap等的顺序,固定取每层第一个数,但是为了和有顺序的保持一致,还是用了i

……

总结

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WuPeng_uin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值