力扣方法总结:回溯(DFS)篇

46. 全排列 Medium 回溯 路过改值 2023/2/10

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

本题为无重复数字的全排列问题,方法二使用路过改值的方法,每次选择不会选择已选择过的元素

回溯的经典解法,本质上是个多叉树的遍历问题,关键就是在前序遍历位置和后序遍历位置做一些操作。本质上还是纯暴力穷举。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> track;
    vector<vector<int>> permute(vector<int>& nums) {
        backtrack(nums);
        return res;
    }
    void backtrack(vector<int>& nums){
        // 结束条件
        if (track.size() == nums.size()){
            res.push_back(track);
            return;
        }     
        // 循环遍历
        for (int i : nums){
            // 排除不合法的选择
            if (find(track.begin(), track.end(), i) != track.end())
                continue;
            // 做选择
            track.push_back(i);
            // 进入下一层决策树
            backtrack(nums);
            // 取消选择
            track.pop_back();
        }
    }
};

上述代码在排除不合法的选择时使用了find方法,带入了O(n)的时间复杂度,可以通过路过改值来降低时间复杂度,在取消选择时把值改回来即可。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> track;
    vector<vector<int>> permute(vector<int>& nums) {
        backtrack(nums);
        return res;
    }
    void backtrack(vector<int>& nums){
        // 结束条件
        if (track.size() == nums.size()){
            res.push_back(track);
            return;
        }     
        // 循环遍历
        for (int i = 0; i < nums.size(); i++){
            // 排除不合法的选择
            if (nums[i] == 999) continue;
            int temp = nums[i];
            // 做选择
            nums[i] = 999;
            track.push_back(temp);
            // 进入下一层决策树
            backtrack(nums);
            // 取消选择
            track.pop_back();
            nums[i] = temp;
        }
    }
};

47. 全排列 II Medium 回溯 路过改值 相邻数比较 2023/2/10

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]

本题为上题的进阶版本,为含重复数字的不重复全排列问题,使用路过改值的方法,每次选择不会选择已选择过的元素。且某数退出选择后,其之后相同的数还有可能被选择,使用相邻数比较的方法,与路过改值同时使用,可以实现完美横向剪枝

首先排序保证相同的数在一起,再对相邻的数大小进行判断,如果相同则进行剪枝(此处需要深刻体会,非常精妙)。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> track;
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        // 先排序,保证相同的数在一起
        sort(nums.begin(), nums.end());
        backtrack(nums);
        return res;
    }
    void backtrack(vector<int>& nums) {
        int len = nums.size();
        for (int i = 0; i < len; i++) {
            if (track.size() == len) {
                res.push_back(track);
                return;
            }
            // 排除不合法的选择
            if (nums[i] == 999) continue; // 纵向剪枝,不走回头路
            if (i >= 1 && nums[i] == nums[i - 1]) continue; // 横向剪枝,非常精妙
            int temp = nums[i];
            // 做选择
            nums[i] = 999;
            track.push_back(temp);
            // 进入下一层决策树
            backtrack(nums);
            // 取消选择
            track.pop_back();
            nums[i] = temp;
        }
    }
};

39. 组合总和 Medium 回溯 2023/2/13

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

本题为无重复数字的可重复选取回溯问题,且元素均为正数。
在选取过程中,不可能走回头路,故使用index记录遍历的开始位置,且若当前sum + candidates[i] > target,需及时break退出循环,会比下一次判断sum > target更优,避免了接下来所有无效的遍历。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> comb;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        backtrack(candidates, target, 0, 0);
        return res;
    }
    void backtrack(vector<int> candidates, int target, int sum, int index) {
        if (sum == target) {
            res.push_back(comb);
            return;
        }
        for (int i = index; i < candidates.size(); i++) {
            if (sum + candidates[i] > target) break;
            // 做选择
            comb.push_back(candidates[i]);
            backtrack(candidates, target, sum + candidates[i], i);
            // 取消选择
            comb.pop_back();
        }
    }
};

40. 组合总和 II Medium 回溯 巧相邻数比较 2023/2/13

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

本题为有重复数字的不可重复选取回溯问题,且元素均为正数,与全排列 II非常相似。
在选取过程中,为避免某数退出选择后,其之后相同的数还有可能被选择的问题,依然采用路过改值+相邻数比较的方法,实现横向剪枝,同时搭配index记录遍历的开始位置。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> track;
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        backtrack(candidates, target, 0, 0);
        return res;
    }
    void backtrack(vector<int>& candidates, int target, int sum, int index) {
        if (sum == target) {
            res.push_back(track);
            return;
        }
        for (int i = index; i < candidates.size(); i++) {
            if (sum + candidates[i] > target) break;
            if (candidates[i] == 9999) continue;
            // 相邻数比较
            if (i >= 1 && candidates[i] == candidates[i - 1]) continue;
            // 路过改值
            int t = candidates[i];
            candidates[i] = 9999;
            // 做选择
            track.push_back(t);
            backtrack(candidates, target, sum + t, i + 1);
            // 取消选择
            track.pop_back();
            candidates[i] = t;
        }
    }
};

