力扣(leetcode)题目总结——递归/回溯篇

leetcode 经典题分类

  • 链表
  • 数组
  • 字符串
  • 哈希表
  • 二分法
  • 双指针
  • 滑动窗口
  • 递归/回溯
  • 动态规划
  • 二叉树
  • 辅助栈

本系列专栏:点击进入 leetcode题目分类 关注走一波


前言:本系列文章初衷是为了按类别整理出力扣(leetcode)最经典题目,一共100多道题,每道题都给出了非常详细的解题思路算法步骤代码实现。很多同学刚开始刷题都是按照力扣顺序刷题,其实这样对新手不太适用,刷题效果也很不好。因为力扣题目顺序是随机的,并没有按照算法分类,导致同一类型的算法强化训练不够,最后刷完也是迷迷糊糊的。所以本系列文章就是来帮你完成算法分类,针对每种算法做强化训练,保证让你以后遇到题目直接秒杀!


电话号码的字母组合

【题目描述】

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

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

在这里插入图片描述

【输入输出实例】

示例 1:

输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]

示例 2:

输入:digits = “”
输出:[]

示例 3:

输入:digits = “2”
输出:[“a”,“b”,“c”]

【算法思路】

首先使用哈希表存储每个字符数字对应的所有可能的字母,然后进行回溯操作。

回溯过程中维护一个字符串,表示已有的字母排列,该字符串初始为空。每次取电话号码的一位数字,从哈希表中获得该数字对应的所有可能的字母,并将其中的一个字母插入到已有的字母排列后面,然后继续处理电话号码的后一位数字,直到处理完电话号码中的所有数字,即得到一个完整的字母排列。然后进行回退操作,遍历其余的字母排列。

如下为树形结构图:

在这里插入图片描述

(1)确定回溯函数参数

首先需要一个字符串path来收集叶子节点的结果,然后用一个字符串数组v保存起来。再来看参数,参数指定是有题目中给的digits,然后还要有一个参数就是int型的index。

注意这个index是记录遍历第几个数字,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。

(2)确定终止条件

例如输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。那么终止条件就是如果index等于输入的数字个数(digits.size),然后收集结果,结束本层递归。

(3)确定单层遍历逻辑

首先要取index指向的数字,并找到对应的字符集(手机键盘的字符集)。注意单层遍历时的for循环,可不像是在回溯算法:求组合问题组合总和中从startIndex开始遍历的。

因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而求组合组合总和都是是求同一个集合中的组合。

【算法描述】

vector<string> letterCombinations(string digits) 
{
    vector<string> v;    //存放所有的字母组合
    if(!digits.size())
    {
        return v;
    }
    string path;    //选择路径
    unordered_map<char, string> hashtable = {    //存放数字到字母的映射
        {'2',"abc"},
        {'3',"def"},
        {'4',"ghi"},
        {'5',"jkl"},
        {'6',"mno"},
        {'7',"pqrs"},
        {'8',"tuv"},
        {'9',"wxyz"},
    };
    DFS(v, path, digits, hashtable, 0);    //回溯算法
    return v;
}

void DFS(vector<string>& v, string& path, const string& digits, unordered_map<char, string>& hashtable, int index)    //index表示digits中的下标
{
    //递归结束条件:digits中所有的数字都处理完
    if(index == digits.size())  
    {
        v.push_back(path);
        return;
    }
    char digit = digits[index];    //依次选择digits中的每位数字
    string letter = hashtable[digit];    //找到该数字对应的字符串
    for(int i = 0; i < letter.size(); i++)    //遍历该字符串
    {
        path.push_back(letter[i]);
        DFS(v, path, digits, hashtable, index+1);    //index+1找digits中的下一位数字
        path.pop_back();   //回溯
    }
}

括号生成

【题目描述】

数字n代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。

【输入输出实例】

示例 1:

输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]

示例 2:

输入:n = 1
输出:[“()”]

【算法思路】

深度优先遍历

通过对左右括号的数量做减法来画出树形结构图。可以生出左枝叶的条件是:左括号剩余数量(严格)大于0;可以生出右枝叶的条件是:左括号剩余数量(严格)小于右括号剩余数量。如下:

在这里插入图片描述

画图以后,可以分析出的结论:

  • 当左右括号数量大于0个时,才产生分支;

  • 产生左分支的时候,只看当前是否还有左括号可以使用;

  • 产生右分支的时候,还受到左分支的限制,右边剩余可以使用的括号数量一定得在严格大于左边剩余数量的时候,才可以产生分支;

  • 在左边和右边剩余的括号数都等于0的时候递归结束,对应结点是有效的括号组合。

【算法描述】

//回溯算法
vector<string> generateParenthesis(int n) {
    vector<string> v;    //存放所有可能的有效括号组合
    string path;    //选择路径
    DFS(v, path, n, n);    //n对括号 = n个左括号 + n个右括号
    return v;
}

//left为左括号剩余数量,right为右括号剩余数量
void DFS(vector<string>& v, string& path, int left, int right)
{
    if(left == 0 && right == 0)    //递归结束条件
    {
        v.push_back(path);
        return;
    }
    if(left > 0)    //左括号有剩余时,可以放入左括号
    {
        left--;    //左括号数减1
        path += '(';    //放入左括号
        DFS(v, path, left, right);
        path.pop_back();    //回溯
        left++;
    }
    if(left < right)    //左括号少于右括号时,可以放入右括号
    {
        right--;    //右括号数减1
        path += ')';    //放入右括号
        DFS(v, path, left, right);
        path.pop_back();    //回溯
        right++;
    }
}

解数独

【题目描述】

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

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

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

数独部分空格内已填入了数字,空白格用 '.' 表示。题目数据 保证 输入数独仅有一个解

【输入输出实例】

示例 1:

在这里插入图片描述

