Bootstrap

回溯总结1(组合问题)

重复的元素等效于一个,比如 {4,5} 和 {5,4} 是同一个子集。
需要对每次进行元素选取的集合进行修改的话就需要使用index进行起始位置修改
dfs的每一层都是对当前集合的遍历
可能还需要使用数组辅助进行已使用的元素标记
n个集合求组合

n个集合求组合

给你一个例子,假设有3个集合:

set1 = {1, 2}
set2 = {a, b}
set3 = {x, y}

使用回溯法求组合的代码如下:

class Solution {
private:
    vector<vector<string>> res;  // 存储最终结果
    vector<string> path;         // 当前组合路径
    
    void backtrack(vector<vector<string>>& sets, int depth) {
        // 终止条件:已经选择了k个元素(每个集合选一个)
        if (depth == sets.size()) {
            res.push_back(path);
            return;
        }
        
        // 遍历当前集合的所有元素
        for (int i = 0; i < sets[depth].size(); i++) {
            // 选择当前元素
            path.push_back(sets[depth][i]);
            // 递归到下一个集合
            backtrack(sets, depth + 1);
            // 回溯,撤销选择
            path.pop_back();
        }
    }
    
public:
    vector<vector<string>> combine(vector<vector<string>>& sets) {
        backtrack(sets, 0);
        return res;
    }
};

解释一下搜索过程:

  1. depth = 0(第一层):
    • 选择set1中的1或2
  2. depth = 1(第二层):
    • 选择set2中的a或b
  3. depth = 2(第三层):
    • 选择set3中的x或y

对于上面的例子,会生成如下组合:

[1,a,x], [1,a,y], [1,b,x], [1,b,y]
[2,a,x], [2,a,y], [2,b,x], [2,b,y]

这里depth(深度)代表:

  • 当前正在处理第几个集合
  • 当depth等于集合数量时,表示找到一个完整组合

总结:

  1. 每层代表一个集合的选择
  2. 每层的选择是独立的,不需要去重
  3. 路径长度等于集合数量时得到一个完整组合

77 组合(组合型枚举)

链接
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。 示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]

思路

image.png

  1. 需要循环(每一个集合的大小)
  2. 回溯函数的参数确定
    • 需要索引
    • 当路径长度等于要求的k时,说明找到一个合法组合
    • n是可选的
  3. 深度指的当前path的大小
class Solution {
public:
    vector<vector<int>> res;
    void dfs(vector<int> &path, int start, int n, int k) {
        if(path.size() == k) {
            res.push_back(path);  
            return;
        }
        for(int i = start; i <= n; i++) {  
            path.push_back(i);  // 应该是将 i 加入 path,而不是加入 res
            dfs(path, i + 1, n, k);
            path.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        vector<int> path;
        dfs(path, 1, n, k);  // start 应该从 1 开始,因为题目要求从 1 开始
        return res;
    }
};

image.png
剪枝

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int n, int k, int startIndex) {
        if (path.size() == k) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方
            path.push_back(i); // 处理节点
            backtracking(n, k, i + 1);
            path.pop_back(); // 回溯,撤销处理的节点
        }
    }
public:

    vector<vector<int>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
};

39 组合总和

链接
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:

  • 所有数字(包括 target)都是正整数。
  • 解集不能包含重复的组合。
    示例 1:
  • 输入:candidates = [2,3,6,7], target = 7,
  • 所求解集为: [ [7], [2,2,3] ]
    示例 2:
  • 输入:candidates = [2,3,5], target = 8,
  • 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]

思路

  1. 怎么达到可以重复选,已知dfs的index参数即对每一个集合的大小进行约束,那么使得index=i即可
  2. 每一层的sum可以使用局部变量进行传递,即sum+candidate[i]
