自学内容网 自学内容网

代码随想录-笔记-其六

回溯算法

模板:

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

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

回溯算法本质上是穷举方法,并不是很聪明的算法,所以基本只在特定的无其他方法可用的前提下使用,包括组合问题,分割问题,子集问题等。

组合问题

77. 组合 - 力扣(LeetCode)

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

你可以按 任何顺序 返回答案。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> temp;
    void backtrack(int n, int k,int index){
        if(temp.size()==k){
            res.push_back(temp);
            return;
        }
        for(int i=index;i<=n;++i){
            temp.push_back(i);
            backtrack(n, k,i+1);
            temp.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backtrack(n, k, 1);
        return res;
    }
};

这个题是比较标准的回溯模板题,我们只需要记录一个序号即可。

216. 组合总和 III - 力扣(LeetCode)

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次 

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> temp;
    void backtrack(int k, int n,int sum,int index){
        if(sum==n&&temp.size()==k){
            res.push_back(temp);
            return;
        }
        if(sum>n||temp.size()>k)return;
        for(int i=index;i<=9;i++){
            temp.push_back(i);
            backtrack(k, n, sum+i,i+1);
            temp.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backtrack(k, n, 0, 1);
        return res;
    }
};

这个题相比起之前的题来说是多了三个限制条件的回溯算法题,一是要求和为n,二是要求数字的个数为k,三就是要求数字在1到9之间,不过本身逻辑并没有更加复杂,我们只需要依然按着模板来写即可。

17. 电话号码的字母组合 - 力扣(LeetCode)

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

class Solution {
public:
    unordered_map<char,string> mp=
    {
        {'2',"abc"},{'3',"def"},{'4',"ghi"},{'5',"jkl"},{'6',"mno"},{'7',"pqrs"},{'8',"tuv"},{'9',"wxyz"}
    };
    vector<string> res;
    string temp;
    void backtrack(string digits,int index){
        if(index==digits.size()){
            res.push_back(temp);
            return;
        }
        char digit=digits[index];
        string str=mp[digit];
        for(char ch:str){
            temp.push_back(ch);
            backtrack(digits, index+1);
            temp.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
        if(digits.size()==0)return res;
        backtrack(digits, 0);
        return res;
    }
};

如果可以的话不想再写这个题了(这个哈希表写起来太麻烦了),我们用一个哈希表构建起字符与字符串的联系,然后维护一个序号之后就进行正常的回溯过程即可。

39. 组合总和 - 力扣(LeetCode)

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> temp;
    void backtrack(vector<int>& candidates, int target, int index) {
        if (target == 0) {
            res.push_back(temp);
            return;
        }
        if (index >= candidates.size() || target < 0) {
            return;  
        }
        if (target - candidates[index] >= 0) {
            temp.push_back(candidates[index]);
            backtrack(candidates, target - candidates[index], index); 
            temp.pop_back();
        }
        backtrack(candidates, target, index + 1);
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtrack(candidates, target, 0);
        return res;
    }
};

这个题是可以允许元素重复的情况,所以我们要分两步考虑:第一步考虑只使用当前元素的情况,第二步就是将序号更新的情况。

40. 组合总和 II - 力扣(LeetCode)

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。 