输入:board =
[[“5”,“3”,“.”,“.”,“7”,“.”,“.”,“.”,“.”]
,[“6”,“.”,“.”,“1”,“9”,“5”,“.”,“.”,“.”]
,[“.”,“9”,“8”,“.”,“.”,“.”,“.”,“6”,“.”]
,[“8”,“.”,“.”,“.”,“6”,“.”,“.”,“.”,“3”]
,[“4”,“.”,“.”,“8”,“.”,“3”,“.”,“.”,“1”]
,[“7”,“.”,“.”,“.”,“2”,“.”,“.”,“.”,“6”]
,[“.”,“6”,“.”,“.”,“.”,“.”,“2”,“8”,“.”]
,[“.”,“.”,“.”,“4”,“1”,“9”,“.”,“.”,“5”]
,[“.”,“.”,“.”,“.”,“8”,“.”,“.”,“7”,“9”]]
输出:board =
[[“5”,“3”,“4”,“6”,“7”,“8”,“9”,“1”,“2”],
[“6”,“7”,“2”,“1”,“9”,“5”,“3”,“4”,“8”],
[“1”,“9”,“8”,“3”,“4”,“2”,“5”,“6”,“7”],
[“8”,“5”,“9”,“7”,“6”,“1”,“4”,“2”,“3”],
[“4”,“2”,“6”,“8”,“5”,“3”,“7”,“9”,“1”],
[“7”,“1”,“3”,“9”,“2”,“4”,“8”,“5”,“6”],
[“9”,“6”,“1”,“5”,“3”,“7”,“2”,“8”,“4”],
[“2”,“8”,“7”,“4”,“1”,“9”,“6”,“3”,“5”],
[“3”,“4”,“5”,“2”,“8”,“6”,“1”,“7”,“9”]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:

在这里插入图片描述

【算法思路】

  1. 状态压缩

    使用 bitset<9> 来压缩存储每一行、每一列、每一个 3x3 宫格中 1-9 是否出现。(其中 3x3 宫格的哈希索引可见[36、有效的数独](# 36、有效的数独))

    • 这样每一个格子就可以计算出所有不能填的数字,然后得到所有能填的数字,用 getPossibleStatus() 函数实现;
    • 填入数字和回溯时,只需要更新存储信息;
    • 每个格子在使用时,会根据存储信息重新计算能填的数字。
  2. 回溯

    每次都使用 getNext() 选择能填的数字最少的格子开始填,这样填错的概率最小,回溯次数也会变少。

    • 使用 fillNum() 在填入和回溯时负责更新存储信息;
    • 一旦全部填写成功,一路返回 true ,结束递归。

图解:

在这里插入图片描述

【算法描述】

class Solution {
public:
    // 填充坐标(x,y)对应的数字在该行、列、块中出现的位置
    void fillNum(int x, int y, int n, bool flag) {
        row[x][n] = flag ? 1 : 0;
        col[y][n] = flag ? 1 : 0;
        block[y/3 + (x/3)*3][n] = flag ? 1 : 0;
    }

    // 计算出坐标(x,y)对应的格子所有能填的数字
    bitset<9> getPossibleStatus(int x, int y) {
        return ~(row[x] | col[y] | block[y/3 + (x/3)*3]);
    }

    // 选择出能填的数字最少的格子坐标
    vector<int> getNext(vector<vector<char>>& board) {
        int minCount = 10;
        vector<int> v = {0, 0};
        for(int i = 0; i < 9; ++i) {
            for(int j = 0; j < 9; ++j) {
                if(board[i][j] == '.') {
                    bitset<9> bit = getPossibleStatus(i, j);
                    if(bit.count() < minCount) {
                        minCount = bit.count();
                        v = {i, j};
                    }
                }
            }
        }
        return v;
    }

    // 回溯遍历,直到所有数字都填进去才成功
    bool dfs(vector<vector<char>>& board, int count) {
        if(count == 0) {
            return true;
        }
        // 选择出能填数字最少的格子开始填,这样填错的概率最小,回溯次数也会变少
        vector<int> v = getNext(board);
        // 遍历该格子所有能填的数
        bitset<9> n = getPossibleStatus(v[0], v[1]);
        for(int i = 0; i < n.size(); ++i) {
            // 第i位置为true,表示可以选择填入该下标对应的数
            if(n.test(i)) {
                board[v[0]][v[1]] = '1' + i;
                fillNum(v[0], v[1], i, true);
                if(dfs(board, count-1))  return true;
                fillNum(v[0], v[1], i, false);
                board[v[0]][v[1]] = '.';
            }
        }
        return false;
    }

    void solveSudoku(vector<vector<char>>& board) {
        // 状态压缩 
        row = vector<bitset<9> >(9, bitset<9>());
        col = vector<bitset<9> >(9, bitset<9>());
        block = vector<bitset<9> >(9, bitset<9>());
        int count = 0;
        for(int i = 0; i < 9; ++i) {
            for(int j = 0; j < 9; ++j) {
                if(board[i][j] == '.') {
                    count++;
                    continue;
                }
                int n = board[i][j] - '1';
                fillNum(i, j, n, true);
            }
        }
        // 回溯
        dfs(board, count);
    }

public:
    vector<bitset<9> > row;     // 行
    vector<bitset<9> > col;     // 列
    vector<bitset<9> > block;   // 块
};

组合总和

【题目描述】

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

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

【输入输出实例】

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]

示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1
输出: []

【算法思路】

利用深度优先遍历「回溯算法」来实现:

在这里插入图片描述

  • 以目标值target为根结点,创建一个分支的时做减法;

  • 每一个箭头表示:从父亲结点的数值减去边上的数值,得到孩子结点的数值。边的值就是题目中给出数组的每个元素的值;

  • 减到0或者负数的时候停止,即:结点0和负数结点成为叶子结点;

  • 所有从根结点到结点0的路径(只能从上往下,没有回路)就是题目要找的一个结果。

该问题为组合问题,不讲究排序,所以要去重:

去重剪枝具体的做法是:每一次搜索的时候设置下一轮搜索的起点begin,即:从每一层的第2个结点开始,都不能再搜索产生同一层结点已经使用过的candidate里的元素。

【算法描述】

vector<vector<int>> combinationSum(vector<int>& candidates, int target) 
{
    vector<vector<int>> v;   //存放所有可能的组合
    vector<int> path;   //选取组合中元素的路径
    sort(candidates.begin(), candidates.end());   //剪枝的前提:排序
    DFS(v, candidates, path, target, 0);
    return v;
}

//通过深度优先搜索来找出所有的组合
void DFS(vector<vector<int>>& v, vector<int>& candidates, vector<int>& path, int target, int begin)
{
    int len = candidates.size();
    if(target == 0)   //递归结束条件:叶子结点为0
    {
        v.push_back(path);
        return;
    }
    for(int i = begin; i < len; i++) //递归循环:从起点begin开始搜索,可跳过同一层结点已经使用过的candidate里的元素
    {
        //因为candidates数组提前做了排序处理,所以只要出现candidates[i] > target,则表明candidates[i]往后均为大于target,可直接跳过循环
        if(target < candidates[i]) 
        {
            break;
        }
        path.push_back(candidates[i]);
        DFS(v, candidates, path, target-candidates[i], i); //每一次搜索的时候设置下一轮搜索的起点begin为i
        path.pop_back();   //回溯
    }
}

【知识点】

回溯算法

1、什么时候使用used数组,什么时候使用begin变量?

(1)排列问题:讲究顺序(即[2, 2, 3]与[2, 3, 2]视为不同列表时),需要记录哪些数字已经使用过,此时用used数组

(2)组合问题:不讲究顺序(即[2, 2, 3]与[2, 3, 2]视为相同列表时),需要按照某种顺序搜索,此时使用begin/first变量

  • 对于组合问题,如果集合中的元素不重复,但却可以重复利用,则每次递归搜索时应该设置下一轮搜索的起点 begin 为 i ,即为DFS(v, candidates, path, target - candidates[i], i, len);

  • 对于组合问题,如果集合中元素不重复,并且不能重复利用,则每次递归搜索时应该设置下一轮搜索的起点 begin 为 i+1 ,即为DFS(v, candidates, path, target - candidates[i], i+1, len);

2、集合内有重复元素的问题

集合内如果有重复数字,则表明这些重复数字将重复出现。为了防止重复数字造成的最终结果冗余,要进行剪枝处理。

具体过程:先对集合元素进行****排序****,保证相同的数字都相邻,然后在保证每次填入的数一定是这个数所在重复数集合中「从左往右第一个未被填过的数字」,对于排列和组合问题有不同的判断条件:

(1)排列问题:(用used数组)

if(i > 0 && candidates[i] == candidates[i-1] && !used[i-1])
{
	continue;
}

(2)组合问题:(用begin/first变量)

if(i > first && candidates[i] == candidates[i-1])
{
	continue;
}

特殊:如果不能改变集合元素,即不能对给定元素排序,要做到剪枝去重,则应该使用哈希表去记录单层中元素的使用情况:

unordered_set<int> used;  //使用set对本层元素进行去重
for(int i = index; i < nums.size(); i++)  //for循环单层元素
{
    if(used.find(nums[i]) != used.end())   //若发现元素在used存在,表示已用过该元素,则直接跳过
    {
    	continue;
    }
    ......
    used.insert(nums[i]);   //记录该元素在本层已使用过,本层后面不能再使用
    ......
}

注意:用set、map容器做哈希占空间和时间较大,所以已知集合元素的数值范围,并且数值范围小的话尽量用数组来实现哈希。

例如:题中告知-100 <= nums[i] <= 100,则可用used[201]来代替set、map容器做哈希:

int used[201] = {0};   //使用数组used对本层元素进行去重
for(int i = index; i < nums.size(); i++)  //for循环单层元素
{
    if( used[nums[i] + 100] )    //若发现元素在used存在,表示已用过该元素,则直接跳过
    {
    	continue;
    }
    ......
    used[nums[i] + 100] = 1;   //记录该元素在本层已使用过,本层后面不能再使用
    ......
}

组合总和II

【题目描述】

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

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

【输入输出实例】

示例 1:

输入:candidates = [10,1,2,7,6,1,5], target = 8,
输出:[[1,1,6], [1,2,5], [1,7], [2,6]]

示例 2:

输入:candidates = [2,5,2,1,2], target = 5,
输出:[[1,2,2], [5]]

【算法思路】

同上一题的核心算法相同,仍是利用深度优先搜索来实现。不同的地方在于该题不能重复使用集合中的元素,所以要去重。

剪枝发生在:同一层数值相同的结点第2、3、…个结点,因为数值相同的第1个结点已经搜索出了包含了这个数值的全部结果,同一层的其它结点,候选数的个数更少,搜索出的结果一定不会比第1个结点更多,并且是第1个结点的子集。则利用:

if(i>0 && nums[i] == nums[i-1] && used[i-1] == false)  //去重 
{
	continue;  //不执行下面的语句,直接进入下一次循环
}

【算法描述】

vector<vector<int>> combinationSum2(vector<int>& candidates, int target) 
{
    vector<vector<int>> v;   //存放各种组合
    vector<int> path;   //存放路径
    sort(candidates.begin(), candidates.end());  //剪枝的前提:排序
    DFS(v, candidates, path, target, 0);
    return v;
}

void DFS(vector<vector<int>>& v, vector<int>& candidates, vector<int>& path, int target, int first)
{
    int len = candidates.size();
    if(target == 0)    //递归结束条件:组合数之和达到目标数
    {
        v.push_back(path);
        return;
    }
    for(int i = first; i < len; i++)   //递归循环
    {
        //为了保证集合中每个数字在每个组合中只能使用一次,防止数字重复使用
        if(i > first && candidates[i] == candidates[i-1])    //去重前提:排序
        {
            continue;
        }
        //因为candidates数组提前做了排序处理,所以只要出现candidates[i] > target,则表明candidates[i]往后均为大于target,可直接跳过循环
        if(candidates[i] > target)   //这里剪枝只为提速,前提是要排序
        {
            break;
        }
        path.push_back(candidates[i]);
        DFS(v, candidates, path, target - candidates[i], i+1); //每一次搜索的时候设置下一轮搜索的起点begin为i+1
        path.pop_back();  //回溯
    }
}

全排列

【题目描述】

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

【输入输出实例】

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

【算法思路】

思路一:

得到全排列,就是在一个树形结构中完成遍历,从树的根结点到叶子结点形成的路径就是其中一个全排列。撤销(回溯)与选择在操作上是相反的。

在这里插入图片描述

设计状态变量:

  • 首先这棵树除了根结点和叶子结点以外,每一个结点做的事情其实是一样的,即:在已经选择了一些数的前提下,在剩下的还没有选择的数中,依次选择一个数,这显然是一个递归结构。

  • 递归的终止条件:一个排列中的数字已经选够了,即记录排列路径的容器path.size() == nums.size(),表示到达叶子节点。

  • 布尔数组used,初始化的时候都为false表示这些数还没有被选择,当我们选定一个数加入path容器的时候,就将used数组的相应位置设置为true,这样在考虑下一个位置的时候,就能够以O(1)的时间复杂度判断这个数是否被选择过,这是一种 “以空间换时间” 的思想。

思路二:

如果first == nums.size(),说明我们已经填完了n个位置,找到了一个可行的解,我们将nums放入答案数组中,递归结束。

【算法描述】

//思路一
vector<vector<int>> permute(vector<int>& nums) 
{
    int len = nums.size();
    vector<vector<int>> v;   //存放所有可能的全排列数组
    vector<int> path;   //排序的路径
    vector<bool> used(len);   //记录数组中的每一位数是否被访问过
    createpermute(v, nums, path, used);
    return v;
}

//利用深度优先搜索的方法来找全排列
void createpermute(vector<vector<int>>& v, vector<int>& nums, vector<int>& path, vector<bool>& used)
{
    int len = nums.size();
    if(path.size() == len)  //递归结束条件(到达叶子节点),表示找到了一种排列可能
    {
        v.push_back(path);
        return;
    }
    for(int i = 0; i < len; i++)
    {
        //在非叶子结点处,产生不同的分支,这一操作的语义是:在还未被选择的数中依次选择一个元素作为下一个位置的元素
        if(used[i] == false)
        {
            used[i] = true;   //标记数据已被选择过
            path.push_back(nums[i]);
            createpermute(v, nums, path, used);
            //下面这两行代码为「回溯」,回溯发生在从 深层结点 回到 浅层结点 的过程,代码在形式上和递归之前是对称的
            path.pop_back();   //删除path容器的最后一个元素
            used[i] = false;
        }
    }
}

//思路二
vector<vector<int>> permute(vector<int>& nums) 
{
    int len = nums.size();
    vector<vector<int>> v;   //存放所有可能的全排列数组
    createpermute(v, nums, 0, len);
    return v;
}

void createpermute(vector<vector<int>>& v, vector<int>& nums, int first, int len) 
{
    if(first == len)    //递归结束条件(到达叶子节点),表示找到了一种排列可能
    {
        v.push_back(nums);
        return;
    }
    for(int i = first; i < len; i++)
    {
        swap(nums[i], nums[first]);
        createpermute(v, nums, first+1, len);
        swap(nums[i], nums[first]);    //回溯
    }
}

【知识点】

深度优先搜索算法(DFS)是一种用于遍历或搜索树或图的算法。这个算法会尽可能深的搜索树的分支。当结点 v 的相邻结点都己被探寻过,搜索将回溯到发现结点v的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。

每一个结点表示了求解全排列问题的不同的阶段,这些阶段通过变量的不同的值体现,这些变量的不同的值,称之为「状态」;

使用深度优先遍历有回溯的过程,在回溯以后,状态变量需要设置成为和先前一样,因此在回到上一层结点的过程中,需要撤销上一次的选择,这个操作称之为「状态重置」;

深度优先遍历,借助系统栈空间,保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈;

全排列II

【题目描述】

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

【输入输出实例】

示例 1:

输入:nums = [1,1,2]
输出:[[1,1,2], [1,2,1], [2,1,1]]

示例 2:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

【算法思路】

由于上一题已经实现了不重复数字序列的全排列,在本题中只需在上一题的基础上,对重复序列进行剪枝,在递归时去重。

要解决重复问题,我们只要设定一个规则,保证重复数字只会被填入一次即可。而在本题解中,我们首先对原数组排序,保证相同的数字都相邻,然后在保证每次填入的数一定是这个数所在重复数集合中「从左往右第一个未被填过的数字」,即如下的判断条件:

if(i>0 && nums[i] == nums[i-1] && used[i-1] == false)  //去重 
{
	continue;  //不执行下面的语句,直接进入下一次循环
}

【算法描述】

vector<vector<int>> permuteUnique(vector<int>& nums) {
    int len = nums.size();
    vector<vector<int>> v;    //存放所有不重复的全排列
    vector<int> path;     //排序的路径
    vector<bool> used(len);    //记录数组中的每一位数是否被访问过
    sort(nums.begin(), nums.end());
    DFS(v, nums, path, used, len);
    return v;
}

//利用深度优先搜索的方法来找全排列
void DFS(vector<vector<int>>& v, vector<int>& nums, vector<int>& path, vector<bool>& used, int len)
{
    if(path.size() == len)    //递归结束条件(到达叶子节点),表示找到了一种排列可能
    {
        v.push_back(path);
        return;
    }
    for(int i = 0; i < len; i++)
    {
        if(!used[i])
        {
            if(i>0 && nums[i] == nums[i-1] && used[i-1] == false)  //去重
            {
                continue;
            }
            used[i] = true;   //标记数据已被选择过
            path.push_back(nums[i]);
            DFS(v, nums, path, used, len);
            path.pop_back();    //回溯
            used[i] = false;
        }
    }
}

N皇后

【题目描述】

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

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

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

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

【输入输出实例】

示例 1:

在这里插入图片描述

输入:n = 4
输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”],[“…Q.”,“Q…”,“…Q”,“.Q…”]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

输入:n = 1
输出:[[“Q”]]

【算法思路】

首先来看一下皇后们的约束条件:

  1. 不能同行
  2. 不能同列
  3. 不能同斜线

下面用一个3 * 3 的棋牌,将搜索过程抽象为一颗树,如图:

在这里插入图片描述

从图中,可以看出,二维矩阵中矩阵的高就是这颗树的高度,矩阵的宽就是树形结构中每一个节点的宽度。

那么我们用皇后们的约束条件,来回溯搜索这颗树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了

按照总结的如下回溯模板,来写:

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

【算法描述】

class Solution {
public:
   vector<vector<string>> solveNQueens(int n) {
       vector<vector<string>> result;		// 记录结果
       vector<pair<int, int>> queens;		// 存放可放置皇后的下标
       dfs(result, queens, n, 0);
       return result;
   }

   void dfs(vector<vector<string>>& result, vector<pair<int, int>> &queens, int n, int begin) {
       // 放够n个皇后即表示为一种答案
       if(queens.size() == n) {	
           auto v = generateBoard(queens);
           result.push_back(v);
           return;
       }
       // 遍历每个位置,看该位置是否可放置皇后
       for(int i = begin; i < n; ++i) {
           for(int j = 0; j < n; ++j) {
               // if(row.find(i) != row.end()) {
               //     break;
               // }
               if(col.find(j) != col.end()) {				// 该列上是否有皇后
                   continue;
               }
               int index1 = i - j;
               if(diag1.find(index1) != diag1.end()) {		// 从左上到右下方向斜线是否有皇后
                   continue;
               }
               int index2 = i + j;
               if(diag2.find(index2) != diag2.end()) {		// 从右上到左下方向斜线是否有皇后
                   continue;
               }
               queens.push_back({i, j});		// 入栈并存入下标
               // row.insert(i);
               col.insert(j);
               diag1.insert(index1);
               diag2.insert(index2);
               dfs(result, queens, n, i+1);
               // row.erase(i);
               col.erase(j);
               diag1.erase(index1);
               diag2.erase(index2);
               queens.pop_back();
           }
           return;    // 改进,时间内存好100倍
       }
   }
   
   // 根据所有存放皇后的下标来生成
   vector<string> generateBoard(vector<pair<int, int>> &queens) {
       int n = queens.size();
       vector<string> v(n, string(n, '.'));
       for(const auto& q : queens) {
           v[q.first][q.second] = 'Q';
       }
       return v;
   }

public:
   // unordered_set<int> row;
   unordered_set<int> col;			// 记录有皇后的列下标
   unordered_set<int> diag1;		// 记录有皇后的左上到右下斜线下标
   unordered_set<int> diag2;		// 记录有皇后的右上到左下斜线下标
};


// 优化,上述可以看到,不需要遍历每一格,只需要遍历列即可,因为放置完一个皇后,dfs递归到下一层时,直接会从新的一行的第一列开始遍历,所以用一个下标来记录遍历到哪行即可。同时如果如果row行一个皇后都没放置,也不需要遍历下面的行,而是直接回溯,将上一个皇后放置点清除。如果是上述代码,发现某一行一个皇后都没有,还会继续遍历下面所有的行,浪费时间内存,针对该问题,在上述代码在外层for循环结束加return就能达到相同效果。
class Solution {
public:
   vector<vector<string>> solveNQueens(int n) {
       vector<vector<string>> result;		// 记录结果
       vector<int> queens(n, -1);			// queens[i] = j,表示第i+1行第j+1列存放皇后
       dfs(result, queens, n, 0);
       return result;
   }

   void dfs(vector<vector<string>>& result, vector<int> &queens, int n, int row) {
       // 遍历完所有行
       if(row == n) {
           auto v = generateBoard(queens);
           result.push_back(v);
           return;
       }
       // 遍历row行的每列,看该位置是否可放置皇后
       for(int j = 0; j < n; ++j) {
           if(col.find(j) != col.end()) {				// 该列上是否有皇后
               continue;
           }
           int index1 = row - j;
           if(diag1.find(index1) != diag1.end()) {		// 从左上到右下方向斜线是否有皇后
               continue;
           }
           int index2 = row + j;
           if(diag2.find(index2) != diag2.end()) {		// 从右上到左下方向斜线是否有皇后
               continue;
           }
           queens[row] = j;		// 入栈并存入下标
           col.insert(j);
           diag1.insert(index1);
           diag2.insert(index2);
           dfs(result, queens, n, row+1);
           col.erase(j);
           diag1.erase(index1);
           diag2.erase(index2);
           queens[row] = -1;
       }
   }
   
   // 根据所有存放皇后的下标来生成
   vector<string> generateBoard(vector<int> &queens) {
       int n = queens.size();
       vector<string> v(n, string(n, '.'));
       for(int i = 0; i < n; ++i) {
           v[i][queens[i]] = 'Q';
       }
       return v;
   }

public:
   unordered_set<int> col;			// 记录有皇后的列下标
   unordered_set<int> diag1;		// 记录有皇后的左上到右下斜线下标
   unordered_set<int> diag2;		// 记录有皇后的右上到左下斜线下标
};


// 代码随想录解法,优先考虑这种解法
class Solution {
public:
   vector<vector<string>> solveNQueens(int n) {
       vector<vector<string>> result;					// 存放结果
       vector<string> chessboard(n, string(n, '.'));	// 一种解法
       backtracking(result, chessboard, n, 0);
       return result;
   }

   // n 为输入的棋盘大小,row 是当前递归到棋牌的第几行
   void backtracking(vector<vector<string>>& result, vector<string>& chessboard, int n, int row) {
       // 遍历完所有行
       if (row == n) {
           result.push_back(chessboard);
           return;
       }
       // 遍历row行的每列,看该位置是否可放置皇后
       for (int col = 0; col < n; col++) {
           if (isValid(row, col, chessboard, n)) { 		// 验证合法就可以放
               chessboard[row][col] = 'Q'; 				// 放置皇后
               backtracking(result, chessboard, n, row + 1);
               chessboard[row][col] = '.'; 				// 回溯,撤销皇后
           }
       }
   }
   
   bool isValid(int row, int col, vector<string>& chessboard, int n) {
       int count = 0;
       // 检查列
       for (int i = 0; i < row; i++) {         // 这是一个剪枝
           if (chessboard[i][col] == 'Q') {
               return false;
           }
       }
       // 检查45度角是否有皇后
       for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {
           if (chessboard[i][j] == 'Q') {
               return false;
           }
       }
       // 检查135度角是否有皇后
       for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
           if (chessboard[i][j] == 'Q') {
               return false;
           }
       }
       return true;
   }
};