但其实还有更漂亮的只相邻数比较的方法,只需在判断中加入i > index即可,避免了路过改值的麻烦。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> track;
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        backtrack(candidates, target, 0, 0);
        return res;
    }
    void backtrack(vector<int>& candidates, int target, int sum, int index) {
        if (sum == target) {
            res.push_back(track);
            return;
        }
        for (int i = index; i < candidates.size(); i++) {
            if (sum + candidates[i] > target) break;
            // 剪枝
            if (i > index && candidates[i] == candidates[i - 1]) continue;
            // 做选择
            track.push_back(candidates[i]);
            backtrack(candidates, target, sum + candidates[i], i + 1);
            track.pop_back();
        }
    }
};

784. 字母大小写全排列 Medium 回溯 大小写转换 2023/2/11

给定一个字符串 s ,通过将字符串 s 中的每个字母转变大小写,我们可以获得一个新的字符串。
返回 所有可能得到的字符串集合 。以 任意顺序 返回输出。
示例:
输入:s = “a1b2”
输出:[“a1b2”, “a1B2”, “A1b2”, “A1B2”]

本题为只更改当前字符回溯问题,在backtrack回溯方法中不需要使用for循环,只需直接把当前元素变更大小写并向下带索引递归即可。

首先想到的想法是使用str记录回溯时的字符串,并记录当前index,每次进回溯前拼接字符,回溯后删掉末尾字符,如果字符是字母,则拼接其对应大小写再回溯一次。

将字母转为对应大/小写使用了与32异或的做法,非常简洁精妙。

class Solution {
public:
    vector<string> res;
    string str;
    vector<string> letterCasePermutation(string s) {
        dfs(s, 0);
        return res;
    }
    void dfs(string s, int index) {
    	// 终止条件
        if (index == s.size()) {
            res.push_back(str);
            return;
        }
        // 做选择
        str += s[index];
        // 回溯
        dfs(s, index + 1);
        // 取消选择
        str.erase(str.end() - 1);
        if (isalpha(s[index])) {
            // 做选择
            str += s[index] ^ 32;
            // 回溯
            dfs(s, index + 1);
            // 取消选择
            str.erase(str.end() - 1);
        }
    }
};

由于本题可以直接对s进行操作并存入s的值,可以省去str变量,非常巧妙!这里注意s传入的不能是引用。

class Solution {
public:
    vector<string> res;
    vector<string> letterCasePermutation(string s) {
        dfs(s, 0);
        return res;
    }
    void dfs(string s, int index) {
    	// 终止条件
        if (index == s.size()) {
            res.push_back(s);
            return;
        }
        // 直接递归回溯
        dfs(s, index + 1);
        // 如果是字母,转为大/小写并向后dfs
        if (isalpha(s[index])) {
            s[index] ^= 32; // // s[i] == islower(s[i]) ? toupper(s[i]) : tolower(s[i]);
            dfs(s, index + 1);
        }
    }
};

套路总结

  • 需要重头开始找⇒大for循环遍历(起始和终止索引不变)
    • 无重复元素的不能重复选择⇒路过改值
    • 有重复元素的不重复的排列⇒路过改值+相邻数比较
  • 不需要重头开始找⇒直接向下递归(少) or 带索引向下递归(更改起始索引)
    • 有重复元素的不重复的排列⇒路过改值+相邻数比较 or 只相邻数比较

526. 优美的排列 Medium 回溯 2023/2/12

假设有从 1 到 n 的 n 个整数。用这些整数构造一个数组 perm(下标从 1 开始),只要满足下述条件 之一 ,该数组就是一个 优美的排列 :
perm[i] 能够被 i 整除
i 能够被 perm[i] 整除
给你一个整数 n ,返回可以构造的 优美排列 的 数量 。
示例:
输入:n = 2
输出:2
解释:
第 1 个优美的排列是 [1,2]:
- perm[1] = 1 能被 i = 1 整除
- perm[2] = 2 能被 i = 2 整除
第 2 个优美的排列是 [2,1]:
- perm[1] = 2 能被 i = 1 整除
- i = 2 能被 perm[2] = 1 整除

