代码随想录-笔记-其六

回溯算法

模板:

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

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

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

组合问题

77. 组合 - 力扣(LeetCode)

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

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

cpp 复制代码
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)

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

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

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

cpp 复制代码
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 不对应任何字母。

cpp 复制代码
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 个。

cpp 复制代码
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 中的每个数字在每个组合中只能使用 一次

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

cpp 复制代码
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 所有可能的分割方案。

cpp 复制代码
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 ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

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

cpp 复制代码
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 ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。

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

cpp 复制代码
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 ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

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

cpp 复制代码
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 ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

cpp 复制代码
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按任意顺序 返回所有不重复的全排列。

cpp 复制代码
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"] 相比就更小,排序更靠前。

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

cpp 复制代码
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''.' 分别代表了皇后和空位。

cpp 复制代码
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 宫内只能出现一次。(请参考示例图)

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

cpp 复制代码
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);
    }
};
相关推荐
一只侯子17 分钟前
Face AE Tuning
图像处理·笔记·学习·算法·计算机视觉
whale fall3 小时前
【剑雅14】笔记
笔记
星空的资源小屋4 小时前
跨平台下载神器ArrowDL,一网打尽所有资源
javascript·笔记·django
Xudde.4 小时前
Quick2靶机渗透
笔记·学习·安全·web安全·php
AA陈超5 小时前
Git常用命令大全及使用指南
笔记·git·学习
愚戏师6 小时前
Python3 Socket 网络编程复习笔记
网络·笔记
降临-max7 小时前
JavaSE---网络编程
java·开发语言·网络·笔记·学习
大白的编程日记.7 小时前
【计算网络学习笔记】MySql的多版本控制MVCC和Read View
网络·笔记·学习·mysql
IMPYLH8 小时前
Lua 的 require 函数
java·开发语言·笔记·后端·junit·lua
YJlio12 小时前
进程和诊断工具学习笔记(8.29):ListDLLs——一眼看清进程里加载了哪些 DLL,谁在偷偷注入
android·笔记·学习