排列序列

【题目描述】

给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。按大小顺序列出所有排列情况,如当 n = 3 时, 所有排列如下:“123”、“132”、“213”、“231”、“312”、“321”。给定n和k,返回第k个排列。

【输入输出实例】

示例 1:

输入:n = 3, k = 1
输出:“123”

示例 2:

输入:n = 3, k = 3
输出:“213”

示例 3:

输入:n = 4, k = 9
输出:“2314”

【算法思路】

所求排列一定在叶子结点处得到,进入每一个分支,可以根据已经选定的数的个数,进而计算还未选定的数的个数,然后计算阶乘,就知道这一个分支的叶子结点的个数:

  • 如果 k 大于这一个分支产生的叶子结点数,直接跳过这个分支,这个操作为「剪枝」;

  • 如果 k 小于等于这一个分支产生的叶子结点数,则说明所求的全排列一定在这一个分支所产生的叶子结点里,需要递归求解。

例如:n = 4, k = 9

在这里插入图片描述

以1开头的全排列一共有3! = 6个,并且6 < k=9,因此,所求的全排列一定不以1开头,可以在这里剪枝。

以2开头时(注意此时k应该减去上一轮剪枝的叶子结点个数),这里k=9-3!=3 < 6个,故所求的全排列一定在这个分支里,即所求的全排列一定以2开头,则将2存入path,继续递归求解下一位。