经典回溯暴力遍历,使用bool数组记忆化回溯,回溯时num表示当前是为num下标找一个数组值。

class Solution {
public:
    int res = 0;
    int countArrangement(int n) {
        vector<bool> b(n + 1, false);
        dfs(n, 1, b);
        return res;
    }
    // 为num下标找一个数组值
    void dfs(int n, int num, vector<bool> b) {
        // 终止条件
        if (num > n) {
            res++;
            return;
        }
        // 做选择
        for (int i = 1; i <= n; i++) {
            // 排除不合法的选择
            if (b[i] == true) continue;
            if (num % i != 0 && i % num != 0) continue;
            b[i] = true;
            dfs(n, num + 1, b);
            b[i] = false;
        }
    }
};

17. 电话号码的字母组合 Medium 抽象化问题 回溯 2023/2/13

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
在这里插入图片描述
示例:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]

本题多一层问题抽象,将2-9的字符串对应到一个数组中,接着对数字的每一位找到其数组对应的string进行回溯遍历,回溯时使用num记录遍历到哪一位了。

class Solution {
public:
    vector<string> res;
    string str;
    vector<string> letterCombinations(string digits) {
        if (digits.empty()) return res;
        vector<string> s{"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        backtrack(digits, s, 0);
        return res;
    }
    void backtrack(string& digits, vector<string>& s, int num) {
        if (str.size() == digits.size()) {
            res.push_back(str);
            return;
        }
        for (char c: (s[digits[num] - '2'])) {
            // 做选择
            str += c;
            backtrack(digits, s, num + 1);
            // 取消选择
            str.erase(str.end() - 1);
        }
    }
};

22. 括号生成 Medium 抽象化问题 回溯 2023/2/13

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例:
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]

本题难点也在于问题抽象,回溯时使用left和right记录已经使用的左括号和右括号数,并判断接下来该加入哪个括号。

class Solution {
public:
    vector<string> res;
    string s;
    vector<string> generateParenthesis(int n) {
        backtrack(n, 0, 0);
        return res;
    }
    void backtrack(int n, int left, int right) {
        if (s.size() == 2 * n) {
            res.push_back(s);
            return;
        }
        // 加入左括号的条件
        if (left < n) {
            s += '(';
            backtrack(n, left + 1, right);
            s.erase(s.end() - 1);
        }
        // 加入右括号的条件
        if (left > right) {
            s += ')';
            backtrack(n, left, right + 1);
            s.erase(s.end() - 1);
        }
    }
};

79. 单词搜索 Medium 迷宫 回溯 2023/2/14

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
在这里插入图片描述
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true

本题与常规回溯相比,更加抽象,首先使用一个两层for循环寻找单词起点,在每次的起点使用dfs进行递归,递归的参数除了wordboard之外,还有当前匹配的字符下标start,当前的位置i, j, 最关键的是记录当前位置有没有被来过,需要创建一个bool数组visited,只有当当前位置合法、当前字符正确、当前位置没来过的情况下,向四周走动,并在合适的位置改变visited的值。

class Solution {
public:
    bool res = false;
    bool exist(vector<vector<char>>& board, string word) {
        int rows = board.size(); 
        int cols = board[0].size();
        vector<vector<bool>> visited(rows, vector<bool>(cols, false));
        for (int i = 0; i <= rows; i++) {
            for (int j = 0; j <= cols; j++) {
                // 找起点
                dfs(board, word, 0, i, j, visited);
                if (res == true) return res;
            }
        }
        return res;
    }
    void dfs(vector<vector<char>>& board, string word, int start, int i, int j, vector<vector<bool>>& visited) {
        int rows = board.size(); 
        int cols = board[0].size();
        // 成功找到
        if (start == word.size()) {
            res = true;
            return;
        }
        // 跑到地图外边了
        if (i < 0 || i >= rows || j < 0 || j >= cols) return;
        // 字符不对
        if (board[i][j] != word[start]) return;
        // 该区域来过
        if (b[i][j] == true) return;
        // 做选择
        b[i][j] = true;
        dfs(board, word, start + 1, i, j + 1, visited); // 向右走 
        dfs(board, word, start + 1, i, j - 1, visited); // 向左走
        dfs(board, word, start + 1, i + 1, j, visited); // 向下走 
        dfs(board, word, start + 1, i - 1, j, visited); // 向上走
        // 取消选择
        b[i][j] = false;
    }
};

93. 复原 IP 地址 Medium 抽象 回溯 2023/2/15

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
示例:
输入:s = “25525511135”
输出:[“255.255.11.135”,“255.255.111.35”]

