LeetCode题解随笔: 回溯算法

目录

零、前言

一、组合

77. 组合

216. 组合总和 III

17. 电话号码的字母组合

39. 组合总和

40. 组合总和 II[*]

二、分割

131. 分割回文串

93. 复原 IP 地址[*]

三、子集

78. 子集

 90. 子集 II

491. 递增子序列

698. 划分为k个相等的子集[*]

四、排列

46. 全排列

47. 全排列 II

332. 重新安排行程[*]

22. 括号生成

五、棋盘问题

51. N 皇后[*]

37. 解数独


零、前言

回溯法解决的问题都可以抽象为树形结构(N叉树),回溯法解决的问题都是在集合中递归查找子集,集合的大小构成了树的宽度,递归的深度构成树的深度。【参考:代码随想录】

回溯法模板:

  • 回溯函数模板返回值以及参数
void backtracking(参数)
  • 回溯函数终止条件
if (终止条件) {
    存放结果;
    return;
}
  • 回溯搜索的遍历过程

回溯算法理论基础

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

回溯算法能解决如下问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 棋盘问题:N皇后,解数独等等

无论是排列、组合还是子集问题,简单说就是从序列 nums 中以给定规则取若干元素,主要有以下几种变体:

  • 形式一:元素无重不可复选,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次,这也是最基本的形式

解决:利用used数组进行去重。

  • 形式二:元素可重不可复选,即 nums 中的元素可以存在重复,每个元素最多只能被使用一次

解决:核心在于排序和剪枝。层内剪枝:nums[i] == nums[i-1]时跳过;排列问题遇到重复元素还要保证相对顺序,参见《全排列Ⅱ》,增加了 && !used[i - 1] 这一判定条件。 

  • 形式三:元素无重可复选,即 nums 中的元素都是唯一的,每个元素可以被使用若干次

解决:将BackTracking(nums, i+1)改为BackTracking(nums, i),设置合适的 base case 以结束算法。 

 利用回溯方法搜索所有结果的框架:

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

如果不需要得到所有结果,只寻找一个满足要求的解,可以把回溯函数返回值设置为bool。(见“解数独”问题)。 


一、组合

77. 组合

vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> combine(int n, int k) {
        BackTracking(n, k, 1);
        return res;
    }

    void BackTracking(int n, int k, int start) {
        // 递归终止条件
        if (path.size() == k) {
            res.push_back(path);
            return;
        }
        // 递归逻辑
        for (int i = start; i < n; i++) {
            path.push_back(i);
            BackTracking(n, k, i + 1);
            // 回溯
            path.pop_back();
        }
    }

本题即使采用暴力搜索,也很难完成,因此需要用到回溯法。递归的过程如下图所示(来源:代码随想录):

77.组合1

 递归可以进行枝剪,本题递归中的for循环代码可以进行如下修改:

for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)

216. 组合总和 III

vector<vector<int>> res;
    vector<int> path;
    int sum = 0;
    vector<vector<int>> combinationSum3(int k, int n) {
        BackTracking(k, n, 1);
        return res;
    }

    void BackTracking(int k, int n, int start) {
        if (path.size() == k) {
            if (sum == n) {
                res.push_back(path);
            }
            return;
        }
        for (int i = start; i <= 9; i++) {
            if (i >= n)    return;
            path.push_back(i);
            sum += i;
            BackTracking(k, n, i + 1);
            sum -= i;
            path.pop_back();
        }
    }

 本题与上一题类似,不再赘述。

17. 电话号码的字母组合

const string letterMap[10] = {
        "", // 0
        "", // 1
        "abc", // 2
        "def", // 3
        "ghi", // 4
        "jkl", // 5
        "mno", // 6
        "pqrs", // 7
        "tuv", // 8
        "wxyz", // 9
    };
    
    vector<string> res;
    string path;
    vector<string> letterCombinations(string digits) {
        if (digits.empty())    return res;
        BackTracking(digits, 0);
        return res;
    }
    void BackTracking(string& digits, int digitIndex) {
        if (path.size() == digits.size()) {
            res.push_back(path);
            return;
        }      
        int digit = digits[digitIndex] - '0';
        for (int i = 0; i < letterMap[digit].size(); i++) {
            path.push_back(letterMap[digit][i]);
            BackTracking(digits, digitIndex + 1);
            path.pop_back();
        }
    }

首先,本题采用string数组存储数字和字符串间的映射关系。构造递归时,要想清楚以下过程(来源:代码随想录):