【算法描述】

//这次使用的面向对象(oop)编程
class Solution {
public:
    //返回第k个排列
    string getPermutation(int n, int k) 
    {
        this->n = n;
        this->k = k;
        vector<bool> used(n+1);  //用来判断集合元素是否使用过
        DFS(used);     //深度优先遍历+剪枝
        return this->path;
    }
    
    //采用深度优先遍历+剪枝得到第k个排列
    void DFS(vector<bool>& used)
    {
        int len = path.size();
        if(len == n)  //递归结束条件:到达叶子结点
        {
            return;
        }
        int k_index = factorial(n - 1 - len);  //计算还未确定的数字的全排列的个数
        for(int i = 1; i <= n; i++)  //进入每一个分支
        {
            if(used[i])  //如果某元素已使用过,直接找下一个元素
            {
                continue;
            }
            if(k_index < k)  //确定所求的全排列在哪个分支产生的叶子结点里
            {
                k -= k_index;  //如果所求的全排列不在第i个分支上,则减去一个分支的结点个数(k_index),再向下i+1检测
                continue;
            }
            used[i] = true;
            path += to_string(i);  //所求的全排列在第i个分支上,存入path中
            DFS(used);
            return;  //不需要回溯,最后结果只需要一个叶子节点,所以找到所需叶子结点直接结束,没有回头的过程
        }
    }
    
