代码随想录1刷--day7回溯

回溯

基础:

回溯常用于解决

  • 组合
    • 77.组合
    • 17.电话号码的字母组合
    • 39.组合总和
    • 40.组合总和2
    • 216.组合总和3
  • 分割
    • 131.分割回文串
    • 93.复原IP地址
  • 子集
    • 78.子集
    • 90.子集2
  • 排列
    • 46.全排列
    • 47.全排列2
  • 棋盘问题
    • 51.N皇后
    • 37.解数独
  • 其他
    • 491.递增子序列
    • 332.重新安排行程

回溯其实就是暴力搜索,回溯是递归的副产品,只要有递归就有回溯
回溯三部曲:

  1. 确定回溯函数返回值以及参数
void backtracking(参数)
  1. 回溯函数终止条件
if(终止条件)
{
    存放结果;
    return;
}
  1. 回溯搜索的遍历过程
for(选择:本层集合中的元素)
{
    处理节点;
    backtracking(路径,选择列表);   // 递归
    回溯,撤销处理结果;
}

整体框架如下:

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

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

组合问题

  • 77:组合
class Solution {
private:
    vector> result;
    vector 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();        // 回退
        }
        return;
    }
public:
    vector> combine(int n, int k) {
        result.clear();
        path.clear();
        backtracking(n,k,1);
        return result;
    }
};
  • 216:组合总和III
class Solution {
private:
    vector> result;
    vector path;
    void backtracking(int k, int target, int sum, int startIndex)     // 确定要传递的参数和返回值
    {
        if(sum>target)      // 这里设计一部分剪枝
            return;
        // 确定终止条件并保存结果
        if(path.size()==k) 
        {
            if(sum==target)
                result.push_back(path);
            return;
        }
        // 回溯搜索过程
        for(int i = startIndex; i<=9-(k-path.size())+1;i++)  // 这里涉及一部分剪枝
        {
            sum+=i;
            path.push_back(i);
            backtracking(k,target,sum,i+1);
            sum-=i; // 别忘了减
            path.pop_back();
        }
         return;
    }
public:
    vector> combinationSum3(int k, int n) {
        backtracking(k,n,0,1);
        return result;
    }
};
  • 电话号码的字母组合
    时间复杂度: O(3^m * 4^n),其中 m 是对应四个字母的数字个数,n 是对应三个字母的数字个数
    空间复杂度:O(3^m * 4^n)