17. 电话号码的字母组合

 这里面一定要理清楚两条线:for循环的横向遍历递归的纵向遍历

  • for循环的横向遍历:遍历的是单个按键上的字符串,因此要写为:
for (int i = 0; i < letterMap[digit].size(); i++)
  • 递归的纵向遍历:遍历的是按键数字,改变对应的for循环循环集合,因此要写作:
BackTracking(digits, digitIndex + 1);

理清楚这两条线,回溯算法写起来就很简单,否则会一头雾水。理清楚这两条线的关键在于搞清楚组合方式,不要重复或遗漏,在此基础上建立起上图中的树模型。

39. 组合总和

vector<vector<int>> res;
    vector<int> path; 
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        int sum = 0;
        sort(candidates.begin(), candidates.end());
        BackTracking(candidates, sum, target, 0);
        return res;
    }
    void BackTracking(vector<int>& candidates, int& sum, int target, int start) {
        if (sum >= target) {
            if (sum == target)  res.push_back(path);
            return;
        }
        for (int i = start; i < candidates.size(); i++) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            BackTracking(candidates, sum, target, i);
            path.pop_back();
            sum -= candidates[i];
        }
    }

首先,应明确下图中的树所展现出的搜索逻辑(来源:代码随想录):

39.组合总和1

取2时能在[2,3,5]中取值,取3时能在[3,5]中取值。这蕴含两个信息:

  • 仍需要startIndex,某一个元素递归完后,意味着含有该元素的结果全都被查找完毕了。后续递归时不需要再囊括此元素;
  • 由于递归时该元素仍能重复被使用,所以递归函数为BackTracking(candidates, sum, target, i),其中参数非i+1。

本题的另一技巧是提前对candidates数组排序,从而结合递归函数中的判断,提前终止递归。

  • 如果是一个集合来求组合的话,就需要startIndex;
  • 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex。

40. 组合总和 II[*]

vector<vector<int>> res;
    vector<int> path; 
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        int sum = 0;
        sort(candidates.begin(), candidates.end());
        BackTracking(candidates, sum, target, 0);
        return res;
    }
    void BackTracking(vector<int>& candidates, int& sum, int target, int start) {
        if (sum >= target) {
            if (sum == target)  res.push_back(path);
            return;
        }
        for (int i = start; i < candidates.size(); i++) {
            if (i > start && candidates[i] == candidates[i - 1])   continue;
            sum += candidates[i];
            path.push_back(candidates[i]);
            BackTracking(candidates, sum, target, i + 1);
            path.pop_back();
            sum -= candidates[i];
        }
    }

本题和上一题不同之处在于需要处理重复的结果,仍要明确一个基本点:树结构单层循环时,第一个元素递归完毕,证明含有第一个元素的结果已经全部找齐了!所以下一个元素和前一个元素相同时,下一个元素应当被舍去(candidates数组提前经过排序)。即应在单层循环内进行去重

可以利用candidates数组对单层循环进行去重,尤其注意判断条件:

if (i > start && candidates[i] == candidates[i - 1])   continue;

为什么这行代码只对单层元素去重,而没有对深度递归进行去重呢?因为每次深度递归时,start的值都会改变,i>start的条件不会触发;而单层循环时start为定值。 


二、分割

131. 分割回文串

vector<vector<string>> res;
    vector<string> path;   
    vector<vector<string>> partition(string s) {
        BackTracking(s, 0);
        return res;
    }

    void BackTracking(const string& s, int start) {
        if (start >= s.size()) {
            res.push_back(path);
            return;
        }
        for (int i = start; i < s.size(); i++) {
            // 是回文串,path记录子串
            if (isPalindrome(s, start, i)) {
                path.push_back(s.substr(start, i - start + 1));
            }
            // 若不是回文串,无需深度递归,直接返回即可
            else   continue;
            BackTracking(s, i + 1);
            path.pop_back();
        }
    }

    bool isPalindrome(const string& s, int start, int end) {
        for (int i = start, j = end; i < j; i++, j--) {
            if (s[i] != s[j])    return false;
        }
        return true;
    }

本题抽象树结构如下图所示(来源:代码随想录):

93. 复原 IP 地址[*]