    //求n的阶乘并返回
    int factorial(int n)
    {
        int temp = 1;
        for(int i = 1; i <= n; i++)
        {
            temp *= i;
        }
        return temp;
    }
    
private:
    int n;  //集合数
    int k;  //求第k个排列
    string path;  //存放第k个排列
};


// 笨方法1:dfs按顺序保存所有可能的排列组合,取数组第k个元素即为答案
string getPermutation(int n, int k) {
    std::vector<string> result;
    std::vector<bool> used(n, false);
    string path;
    dfs(result, used, path, n);
    return result[k-1];
}
void dfs(std::vector<string>& result, std::vector<bool>& used, string path, int n) {
    if(path.size() == n) {
        result.push_back(path);
        return;
    }
    for(int i = 0; i < n; ++i) {
        if(!used[i]) {
            used[i] = true;
            path += ('1' + i);
            dfs(result, used, path, n);
            path.pop_back();
            used[i] = false;
        }
    }
}


// 笨方法2:用一个计数index,不用保存所有的排列组合,递归遍历到第k个时,拿出当前path即为答案,不用继续递归下去
string getPermutation(int n, int k) {
    std::vector<bool> used(n, false);
    string path;
    int index = 0;
    dfs(used, path, n, k, index);
    return path;
}

void dfs(std::vector<bool>& used, string& path, int n, int k, int& index) {
    if(path.size() == n) {
        ++index;
        return;
    }
    for(int i = 0; i < n; ++i) {
        if(used[i]) {
            continue;
        }
        used[i] = true;
        path += ('1' + i);
        dfs(used, path, n, k, index);
        if(index != k) {
            path.pop_back();
            used[i] = false;
        }
        else {
            return;
        }
    }
}