class Solution {
public:
    vector<vector<int>> res;
    vector<int> temp;
    void backtrack(vector<int>& candidates, int target,int index){
        if(target==0){
            res.push_back(temp);
            return;
        }
        if(target<0||index>=candidates.size())return;
        for(int i=index;i<candidates.size();++i){
            if(i>index&&candidates[i]==candidates[i-1])continue;
            if(target-candidates[i]>=0){
                temp.push_back(candidates[i]);
                backtrack(candidates, target-candidates[i], i+1);
                temp.pop_back();
            }
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        backtrack(candidates, target, 0);
        return res;
    }
};

这个是需要考虑情况最复杂的一题,我们需要去重,所以需要进行排序(不排序的话无法利用这段代码中的方法去重)。

分割问题

131. 分割回文串 - 力扣(LeetCode)

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

class Solution {
public:
    vector<vector<string>> res;
    vector<string> temp;
    vector<vector<int>> f;
    vector<vector<string>> partition(string s) {
        int n=s.size();
        f.assign(n,vector<int>(n,true));
        for(int i=n-1;i>=0;--i){
            for(int j=i+1;j<n;++j){
                f[i][j]=(s[i]==s[j]&&f[i+1][j-1]);
            }
        }
        backtrack(s, 0);
        return res;
    }
    void backtrack(string s,int index){
        if(index==s.size()){
            res.push_back(temp);
            return;
        }
        for(int i=index;i<s.size();i++){
            if(f[index][i]){
                temp.push_back(s.substr(index,i-index+1));
                backtrack(s, i+1);
                temp.pop_back();
            }
        }
    }
};

子集问题

78. 子集 - 力扣(LeetCode)

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

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

class Solution {
public:
    vector<vector<int>> res;
    vector<int> temp;
    void backtrack(vector<int>& nums,int index){
        res.push_back(temp);
        for(int i=index;i<nums.size();++i){
            temp.push_back(nums[i]);
            backtrack(nums, i+1);
            temp.pop_back();
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        backtrack(nums, 0);
        return res;
    }
};

对于所有的子集,我们每一次递归调用backtrack的最开始都将temp加入到res即可,由于原数组中不包含重复的元素,我们回溯的过程会自然而然地去掉重复的子集。

90. 子集 II - 力扣(LeetCode)

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。

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

class Solution {
public:
    vector<vector<int>> res;
    vector<int> temp;
    void backtrack(vector<int>& nums,int index,vector<bool>& used){
        res.push_back(temp);
        if(index==nums.size()){
            return;
        }
        for(int i=index;i<nums.size();++i){
            if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false){
                continue;
            }
            used[i]=true;
            temp.push_back(nums[i]);
            backtrack(nums, i+1, used);
            temp.pop_back();
            used[i]=false;
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        vector<bool> used(nums.size(),false);
        sort(nums.begin(), nums.end());
        backtrack(nums, 0,used);
        return res;
    }
};

这个题相比起上一道子集题来说,主要是添加了对树层遍历的去重要求,所以我们需要添加一些去重的功能。

491. 非递减子序列 - 力扣(LeetCode)

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> temp;
    void backtrack(vector<int>& nums,int index){
        if(temp.size()>=2){
            res.push_back(temp);
        }
        int used[201]={0};
        for(int i=index;i<nums.size();++i){
            if(!temp.empty()&&nums[i]<temp.back()||used[nums[i]+100]==1){
                continue;
            }
            used[nums[i]+100]=1;
            temp.push_back(nums[i]);
            backtrack(nums, i+1);
            temp.pop_back();
        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        backtrack(nums, 0);
        return res;
    }
};

这个题相比起之前的主要区别在于:他要求顺序上的一贯,因此我们不能直接使用sort函数对原数组的顺序进行变动,所以我们就需要一个哈希表来对每一个树枝(每一个递归过程)使用过的数字进行记录。用数组记录的运算速度会显著提升,但问题在于数组的索引大小(数组的索引不能为负),所以我们根据给的数的范围将其修正为非负数即可。

排列问题

46. 全排列 - 力扣(LeetCode)

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

class Solution {
public:
    vector<vector<int>> res;
    vector<int> temp;
    void backtrack(vector<int>& nums,vector<bool>& used){
        if(temp.size()==nums.size()){
            res.push_back(temp);
            return;
        }
        for(int i=0;i<nums.size();++i){
            if(used[i]==true){
                continue;
            }
            used[i]=true;
            temp.push_back(nums[i]);
            backtrack(nums, used);
            temp.pop_back();
            used[i]=false;
        }
    }
    vector<vector<int>> permute(vector<int>& nums) 
    {
        vector<bool> used(nums.size(),false);
        backtrack(nums, used);
        return res;
    }
};

排列问题相比起子集问题有两个显著的差别:回溯中的序号变化都是从0开始(backtrack函数中的for循环),以及去重的时候检查的元素也显然不同(子集问题中我们要检查的是树层的重复元素,排列的时候我们要检查的是树枝的重复元素)。

47. 全排列 II - 力扣(LeetCode)

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> temp;
    void backtrack(vector<int>& nums,vector<bool>& used){
        if(temp.size()==nums.size()){
            res.push_back(temp);
            return;
        }
        for(int i=0;i<nums.size();++i){
            if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false){
                continue;
            }
            if(used[i]==false){
                used[i]=true;
                temp.push_back(nums[i]);
                backtrack(nums, used);
                temp.pop_back();
                used[i]=false;
            }
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<bool> used(nums.size(),false);
        sort(nums.begin(), nums.end());
        backtrack(nums, used);
        return res;
    }
};

不难发现,当原数组包含重复元素时,我们常常考虑使用used数组(其实是一个bool类型的容器)来记录是否使用过重复元素。

一些困难题

332. 重新安排行程 - 力扣(LeetCode)

给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