class Solution {
private:
    const string lettermap[8] ={
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz"
    };
    vector result;
    string s;
    void backtracking(const string& digits, int index)
    {
        if(index==digits.size())
        {
            result.push_back(s);
            return;
        }
        // 确定单层递归逻辑
        int dig = digits[index] - '0';      // 转化成数字
        string letters = lettermap[dig-2];   // 取字符串
        for(int i=0;i letterCombinations(string digits) {
        if(digits.size()==0)    // 对空进行处理
            return result;
        backtracking(digits,0);
        return result;
    }
};
  • 39:组合总和
    这题如果需要剪枝的话,需要对数组排序,不一定会提高效率
class Solution {
private:
    vector> result;
    vector path;
    void backtracking(vector& candidates, int target, int sum, int index)
    {
        if(sum>target)
            return;
        // 不再是数字
        if(sum==target)
        {
            result.push_back(path);
            return;
        }
        // 回溯搜索过程
        for(int i = index;i> combinationSum(vector& candidates, int target) {
        // 注意这题需要先排序,否则没办法进行剪枝
        // sort(candidates.begin(),candidates.end());
        backtracking(candidates,target,0,0);
        return result;
    }
};
  • 40:组合总和II
// 这题关键是里面存在重复的元素,所以需要添加一个map来记录,用数组即可
// 每个数字在每个组合中只能使用一次,在同一层上进行去重
// 所以可以先对数组排序,然后就可以方便对相同的元素进行去重
class Solution {
private:
    vector> result;
    vector path;
    void backtracking(vector& candidates, int target, int sum, int index, vector used)
    {
        if(sum>target)
            return;
        
        if(sum==target)
        {
            result.push_back(path);
            return;
        }
        // 单层递归逻辑
        for(int i=index; i0 && candidates[i]==candidates[i-1] && used[i-1]==false)
                continue;
            used[i] = true;
            sum+=candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates,target,sum,i+1,used);    // 注意因为不能重复
            sum-=candidates[i];
            path.pop_back();
            used[i] = false;
        }
    }
public:
    vector> combinationSum2(vector& candidates, int target) {
        vector used(candidates.size(),false);
        sort(candidates.begin(),candidates.end());
        backtracking(candidates,target,0,0,used);
        return result;
    }
};

分割

  • 131:分割回文串
class Solution {
private:
    vector> result;
    vector path;
    bool isPalindrome(const string& s, int start, int end)
    {
        for(int i=start, j=end; i=s.size())
        {
            result.push_back(path);
            return;
        }
        // 单层搜索逻辑
        for(int i=index; i> partition(string s) {
        backtracking(s,0);
        return result;
    }
};
  • 93.复原ip地址
// 时间复杂度O(3^4)
// 空间复杂度O(n)
class Solution {
private:
    vector result;
    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++) // 不能大于255
        {
            if(s[i]>'9' || s[i]<'0')  return false;
            num = num*10+(s[i]-'0');
        }
        if(num>255)
            return false;
        return true;
    }
    void backtracking(string& s, int index, int num)
    {
        if(num==3)  // 已经插入3个.
        {
            if(isValid(s,index,s.size()-1)) // 需要检查第四段是否合法
                result.push_back(s);
            return;
        }
        // 单层搜索逻辑
        for(int i=index;i restoreIpAddresses(string s) {
        if(s.size()<13)         // 容易忽视
            backtracking(s,0,0);
        return result;
    }
};

子集问题

组合问题和分割问题都是收集树的叶子节点,而子集问题是找到树的所有节点。

  • 78:子集
class Solution {
private:
    vector> result;
    vector path;
    void backtracking(vector& nums, int index)
    {
        // 因为每个节点都存,这里不需要判断index> subsets(vector& nums) {
        backtracking(nums,0);
        return result;
    }
};
  • 90:子集II
// 提高难度,需要排除重复的,所以引入一个map,但是需要先对nums排序,和前面的排列组合问题一个思路
// 排序O(nlogn),子集最多2^n个,每个子集加入答案时需要拷贝一份,耗时O(n),最后时间复杂度为O(n*2^n);
// 空间复杂度O(n)
class Solution {
private:
    vector> result;
    vector path;
    void backtracking(vector& nums, int index, vector& used)
    {
        result.push_back(path);

        // 单层搜索逻辑
        for(int i=index; i0 && nums[i]==nums[i-1] && used[i-1]==false)
                continue;
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums,i+1,used);
            used[i]=false;
            path.pop_back();
        }
        return;
    }
public:
    vector> subsetsWithDup(vector& nums) {
        sort(nums.begin(),nums.end());
        vector used(nums.size(),false);
        backtracking(nums,0,used);
        return result;
    }
};

排列问题

排列问题也是递归到叶子节点

  • 46:全排列
// 时间复杂度O(n*n!)
// 空间复杂度O(n)
class Solution {
private:
    vector> result;
    vector path;
    void backtracking(vector& nums, vector& used)
    {
        if(path.size()==nums.size())
        {
            result.push_back(path);
            return;
        }
        // 单层搜索逻辑
        for(int i=0;i> permute(vector& nums) {
        vector used(nums.size(),false);
        backtracking(nums,used);
        return result;
    }
};
  • 47:全排列II
// 时间复杂度O(n*n!)
// 空间复杂度O(n)
class Solution {
private:
    vector> result;
    vector path;
    void backtracking(vector& nums, vector& used)
    {
        if(path.size()==nums.size())
        {
            result.push_back(path);
            return;
        }
        // 单层搜索逻辑
        for(int i=0; i0 && nums[i]==nums[i-1] && used[i-1]==false) || used[i]==true)
                continue;
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums,used);
            used[i] = false;
            path.pop_back();
        }
    }
public:
    vector> permuteUnique(vector& nums) {
        sort(nums.begin(),nums.end());
        vector used(nums.size(),false);
        backtracking(nums,used);
        return result;
    }
};

棋盘问题