组合

【题目描述】

给定两个整数n和k,返回范围[1, n]中所有可能的 k 个数的组合。你可以按任何顺序返回答案。

【输入输出实例】

示例 1:

输入:n = 4, k = 2
输出:[[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]

示例 2:

输入:n = 1, k = 1
输出:[[1]]

【算法思路】

既然是树形问题上的深度优先遍历,因此首先画出树形结构。例如输入:n = 4, k = 2,我们可以发现如下递归结构:

在这里插入图片描述

  • 叶子结点的信息体现在从根结点到叶子结点的路径上,因此需要一个表示路径的变量path,它是一个列表;

  • 每一个结点递归地在做同样的事情,区别在于搜索起点,因此需要一个变量first,表示在区间[first, n]里选出若干个数的组合;

每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠first。例如在集合[1,2,3,4]取1之后,下一层递归,就要在[2,3,4]中取数了,那么下一层递归如何知道从[2,3,4]中取数呢,靠的就是first。

【算法描述】

vector<vector<int>> combine(int n, int k) {
    vector<vector<int>> v;   //存放所有可能的组合
    vector<int> path;    //选择组合的路径
    DFS(v, path, 1, n, k);
    return v;
}
void DFS(vector<vector<int>>& v, vector<int>& path, int first, int n, int k)
{
    if(path.size() == k)   //递归结束条件
    {
        v.push_back(path);
        return;
    }
    for(int i = first; i <= n; i++)   //递归循环
    {
        path.push_back(i);
        DFS(v, path, i+1, n, k);   //每一次搜索的时候设置下一轮搜索的起点begin为i+1
        path.pop_back();   //回溯
    }
}

子集

【题目描述】

给你一个整数数组nums,数组中的元素互不相同。返回该数组所有可能的子集(幂集)。解集不能包含重复的子集。你可以按任意顺序返回解集。

【输入输出实例】

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

【算法思路】

(1)画出递归树,找到状态变量(回溯函数的参数)

在这里插入图片描述

观察上图可得,选择列表(黄色框)里的数,都是选择路径(红色框)后面的数,比如 [1] 这条路径,他后面的选择列表只有 “2、3” , [2]这条路径后面只有 “3” 这个选择。那么就应该使用一个参数start,来标识当前的选择列表的起始位置。也就是标识每一层的状态,因此被形象的称为 “状态变量” 。

(2)确立递归结束条件

此题非常特殊,所有路径都应该加入结果集,所以不存在递归结束条件。因此不需要手写结束条件,直接加入结果集。

(3)找选择列表

子集问题的选择列表,是上一条选择路径之后的数。

for(int i = first; i < len; i++)

(4)判断是否需要剪枝

从递归树中看到,路径没有重复的,也没有不符合条件的,所以不需要剪枝。

(5)作出选择,递归调用,进入下一层

(6)撤销选择(回溯)

【算法描述】

vector<vector<int>> subsets(vector<int>& nums) {
    int len = nums.size();
    vector<vector<int>> v;   //存放所有可能的子集
    vector<int> path;   //选择路径
    DFS(v, nums, path, 0, len);
    return v;
}

void DFS(vector<vector<int>>& v, vector<int>& nums, vector<int>& path, int first, int len)
{
    //因为题目要求找子集,则从根结点到叶子结点路径上所有的结点都是需要的,则不需要递归结束条件,每次递归直接存放元素
    v.push_back(path);
    for(int i = first; i < len; i++)    //递归循环 
    {
        path.push_back(nums[i]);
        DFS(v, nums, path, i+1, len);   //将下次都搜索起点设为i+1
        path.pop_back();
    }
}

【知识点】

回溯算法总结:

怎么样写回溯算法(从上而下,根据题目而变化)

(1)画出递归树,找到状态变量(回溯函数的参数);

(2)根据题意,确立递归结束条件;

(3)找准选择列表(与函数参数相关),与第一步紧密关联;

(4)判断是否需要剪枝;

(5)作出选择,递归调用,进入下一层;

(6)撤销选择;

子集II

【题目描述】

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

【输入输出实例】

示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

【算法思路】

整体思路与78、子集一样利用回溯算法,只是需要剪枝,将重复部分删掉:

在这里插入图片描述

剪枝的前提是需要先对数组排序,使用排序函数sort()。我们需要去除重复的集合,即需要剪枝,把递归树上的某些分支剪掉。观察上图不难发现,应该去除当前选择列表中,与上一个数重复的那个数引出的分支,如 “2、2” 这个选择列表,第二个 “2” 是最后重复的,应该去除这个 “2” 引出的分支。

【算法描述】

vector<vector<int>> subsetsWithDup(vector<int>& nums) {
    int len = nums.size();
    vector<vector<int>> v;   //存放所有可能的子集
    vector<int> path;   //选择路径
    sort(nums.begin(), nums.end());   //剪枝前提:排序
    DFS(v, nums, path, 0, len);
    return v;
}

void DFS(vector<vector<int>>& v, vector<int>& nums, vector<int>& path, int first, int len)
{
    //不需要递归结束条件,从根结点到叶子结点都需要
    v.push_back(path);
    for(int i = first; i < len; i++)
    {
        if(i > first && nums[i] == nums[i-1])   //防止重复元素产生重复组合
        {
            continue;
        }
        path.push_back(nums[i]);
        DFS(v, nums, path, i+1, len);
        path.pop_back();   //回溯
    }
}

单词搜索

【题目描述】

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true;否则,返回 false。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中 “相邻” 单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

【输入输出实例】

示例 1:

在这里插入图片描述

输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true

示例 2:

在这里插入图片描述

输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “SEE”
输出:true

示例 3:

在这里插入图片描述

输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCB”
输出:false

【算法思路】

使用深度优先搜索(DFS)和回溯的思想实现。关于判断元素是否使用过,使用一个二维数组 used 对使用过的元素做标记。使用一个变量 index 来记录目标单词 word 的字符索引。

(1) 外层:遍历

首先遍历 board 的所有元素,先找到和 word 第一个字母相同的元素,然后进入递归流程。假设这个元素的坐标为 (i, j),进入递归前,先记得把该元素打上使用过的标记:used[i][j] = true;,表示该位置访问过。

(2) 内层:递归

  • 从 (i, j) 出发,朝它的上下左右试探(注意坐标不要超过边界),看看它周边的这四个元素是否能匹配 word 的下一个字母;

  • 如果匹配到了:先用 used 对该元素做标记,表示访问过该元素。再带着该元素继续进入下一个递归;

  • 如果都匹配不到:返回 false;

  • 当 word 的所有字母都完成匹配后,整个流程返回 true 。

【算法描述】

//回溯法
bool exist(vector<vector<char>>& board, string word) {
    int m = board.size();    //行数
    int n = board[0].size();    //列数
    bool flag = false;    //记录是否成功搜索到单词word
    vector<vector<bool>> used(m, vector<bool>(n));  //记录当前格子是否已被访问过
    for(int i = 0; i < m; i++)    //遍历所有网格点,找和 word 第一个字母相同的元素
    {
        for(int j = 0; j < n; j++)
        {
            if(board[i][j] == word[0])  //网格中找到 word[0]
            {
                used[i][j] = true;   //修改该位置状态,表示访问过
                DFS(board, used, word, i, j, 1, flag);    //递归遍历
                used[i][j] = false;    //回溯:撤销修改
                if(flag)    //成功搜索到单词word
                {
                    return true;
                }
            }
        }
    }
    return false;
}
//DFS:找位置(i,j)四周的字符是否与index指向word的字符一致,只有一致时才继续递归搜索
void DFS(vector<vector<char>>& board, vector<vector<bool>>& used, string word, int i, int j, int index, bool& flag)    //i和j表示当前元素的坐标,used数组记录当前格子是否已被访问过,index记录目标单词word的字符索引
{
    if(index == word.size())    //成功搜索到单词word
    {
        flag = true;
        return;
    }
    if(j < board[0].size()-1 && board[i][j+1] == word[index] && !used[i][j+1])  //访问右边一个位置
    {
        used[i][j+1] = true;
        DFS(board, used, word, i, j+1, index+1, flag);
        used[i][j+1] = false;
        if(flag)    //如果已经搜索到单词word,不在继续递归,直接返回
        {
            return;
        }
    }
    if(i < board.size()-1 && board[i+1][j] == word[index] && !used[i+1][j])  //访问下边一个位置
    {
        used[i+1][j] = true;
        DFS(board, used, word, i+1, j, index+1, flag);
        used[i+1][j] = false;
        if(flag)    //如果已经搜索到单词word,不在继续递归,直接返回
        {
            return;
        }
    }
    if(j > 0 && board[i][j-1] == word[index] && !used[i][j-1])  //访问左边一个位置
    {
        used[i][j-1] = true;
        DFS(board, used, word, i, j-1, index+1, flag);
        used[i][j-1] = false;
        if(flag)    //如果已经搜索到单词word,不在继续递归,直接返回
        {
            return;
        }
    }
    if(i > 0 && board[i-1][j] == word[index] && !used[i-1][j])  //访问上边一个位置
    {
        used[i-1][j] = true;
        DFS(board, used, word, i-1, j, index+1, flag);
        used[i-1][j] = false;
        if(flag)    //如果已经搜索到单词word,不在继续递归,直接返回
        {
            return;
        }
    }
}


//回溯法优化
bool exist(vector<vector<char>>& board, string word) {
    int m = board.size();    //行数
    int n = board[0].size();    //列数
    vector<vector<bool>> used(m, vector<bool>(n));  //记录当前格子是否已被访问过
    for(int i = 0; i < m; i++)    //遍历所有网格点,找和 word 第一个字母相同的元素
    {
        for(int j = 0; j < n; j++)
        {
            if(board[i][j] == word[0])
            {
                if(DFS(board, used, word, i, j, 0))    //开始递归
                {
                    return true;
                }
            }
        }
    }
    return false;
}
//DFS:不断判断当前位置(i, j)字符是否与index指向word的字符一致,只有一致时才继续递归搜索该位置四周的字符
//i和j表示当前元素的坐标,used数组记录当前格子是否已被访问过,index记录目标单词word的字符索引
bool DFS(vector<vector<char>>& board, vector<vector<bool>>& used, string word, int i, int j, int index)
{
    //超出边界、已经访问过、棋盘格中当前字符已经和目标字符不一致时,说明未找到目标单词,返回false
    if(i < 0 || i >= board.size() || j < 0 || j >= board[0].size() || used[i][j] || board[i][j] != word[index])
    {
        return false;
    }
    if(index == word.size() - 1)    //找到目标单词
    {
        return true;
    }
    used[i][j] = true;    //修改该位置状态,表示访问过
    bool ret = DFS(board, used, word, i+1, j, index+1) ||     //访问下边一个位置
        DFS(board, used, word, i, j+1, index+1) ||    //访问右边一个位置
        DFS(board, used, word, i-1, j, index+1) ||    //访问上边一个位置
        DFS(board, used, word, i, j-1, index+1);      //访问左边一个位置
    used[i][j] = false;    //回溯:撤销修改
    return ret;
}

复原IP地址

【题目描述】

有效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中的任何数字。你可以按任何顺序返回答案。

【输入输出实例】

示例 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”]