记忆化回溯,number记录已包含的整数,index记录遍历的下标位置。
需要注意的:

  • 0的情况:有0的时候,窗口大小只能为1(在这里indexSize为0)。
  • 回溯取消选择时,需要把当前数和.都删掉,需要非常细心。
class Solution {
public:
    vector<string> res;
    string track;
    vector<string> restoreIpAddresses(string s) {
        backtrack(s, 4, 0);
        // 删除末尾的.
        for (string &i : res) {
            i.erase(i.end() - 1);
        }
        return res;
    }
    void backtrack(string s, int number, int index) {
        if (number == 0) {
            if (index == s.size())
                res.push_back(track);
            return;
        }
        int indexSize = 2;
        if (s[index] == '0') indexSize = 0;
        // 用窗口去包数[index, index + indexSize]
        for (int i = index; i <= index + indexSize; i++) {
            if (i >= s.size()) break;
            string ip = s.substr(index, i - index + 1);
            if (atoi(ip.c_str()) > 255) break;
            track += ip;
            track += '.';
            backtrack(s, number - 1, i + 1);
            track.erase(track.size() - ip.size() - 1, ip.size() + 1);
        }
    }
};

401. 二进制手表 Easy 回溯 一起点灯 2023/2/24

二进制手表顶部有 4 个 LED 代表 小时(0-11),底部的 6 个 LED 代表 分钟(0-59)。每个 LED 代表一个 0 或 1,最低位在右侧。
给你一个整数 turnedOn ,表示当前亮着的 LED 的数量,返回二进制手表可以表示的所有可能时间。你可以 按任意顺序 返回答案。
小时不会以零开头:
例如,“01:00” 是无效的时间,正确的写法应该是 “1:00” 。
分钟必须由两位数组成,可能会以零开头:
例如,“10:2” 是无效的时间,正确的写法应该是 “10:02” 在这里插入图片描述

常规的回溯无法处理这种 小时+分钟 分别回溯的问题,需要分开回溯,非常麻烦。
可以把这十个灯想象成交替排列的 小时和分钟 灯,即。
小时灯 1 , 2 , 4 , 8 , 0 , 0 , 0 , 0 , 0 , 0 1, 2, 4, 8, 0, 0, 0, 0, 0, 0 1,2,4,8,0,0,0,0,0,0
分钟灯 0 , 0 , 0 , 0 , 1 , 2 , 4 , 8 , 16 , 32 0, 0, 0, 0, 1, 2, 4, 8, 16, 32 0,0,0,0,1,2,4,8,16,32
每次只点亮其中一盏灯,并可轻松实现回溯,其中backtrack函数的参数为:待点亮的灯个数,从哪个灯开始试点,当前小时数,当前分钟数。
本题的回溯没有 选择和取消选择过程,因为是直接向下递归的。

class Solution {
public:
    vector<int> hours = {1, 2, 4, 8, 0, 0, 0, 0, 0, 0};
    vector<int> mins = {0, 0, 0, 0, 1, 2, 4, 8, 16, 32};
    vector<string> res;
    vector<string> readBinaryWatch(int turnedOn) {
        backtrack(turnedOn, 0, 0, 0);
        return res;
    }
    void backtrack(int turnedOn, int index, int hour, int min) {
        // 无效时间
        if (hour >= 12 || min >= 60) return;
        // 点完灯了
        if (turnedOn == 0) {
            string s = to_string(hour) + ":";
            if (min < 10) s += "0";
            s += to_string(min);
            res.push_back(s);
        }
        // 从index开始,挨个试点亮灯
        for (int i = index; i < 10; i++)
            backtrack(turnedOn - 1, i + 1, hour + hours[i], min + mins[i]);
    }
};

还有一种思路也非常巧妙,利用了这些灯表示的数值都是二进制的性质,不可能有不同的灯表示相同的时间。
使用暴力穷举所有时间,并计算当前时间所需要的灯数量是否等于亮灯数量。
当前时间的灯数恰好为当前小时/分钟二进制含1的个数

class Solution {
public:
    vector<string> readBinaryWatch(int turnedOn) {
        vector<string> res;
        for (int i = 0; i < 12; i++) {
            for (int j = 0; j < 60; j++) {
                if (count1(i) + count1(j) == turnedOn) {
                    res.push_back(to_string(i) + ":" + (j < 10 ? "0" + to_string(j) : to_string(j)));
                }
            }
        }
        return res;
    }
    int count1(int n) {
        int res = 0;
        while (n != 0) {
            res += (n & 1);
            n >>= 1;
        }
        return res;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值