vector<string> res;   
    vector<string> restoreIpAddresses(string s) {
        BackTracking(s, 0, 0);
        return res;
    }
    void BackTracking(string& s, int start, int pointNums) {
        // pointNums记录已分隔段数,即逗点个数
        if (pointNums == 3) {
            // 判断最后一段是否合法
            if (isValid(s, start, s.size() - 1)) {
                res.push_back(s);
            }
            return;
        }
        for (int i = start; i < s.size(); i++) {
            if (isValid(s, start, i)) {
                s.insert(s.begin() + i + 1, '.');
                pointNums++;
                // 插入逗点之后下一个子串的起始位置为i+2
                BackTracking(s, i + 2, pointNums);
                pointNums--;
                s.erase(s.begin() + i + 1);
            }
            // 不符合后序递归都不必进行
            else   break;
        }
    }
    // 判断子字符串是否有效
    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;
    }

该解法参考了上一题分割的思路,同时直接在源字符串s上进行操作。抽象树结构如下图所示(来源:代码随想录):

 93.复原IP地址

 分清层序遍历深度递归的目的:

  • 当某一段满足要求,即可向下一层进行深度递归(本题最多递归三层)
  • 利用最后的递归返回条件,判断第四段是否符合要求。符合时收录结果,不符合时回溯;
  • 该段不符合要求时,该层后续循环都不必进行(不符合的条件包括大于255、存在非法字符、某段0以开头,这些问题即使进行层序遍历也无法消除,因此没有继续迭代的意义了)。

三、子集

78. 子集

vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> subsets(vector<int>& nums) {
        BackTracking(nums, 0);
        return res;
    }
    void BackTracking(const vector<int>& nums, int start) {
        res.push_back(path);
        if (start >= nums.size())  return;
        for (int i = start; i < nums.size(); i++) {
            path.push_back(nums[i]);
            BackTracking(nums, i + 1);
            path.pop_back();
        }
    }

子集问题需要搜集抽象树结构的所有结点,而非所有叶子结点。因此要注意代码中更行res的位置。本题抽象树结构如下图所示(来源:代码随想录):

78.子集

 90. 子集 II

vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        BackTracking(nums, 0);
        return res;
    }
    void BackTracking(const vector<int>& nums, int start) {
        res.push_back(path);
        if (start >= nums.size())  return;
        for (int i = start; i < nums.size(); i++) {
            if (i > start && nums[i] == nums[i - 1])    continue;
            path.push_back(nums[i]);
            BackTracking(nums, i + 1);
            path.pop_back();
        }
    }

本题与40.组合总和Ⅱ相似,需要进行去重。这里需要秉持着:一个元素递归完毕后,含有该元素的集合都找全了,因此层间迭代时再遇到相同元素就要跳过。因此去重位置是层间去重,需要放在for循环的内部。

491. 递增子序列

vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        BackTracking(nums, 0);
        return res;
    }
    void BackTracking(const vector<int>& nums, int start) {
        // 收入的是所有结点,而非叶子结点
        if (path.size() >= 2) {
            res.push_back(path);
        }
        // 记录层内重复元素
        unordered_map<int, int> record;
        // 递归返回条件
        if (start >= nums.size())   return;
        for (int i = start; i < nums.size(); i++) {
            if ((!path.empty() && nums[i] < path.back()) || record[nums[i]] == 1)  continue;
            record[nums[i]] = 1;
            path.push_back(nums[i]);
            BackTracking(nums, i + 1);
            path.pop_back();
        }
    }

本题的抽象树结构如下图所示(来源:代码随想录):

491. 递增子序列1

 本题难点在于层间去重,相比于40. 组合总和 II,本题的去重不能只判断两个相邻元素,因为序列不是有序的,某个元素后面可能会再次出现。因此采用一个额外的无序map记录元素是否已经使用过。每层都具有一个record数组,层间迭代时会被更新和检查;递归时下一层会新创建一个record数组。

尤其注意递归或迭代的条件:

if ((!path.empty() && nums[i] < path.back()) || record[nums[i]] == 1)  continue;

前半部分是判断递归时是否满足递增子序列的条件,后半部分是判断层间是否重复

698. 划分为k个相等的子集[*]

int target = 0;
    vector<int> bucket;
    bool canPartitionKSubsets(vector<int>& nums, int k) {
        // 长度小于k
        if (nums.size() < k)    return false;
        int sum = accumulate(nums.begin(), nums.end(), 0);
        // 和不是k的整数倍
        if (sum % k != 0)    return false;
        else    target = sum / k;
        // 大的在前,减少回溯次数
        sort(nums.begin(), nums.end(), greater<int>());
        // 收集路径,记录了单个集合
        bucket.resize(k);
        return BackTracking(nums, 0);
    }
    bool BackTracking(vector<int>& nums, int start) {
        // 树结构每一条路径到底返回true;最终每一条路径记录了一个子数组
        if (start == nums.size())  return true;
        for (int i = 0; i < bucket.size(); i++) {
            // 大于目标值,跳过进行下一次迭代
            if (nums[start] + bucket[i] > target)  continue;
            // 去除【同层】重复元素(数组已排序)
            if (i > 0 && bucket[i] == bucket[i - 1])   continue;
            bucket[i] += nums[start];
            if (BackTracking(nums, start + 1))  return true;
            bucket[i] -= nums[start];
        }
        return false;
    }