【算法思路】

在这里插入图片描述

  • 递归参数

在切割问题中,其类似组合问题,所以index一定是需要的,因为不能重复分割,要记录下一层递归分割的起始位置。本题我们还需要一个变量num,记录添加 ‘.’ 的数量。

  • 递归终止条件

本题明确要求只能分成4段,所以可以用切割线切到最后作为终止条件。本题用 ‘.’ 来切割,所以当 ‘.’ 数量为4时,即num == 4时,表明已经切割到了最后,只需在结束条件内将最后一个 ‘.’ 去掉即可放入result容器。

  • 单层搜索的逻辑

在for循环中[index, i]这个区间就是截取的子串,需要判断这个子串是否合法(即判断子串是否位于0到255之间组成,并且不能含有前导0)。如果合法就在字符串后面加上符号 ‘.’ 表示已经分割。

注意num用来记录 ‘.’ 的数量,因为本题只能分割四段且必须全部分割完,所以当num == 3时,必须将 ‘.’ 放到字符串尾部,再根据最后一段是否合法。

然后就是递归和回溯的过程:递归调用时,下一层递归的index要从i+1开始,同时记录分割符的数量num要+1。回溯的时候,就将刚加入的字符和 ‘.’ 删掉就可以了。

【算法描述】

vector<string> restoreIpAddresses(string s) {
    vector<string> result;   //存放所有有效的IP地址
    string path;   //选择路径
    DFS(result, path, s, 0, 0);
    return result;
}
//回溯算法
void DFS(vector<string>& result, string& path, string s, int index, int num)
{
    //递归结束条件:存入4个'.'后,表示为正好四个整数
    if(num == 4)
    {
        path.pop_back();    //删除最后一个'.'
        result.push_back(path);
        return;
    }
    //for循环为单层遍历
    for(int i = index; i < s.size(); i++)
    {
        if(num == 3)   //分割为四个整数
        {
            i = s.size() - 1;  //若已存入3个'.',则直接把最后一个'.'放入字符串尾
        }
        if(i - index >= 3)   //剪枝
        {
            return;
        }
        if(isOk(s, index, i))  //如果分割的整数符合条件则存入
        {
            int len = path.size();   //提前记录插入之前的最后下标位置
            path += s.substr(index, i-index+1) + ".";   //存入分割字符+'.'
            DFS(result, path, s, i+1, num+1);
            path.erase(len, i-index+2);   //回溯
        }
    }
}
//判断字符串s的某个子串(begin:end)是否符合题目条件:子串对应的数字要位于0~255之间,且不能含有前导0
bool isOk(string s, int begin, int end)
{
    string str = s.substr(begin, end-begin+1);  //子串
    int len = str.size();
    switch(len)
    {
        case 1:    //只有个位数
            return true;
        case 2:    //两位数且不包含前导0
            if(str[0] != '0')
            {
                return true;
            }
            return false;
        case 3:    //三位数且在100~255之间
            char a = str[0];   //要转成char再进行加减,不能用str[0]-'0'
            char b = str[1];
            char c = str[2];
            int num = (a-'0')*100 + (b-'0')*10 + (c-'0');
            if(num >= 100 && num <=255)
            {
                return true;
            }
            return false; 
    }
    return false;         
}