  • 例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前。

假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。

class Solution {
public:
    unordered_map<string,map<string,int>> mp;
    bool backtrack(int ticketNum,vector<string>& res){
        if(ticketNum+1==res.size())return true;
        for(pair<const string,int>& temp:mp[res.back()]){
            if(temp.second>0){
                temp.second--;
                res.push_back(temp.first);
                if(backtrack(ticketNum, res))return true;
                res.pop_back();
                temp.second++;
            }
        }
        return false;
    }
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        vector<string> res;
        for(vector<string>& vec:tickets){
            mp[vec[0]][vec[1]]++;
        }
        res.push_back("JFK");
        backtrack(tickets.size(), res);
        return res;
    }
};

这个题还是有一定难度的,本质上其实是dfs算法,但是我们需要用回溯来穷举所有的可能的行程。

51. N 皇后 - 力扣(LeetCode)

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

class Solution {
public:
    vector<vector<string>> res;
    void backtrack(int n,int row,vector<string>& chessboard){
        if(row==n){
            res.push_back(chessboard);
            return;
        }
        for(int col=0;col<n;++col){
            if(isVaild(row, col, chessboard, n)){
                chessboard[row][col]='Q';
                backtrack(n, row+1, chessboard);
                chessboard[row][col]='.';
            }
        }
    }
    bool isVaild(int row,int col,vector<string>& chessboard,int n){
        for(int i=0;i<row;++i){
            if(chessboard[i][col]=='Q')return false;
        }
        for(int i=row-1,j=col-1;i>=0&&j>=0;--i,--j){
            if(chessboard[i][j]=='Q')return false;
        }
        for(int i=row-1,j=col+1;i>=0&&j<n;--i,++j){
            if(chessboard[i][j]=='Q')return false;
        }
        return true;
    }
    vector<vector<string>> solveNQueens(int n) {
        res.clear();
        vector<string> chessboard(n,string(n,'.'));
        backtrack(n, 0, chessboard);
        return res;
    }
};

大名鼎鼎的N皇后问题,核心就在于如何检测在同行、同列、45度方向与135度方向是否已存在棋子。

37. 解数独 - 力扣(LeetCode)

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

class Solution {
public:
    bool backtrack(vector<vector<char>>& board){
        for(int i=0;i<board.size();++i){
            for(int j=0;j<board[0].size();++j){
                if(board[i][j]=='.'){
                    for(char k='1';k<='9';++k){
                        if(isvaild(i,j,k,board)){
                            board[i][j]=k;
                            if(backtrack(board))return true;
                            board[i][j]='.';
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    }
    bool isvaild(int row,int col,char val,vector<vector<char>>& board){
        for(int i=0;i<9;++i){
            if(board[row][i]==val)return false;
        }
        for(int i=0;i<9;++i){
            if(board[i][col]==val)return false;
        }
        int startRow=(row/3)*3,startCol=(col/3)*3;
        for(int i=startRow;i<startRow+3;++i){
            for(int j=startCol;j<startCol+3;++j){
                if(board[i][j]==val)return false;
            }
        }
        return true;
    }
    void solveSudoku(vector<vector<char>>& board) {
        backtrack(board);
    }
};


原文地址:https://blog.csdn.net/lastXuanGod/article/details/144212587

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!