本题注意收集路径的方式、递归停止条件【类似全排列,执行到底才能返回true】和枝剪去重方法。

四、排列

46. 全排列

vector<vector<int>> res;
    vector<int> path;
    unordered_map<int, int> record;
    vector<vector<int>> permute(vector<int>& nums) {
        BackTacking(nums);
        return res;
    }
    void BackTacking(const vector<int>& nums) {
        if (path.size() == nums.size()) {
            res.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (record[nums[i]] == 1)    continue;
            path.push_back(nums[i]);
            record[nums[i]] = 1;
            BackTacking(nums);
            record[nums[i]] = 0;
            path.pop_back();
        }
    }

本题的抽象树结构如下图所示(来源:代码随想录):

46.全排列

 排列问题需要一个全局的record数组,标记已经使用过的元素。

47. 全排列 II

vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        // 用于递归枚举
        vector<bool> used(nums.size(), false);
        sort(nums.begin(), nums.end());
        BackTacking(nums, used);
        return res;
    }
    void BackTacking(const vector<int>& nums, vector<bool>& used) {
        if (path.size() == nums.size()) {
            res.push_back(path);
            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) {
                path.push_back(nums[i]);
                used[i] = true;
                BackTacking(nums, used);
                used[i] = false;
                path.pop_back();
            } 
        }
    }

 本题的抽象树结构如下图所示(来源:代码随想录):

47.全排列II2

本题的关键在于迭代层内去重递归层外去重。 

  • 递归层外去重是为了不重不漏进行枚举,如上题46.全排列所示,利用全局used数组就可以完成;
  • 迭代层内去重可以仿照491.递增子序列的方式完成,即每一树层都创建一个record数组进行记录。但这样效率较低(用时12ms)。不同于491.递增子序列,本题中的数组可以预先排序,因此可以直接对比nums[i]和nums[i-1]实现去重;
  • 为了不使层内去重和层外去重冲突,判断逻辑如下:
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false)    continue;
            if (used[i] == false) {
                //递归及回溯逻辑
            }
  • 层内迭代时,num[i-1]的逻辑已经执行完毕,因此used[i-1]经回溯已经被置为false,满足相邻两元素相等且上一个元素的used标记为否就可跳过该层内循环;
  • 层外递归时,上一个重复元素used标记会被置为true且还未被回溯,因此可以不重复地进行枚举。

这样的层内去重逻辑效率更高。

332. 重新安排行程[*]

// unordered_map<出发机场, map<到达机场, 航班次数>>
    unordered_map<string, map<string, int>> records;
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        vector<string> res;
        // 对每个机场,初始化其可以到达的目的机场及机票数量
        for (auto ticket : tickets) {
            // target的第二个元素为map,无重复值,且已按照字母顺序排序
            records[ticket[0]][ticket[1]]++;
        }
        res.push_back("JFK");
        BackTracking(tickets.size(), res);
        return res;
    }
    
    // 只需要找到一个行程,返回值类型为bool
    bool BackTracking(const int ticketNum, vector<string>& res) {
        // 总机场数量为票数+1
        if (res.size() == ticketNum + 1) {
            return true;
        }
        // 以记录的最后一个元素(即上一个到达机场)作为出发机场,进行遍历
        for (pair<const string, int>& target : records[res[res.size() - 1]]) {
            if (target.second > 0) {
                res.push_back(target.first);
                target.second--;
                if (BackTracking(ticketNum, res))    return true;
                res.pop_back();
                target.second++;
            }
        }
        return false;
    }

本题难度较高,回溯法的抽象树结构如下图所示(来源:代码随想录):

332.重新安排行程332.重新安排行程1 

本题的关键在于选用恰当的容器存放机票信息,要求做到目的地机场按照字母顺序排序。这里选用

// unordered_map<出发机场, map<到达机场, 航班次数>>
unordered_map<string, map<string, int>> tickets;

 来记录机票的映射关系。