不同的二叉搜索树II

【题目描述】

给你一个整数n,请你生成并返回所有由n个节点组成且节点值从 1n 互不相同的不同二叉搜索树 。可以按任意顺序返回答案。

【输入输出实例】

示例 1:

在这里插入图片描述

输入:n = 3
输出:[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]

示例 2:

输入:n = 1
输出:[[1]]

【算法思路】

题目要求生成节点值从 1n 互不相同的所有二叉搜索树,则可用i来遍历[1, n]区间,生成以i为根节点的二叉搜索树。

对于连续整数序列[left, right]中的一点i,若要生成以i为根节点的二叉搜索树,则有如下规律:

  • 通过for循环遍历[left, right]中每个结点都能按下述生成子树序列;

  • 一旦left大于right,则说明这里无法产生子树,所以此处应该是作为空结点返回;

  • i左边序列可以作为左子树结点,且左儿子可能有多个,则有vector<TreeNode *> leftNodes = generate(left, i-1);

  • i右边序列可以作为右子树结点,同上所以有vector<TreeNode *> rightNodes = generate(i+1, right);

  • 产生以i为根结点的二叉搜索树的子树有leftNodes.size() * rightNodes.size()个,遍历每种情况,即可生成以i为根节点的二叉搜索树;

  • 返回[left, right]中生成的所有子树序列result

【算法描述】

vector<TreeNode*> generateTrees(int n) 
{
    return generate(1, n);    //返回所有节点值从 1 到 n 的二叉搜索树 
}

//对于连续整数序列[left, right]中的一点 i,生成所有以 i 为根节点的二叉搜索树
vector<TreeNode*> generate(int left, int right) {
    vector<TreeNode*> result;    //存放二叉搜索树
    if(left > right)    //递归结束条件:无法产生子树,作为空结点返回
    {
        result.push_back(NULL);
        return result;
    }
    for(int i = left; i <= right; i++)    //用 i 遍历序列[left, right]
    {
        vector<TreeNode*> leftNodes = generate(left, i - 1);    // i 左边的序列可以作为左子树结点
        vector<TreeNode*> rightNodes = generate(i + 1, right);  // i 右边的序列可以作为右子树结点
        
        //TreeNode* node = new TreeNode(i);   //放在循环外的话:如下面多次循环时,因为根节点只有一个,所以每循环一次生成一颗新树,都会将上次循环生成的树覆盖掉,最后导致生成的树都是一样的(即都覆盖为最后一次循环生成的树)

        //产生以i为根结点的二叉搜索树的子树有leftNodes.size()*rightNodes.size()个,遍历每种情况,生成所有的二叉搜索树
        for(auto leftNode : leftNodes)   
        {
            for(auto rightNode : rightNodes)
            {
                TreeNode* node = new TreeNode(i);   //生成二叉搜索树:必须放在循环内
                node->left = leftNode;
                node->right = rightNode;
                result.push_back(node);   //放入result中
            }
        }
    }
    return result;
}

恭喜你全部读完啦!古人云:温故而知新。赶紧收藏关注起来,用之前再翻一翻吧~


📣推荐阅读

C/C++后端开发面试总结:点击进入 后端开发面经 关注走一波

C++重点知识:点击进入 C++重点知识 关注走一波

力扣(leetcode)题目分类:点击进入 leetcode题目分类 关注走一波

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值