class Solution {
public:
    vector<vector<int>> res;
    void backtracing(vector<int>& candidates,vector<int>& path,int index,int sum,int target){
        if(sum == target){
            res.push_back(path);
            return;
        }
        if(sum>target) return;
        for(int i = index;i<candidates.size();i++){
            path.push_back(candidates[i]);
	        backtracing(candidates,path,i,sum+candidates[i],target);
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<int> path;
        int sum = 0;
        int index = 0;
        backtracing(candidates,path,index,sum,target);
        return res;
    }

};

40 组合总和2

链接
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次
说明: 所有数字(包括目标数)都是正整数。解集不能包含重复的组合。

  • 示例 1:
  • 输入: candidates = [10,1,2,7,6,1,5], target = 8,
  • 所求解集为:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]
  • 示例 2:
  • 输入: candidates = [2,5,2,1,2], target = 5,
  • 所求解集为:
[
  [1,2,2],
  [5]
]

思路

对同一层的树进行去重
对纵向的要使用index控制不使用同一个元素
if (i > startIndex && candidates[i] == candidates[i - 1]) {continue;}
这一步比较难想,但是如果要跳过相同的就需要一开始就对数组进行排序
不用担心深度搜索的时候又遇到1,因为是去重后才进行dfs,比如{1,1,1,…}不需要担心在跳过2个1后dfs还会遇到1

  • 第一层(startIndex=0):
    • 选第一个1:继续搜索 [1,…]
    • 跳过第二个1(因为重复)
    • 选2:继续搜索 [2,…]
    • 选5:继续搜索 [5,…]
  • 当选了第一个1后,进入第二层(startIndex=1):
    • 此时可以选第二个1(因为i=startIndex)
    • 可以选2
    • 可以选5
  • 当选了第二个1后,进入第三层(startIndex=2):
    • 可以选2
    • 可以选5
      同一层不会选择重复的数字 不同层次可以选择相同的数字 避免产生重复的组合
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum == target) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            // 要对同一树层使用过的元素进行跳过
            if (i > startIndex && candidates[i] == candidates[i - 1]) {
                continue;
            }
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i + 1); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次
            sum -= candidates[i];
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        path.clear();
        result.clear();
        // 首先把给candidates排序,让其相同的元素都挨在一起。
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0);
        return result;
    }
};


216 组合总和3

简化版的组合总和1
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:

  • 所有数字都是正整数。
  • 解集不能包含重复的组合。
    示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
    示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]

思路

class Solution {
private:
    vector<vector<int>> result; // 存放结果集
    vector<int> path; // 符合条件的结果
    // targetSum:目标和,也就是题目中的n。
    // k:题目中要求k个数的集合。
    // sum:已经收集的元素的总和,也就是path里元素的总和。
    // startIndex:下一层for循环搜索的起始位置。
    void backtracking(int targetSum, int k, int sum, int startIndex) {
        if (path.size() == k) {
            if (sum == targetSum) result.push_back(path);
            return; // 如果path.size() == k 但sum != targetSum 直接返回
        }
        for (int i = startIndex; i <= 9; i++) {
            sum += i; // 处理
            path.push_back(i); // 处理
            backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
            sum -= i; // 回溯
            path.pop_back(); // 回溯
        }
    }

public:
    vector<vector<int>> combinationSum3(int k, int n) {
        result.clear(); // 可以不加
        path.clear();   // 可以不加
        backtracking(n, k, 0, 1);
        return result;
    }
};

17 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
17.电话号码的字母组合
示例:

  • 输入:“23”
  • 输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
    说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

思路

其实就是组合不过组合的集合需要另外求取不是一开始就给定
多个集合求组合

for (int i = startIndex; i <= 9; i++) {
            sum += i; // 处理
            path.push_back(i); // 处理
            backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
            sum -= i; // 回溯
            path.pop_back(); // 回溯
        }

再进行不同集合求组合之前需要先取出集合

const string& letters = letterMap[digits[index] - '0'];
		const string& letters = letterMap[digits[index] - '0'];
        // 遍历当前数字对应的所有字母
        for (char letter : letters) {
            path.push_back(letter);
            backtracking(digits, index + 1, path, res);
            path.pop_back();
        }