另一个需要注意的点是,由于目的机场已经被排序,所以一旦res数组找齐了,res就是正确的结果。因此只需要找到一个目标结果就可以立即返回,从而定义回溯函数的返回值类型为bool

22. 括号生成

class Solution {
public:
    vector<string> res;
    string path;
    vector<string> generateParenthesis(int n) {
        BackTracking(n, n);
        return res;
    }
    // right和left分别是剩余的")"和"("数量
    // 规则1:剩下的右括号数应≥左括号数,即right>left
    // 规则2:剩余左右括号数量应≥0
    void BackTracking(int left, int right) {
        // 违反规则1,不合法
        if (left > right)    return;
        // 违反规则2,不合法
        if (left < 0 || right < 0)  return;
        // 递归返回条件:满足要求
        if (left == 0 && right == 0) {
            res.push_back(path);
            return;
        }
        // 尝试添加一个左括号
        path.push_back('(');
        BackTracking(left - 1, right);
        path.pop_back();
        // 尝试添加一个右括号
        path.push_back(')');
        BackTracking(left, right - 1);
        path.pop_back();
    }
};

本题需要注意括号生成的规则:「合法」括号组合的左括号数量一定等于右括号数量;子串 p[0..i] 中左括号的数量都大于或等于右括号的数量。 


五、棋盘问题

51. N 皇后[*]

vector<vector<string>> res;
    vector<vector<string>> solveNQueens(int n) {
        // 初始化空棋盘
        vector<string> chessboard(n, string(n, '.'));
        BackTracking(n, 0, chessboard);
        return res;
    }
    // chessboard可以视为二维数组,即一个棋盘
    void BackTracking(const int n, int row, vector<string>& chessboard) {
        // 最后一次操作时row = n - 1
        if (row == n) {
            res.push_back(chessboard);
            return;
        }
        for (int col = 0; col < n; col++) {
            if (IsValid(row, col, chessboard, n)) {
                chessboard[row][col] = 'Q'; // 放置皇后
                BackTracking(n, row + 1, chessboard);
                chessboard[row][col] = '.'; // 回溯,撤销皇后
            }
        }
    }
    // 验证当前棋盘是否合法
    bool IsValid(int row, int col, vector<string>& chessboard, const int n) {
        // 检查列
        for (int i = 0; i < row; i++) {
            if (chessboard[i][col] == 'Q') {
                return false;
            }
        }
        // 检查135度角是否有皇后
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }
        // 检查45度角是否有皇后
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }

本题和93. 复原 IP 地址非常类似,定义了一个IsVaild函数用于检验当前棋盘是否合法。依据这个判据,再进行回溯操作就很简单了。本题对应的抽象树结构如下图所示(来源:代码随想录):

51.N皇后

 层间循环搜索行,深度递归处理列。记得要初始化棋盘,从而回溯时,原位置重新设为“.”即可。

37. 解数独

void solveSudoku(vector<vector<char>>& board) {
        BackTracking(board);
    }
    bool BackTracking(vector<vector<char>>& board) {
        // 遍历下一行
        for (int row = 0; row < board.size(); row++) {
            // 在单行中遍历列
            for (int col = 0; col < board[0].size(); col++) {
                if (board[row][col] != '.')    continue;
                for (char c = '1'; c <= '9'; c++) {
                    if (IsValid(row, col, c, board)) {
                        board[row][col] = c;
                        if (BackTracking(board)) return true;
                        board[row][col] = '.';
                    }
                }
                // 9个数都不成立时
                return false;
            }
        }
        return true;
    }
    bool IsValid(int row, int col, char val, vector<vector<char>>& board) {
        // 判断行中是否重复
        for (int i = 0; i < board.size(); i++) {
            if (board[row][i] == val) {
                return false;
            }
        }
        // 判断列中是否重复
        for (int j = 0; j < board[0].size(); j++) {
            if (board[j][col] == val) {
                return false;
            }
        }
        // 判断所在方格中是否重复
        int startRow = (row / 3) * 3;
        int 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;
    }

本题对应的抽象树结构如下图所示(来源:代码随想录):

37.解数独

N皇后问题每一行每一列只放一个皇后,只需要一层for循环遍历; 本题中棋盘的每一个位置都要放一个数字,并检查数字是否合法,因此需要进行二维递归,即两个for循环分别遍历行和列。因为单行中前一个数字确定后,递归不是进入下一行,而是进入该行的下一个数字。

一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性

另外,本题不需要递归终止条件,因为解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。

回溯函数返回值类型为bool,这与上题类似。因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值