  • 51:N皇后
class Solution {
private:
    vector> result;
    bool isvalid(int row, int col, vector& chessboard,int n)
    {
        // 检查列
        for(int i=0;i=0&&i>=0;j--,i--)
        {
            if(chessboard[i][j]=='Q')
                return false;
        }

        // 检查135°
        for(int i=row-1,j=col+1;i>=0&&j& chessboard)
    {
        if(row==n)
        {
            result.push_back(chessboard);
            return;
        }
        // 单层的搜索逻辑
        for(int col=0; col> solveNQueens(int n) {
        vector chessboard(n,string(n,'.'));
        backtracking(0,n,chessboard);
        return result;
    }
};
  • 37:解数独
class Solution {
private:
    bool isValid(int row, int col, char val, vector>& board)
    {
        // 判断行内是否重复
        for(int i=0; i<9; i++)
        {
            if(board[row][i]==val)
                return false;
        }
        // 判断列内是否重复
        for(int j=0; j<9; j++)
        {
            if(board[j][col]==val)
                return false;
        }
        // 判断3*3宫格内是否重复
        int startRow = (row/3)*3;
        int startCol = (col/3)*3;
        for(int i=startRow; i>& board)
    {
        // 遍历整个树形结构找到可能的叶子节点就返回,不需要终止条件
        // 递归单层逻辑,需要一个二维的递归
        for(int i=0; i>& board) {
        backtracking(board);
    }
};

其他

  • 491:递增子序列
    这题由于nums是无序的,不能对其排序,所以无法使用子集II那种方法来排除重复,用set来做或者数组来做是更好的选择,u1s1,也更好理解
class Solution {
private:
    vector> result;
    vector path;
    void backtracking(vector& nums, int index)
    {
        // 第二层开始的所有节点
        if(path.size()>1) // 将合法的结果存
            result.push_back(path);

        // unordered_set used;
        // 用数组更好
        vector used(201,false);
        // 单层搜索逻辑,也要考虑重复问题
        for(int i=index; i> findSubsequences(vector& nums) {
        // vector used(nums.size(),false);   // 惯性思维,nums是乱序的呀
        backtracking(nums,0);
        return result;
    }
};
  • 332:重新安排行程(hard),难啊
// 这个题可以用回溯来解决
// 需要借助一个映射来记录起点和终点,并且要记录从起点到终点的剩余票数
// unordered_map> targets; //unordered_map<出发机场,map<到达机场,航班数>>
class Solution {
private:
// 要找到一个对数据进行排序的容器,而且还要容易增删元素,迭代器还不能失效。
// 选择了unordered_map> targets 来做机场之间的映射。
    unordered_map> targets;
    bool backtracking(int ticketNum, vector& result)
    {
        if(result.size() == ticketNum+1)    // 结束行程时,经过的机场个数==航班数+1
            return true;
        
        // 单层遍历逻辑
        for(pair& target : targets[result[result.size()-1]]) // 遍历所有以result最后一个机场为起点的所有航班,这里用到迭代器
        {
            // 判断一下这个航班是否飞过
            if(target.second > 0)
            {
                result.push_back(target.first);
                target.second--;
                if(backtracking(ticketNum,result))  return true;
                result.pop_back();
                target.second++;
            }
        }
        return false;
    }
public:
    vector findItinerary(vector>& tickets) {
        vector result;
        // 建立映射关系
        for(const vector& vec:tickets)
            targets[vec[0]][vec[1]]++;
        
        result.push_back("JFK");    // 别忘了定义初始
        backtracking(tickets.size(),result);
        return result;
    }
};

你可能感兴趣的:(算法,开发语言)