class Solution {
private:
    const vector<string> letterMap = {
       "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
    };
public:
    void backtracking(const string& digits, int index, string& path, vector<string>& res) {
        if (index == digits.size()) {
            res.push_back(path);
            return;
        }
        // 获取当前数字对应的字母集合
        const string& letters = letterMap[digits[index] - '0'];
        // 遍历当前数字对应的所有字母
        for (char letter : letters) {
            path.push_back(letter);
            backtracking(digits, index + 1, path, res);
            path.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
        if (digits.empty()) return {};
        // 预计算结果大小
        vector<string> res;
        res.reserve(pow(4, digits.size())); // 最大可能有4个字母的情况
        string path;
        path.reserve(digits.size()); // 预分配路径大小
        backtracking(digits, 0, path, res);
        return res;

    }

};

复原IP地址

链接
有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

  • 例如:"0.1.2.201""192.168.1.1"有效 IP 地址,但是 "0.011.255.245""192.168.1.312""[email protected]"无效 IP 地址。
    给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
    示例 1:
    输入:s = “25525511135”
    输出:
    [“255.255.11.135”,“255.255.111.35”]

示例 2:
输入:s = “0000”
输出:
[“0.0.0.0”]

示例 3:
输入:s = “101023”
输出:
[“1.0.10.23”,“1.0.102.3”,“10.1.0.23”,“10.10.2.3”,“101.0.2.3”]

提示:

  • 1 <= s.length <= 20
  • s 仅由数字组成

思路

引用
image.png

随想录
for (int i = startIndex; i < s.size(); i++)循环中 [startIndex, i] 这个区间就是截取的子串,需要判断这个子串是否合法。
如果合法就在字符串后面加上符号.表示已经分割。
如果不合法就结束本层循环,如图中剪掉的分支:
然后就是递归和回溯的过程:
递归调用时,下一层递归的startIndex要从i+2开始(因为需要在字符串中加入了分隔符.),同时记录分割符的数量pointNum 要 +1。
回溯的时候,就将刚刚加入的分隔符. 删掉就可以了,pointNum也要-1。
最后就是在写一个判断段位是否是有效段位了。
主要考虑到如下三点:

  • 段位以0为开头的数字不合法
  • 段位里有非正整数字符不合法
  • 段位如果大于255了不合法
class Solution {
private:
    vector<string> result;// 记录结果
    // startIndex: 搜索的起始位置,pointNum:添加逗点的数量
    void backtracking(string& s, int startIndex, int pointNum) {
        if (pointNum == 3) { // 逗点数量为3时,分隔结束
            // 判断第四段子字符串是否合法,如果合法就放进result中
            if (isValid(s, startIndex, s.size() - 1)) {
                result.push_back(s);
            }
            return;
        }
        for (int i = startIndex; i < s.size(); i++) {
            if (isValid(s, startIndex, i)) { // 判断 [startIndex,i] 这个区间的子串是否合法
                s.insert(s.begin() + i + 1 , '.');  // 在i的后面插入一个逗点
                pointNum++;
                backtracking(s, i + 2, pointNum);   // 插入逗点之后下一个子串的起始位置为i+2
                pointNum--;                         // 回溯
                s.erase(s.begin() + i + 1);         // 回溯删掉逗点
            } else break; // 不合法,直接结束本层循环
        }
    }
    // 判断字符串s在左闭右闭区间[start, end]所组成的数字是否合法
    bool isValid(const string& s, int start, int end) {
        if (start > end) {
            return false;
        }
        if (s[start] == '0' && start != end) { // 0开头的数字不合法
                return false;
        }
        int num = 0;
        for (int i = start; i <= end; i++) {
            if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法
                return false;
            }
            num = num * 10 + (s[i] - '0');
            if (num > 255) { // 如果大于255了不合法
                return false;
            }
        }
        return true;
    }
public:
    vector<string> restoreIpAddresses(string s) {
        result.clear();
        if (s.size() < 4 || s.size() > 12) return result; // 算是剪枝了
        backtracking(s, 0, 0);
        return result;
    }
};


;