CodeAshen's blog CodeAshen's blog
首页
  • Spring Framework

    • 《剖析Spring5核心原理》
    • 《Spring源码轻松学》
  • Spring Boot

    • Spring Boot 2.0深度实践
  • Spring Cloud

    • Spring Cloud
    • Spring Cloud Alibaba
  • RabbitMQ
  • RocketMQ
  • Kafka
  • MySQL8.0详解
  • Redis从入门到高可用
  • Elastic Stack
  • 操作系统
  • 计算机网络
  • 数据结构与算法
  • 云原生
  • Devops
  • 前端
  • 实用工具
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
  • Reference
GitHub (opens new window)

CodeAshen

后端界的小学生
首页
  • Spring Framework

    • 《剖析Spring5核心原理》
    • 《Spring源码轻松学》
  • Spring Boot

    • Spring Boot 2.0深度实践
  • Spring Cloud

    • Spring Cloud
    • Spring Cloud Alibaba
  • RabbitMQ
  • RocketMQ
  • Kafka
  • MySQL8.0详解
  • Redis从入门到高可用
  • Elastic Stack
  • 操作系统
  • 计算机网络
  • 数据结构与算法
  • 云原生
  • Devops
  • 前端
  • 实用工具
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
  • Reference
GitHub (opens new window)
  • 操作系统

  • 计算机网络

  • 算法与数据结构

    • 玩转算法面试

      • 01-复杂度分析
      • 02-数组问题
      • 03-查找表问题
      • 04-链表问题
      • 05-栈、队列、优先队列
      • 06-二叉树和递归
      • 07-递归和回溯法
        • 问题分析
        • 回溯法介绍
        • 相关问题
        • 问题分析
        • 相关问题
        • 问题分析
        • 相关问题
        • 问题分析
        • 相关问题
        • 问题分析
        • 问题分析
        • 相关问题
      • 08-动态规划
      • 09-贪心算法
  • 内功心法
  • 算法与数据结构
  • 玩转算法面试
CodeAshen
2023-02-10
目录

07-递归和回溯法

# 树形问题(电话号码的字母组合)

上一章中,我们介绍了二叉树相关问题,通常需要使用递归算法,这一章我们来看一下更多递归的应用,以及使用递归算法时一个非常经典的思想——回溯法,这个思想通常都应用在一类问题上,我们称作树形问题。这类问题本身没有定义在二叉树机构中,但是当我们具体分析后,会发现解决这个问题的思路本质是一棵树的形状。

这一节我们先从一个比较简单的问题入手。

# 问题分析

17. 电话号码的字母组合 (opens new window)中等

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

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

img
输入:digits = "23" 
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
1
2

如下图所示,我们以 digits = "23" 为例,先来看 2 可以代表 (a, b , c) 三个字母,所以我们就需要基于 (a, b , c) 这三种可能来考虑下一个数字 3 能代表哪些字母,3 能代表 (d, e, f) 三个字母,所以我们就得到了 3 * 3 = 9 种字母组合 ["ad","ae","af","bd","be","bf","cd","ce","cf"]。

image-20210902153448426

经过分析,我们看到形成了一棵树,这类问题的思路是隐藏在一颗树种的,所以我们把这类问题称为树形问题。因为是树形结构,所以我们很容易想到使用递归的方式来解决,这个问题的递归结构在哪里呢?

我们从 2 开始看可以表示哪些字母,我们只需要求出 3 能代表哪些字母,然后在前面加上 2 所能代表的这些字母,一起就构成了结果。

/**
 * 时间复杂度: O(3^m * 4^n),m 为可以代表 3 个字母的数字个数,n 为可以代表 4 个字母的数字个数
 * 空间复杂度: O(m + n),m+n 为输入数字的总个数
 */
class Solution {
    // 每个数字对应的字母,0 和 1 在本题中不用
    private String[] letterMap = {
        " ",    // 0
        "",     // 1
        "abc",  // 2
        "def",  // 3
        "ghi",  // 4
        "jkl",  // 5
        "mno",  // 6
        "pqrs", // 7
        "tuv",  // 8
        "wxyz"  // 9
    };

    // 存放结果
    private List<String> res;

    public List<String> letterCombinations(String digits) {
        res = new ArrayList<>();
        if (digits.equals(""))
            return res;
        findCombination(digits, 0, "");
        return res;
    }

    /**
     * 翻译 digits 字符串 index 索引下的字符,拼接到字符串 s 上,然后添加到结果 res 中
     *
     * @param digits 数字字符串
     * @param index  将要翻译的索引
     * @param str    到目前位置翻译的结果,当翻译到 digits 最后一位时将其添加到 res 中
     */
    private void findCombination(String digits, int index, String str) {
        if (index == digits.length()) {
            res.add(str);
            return;
        }

        String letters = letterMap[digits.charAt(index) - '0'];
        for (int i = 0; i < letters.length(); i++)
            findCombination(digits, index + 1, str + letters.charAt(i));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 什么是回溯

# 回溯法介绍

下面我们结合图示来解释上一节题目对应程序的运行过程。

image-20210902153448426

2 可以表示 (a, b, c) 三个字母,我们先尝试 2=a 的时候来到 3 节点,对于 3 可以表示 (d, e, f),当 3 表示 d 我们就得到了 ad, 然后回到 3 节点,它还可以表示 e 我们就得到了 ae,再回到 3 节点还可以表示 f 我们就得到了 af,此时 3 的所以可能已经尝试完了,我们再回到 2 节点,2 还可以表示 b……

通过上面的过程我们得到递归调用一个非常重要的性质——要返回。即递归调用结束之后我们总是要返回到上一层继续调用,每一层的递归调用都是如此,我们要逐步返回,直到在根节点的那次递归调用的所有可能性都尝试完成,我们整个的递归函数才结束。也正是如此我们这种递归尝试寻找答案的规程也被称之为 回溯。

也就是说我们沿着一条路径寻找答案,一旦找到答案或者没找到答案就回去继续找,以此类推,这个过程就是回溯。用这个概念看的话我们在上一章学习的跟树相关的算法,本质也是回溯,因为我们使用了递归,递归的话就需要返回。只不过 回溯法 这个词通常被用于问题是查找一个解。

上一节算法的时间复杂度大致是 3^n = O(2^n),是一个指数级的算法,所以其效率是非常低的。我们来结合这棵树看,第一层有 3 种情况,第二层有 9 种情况,一直下去,是以指数级上升的。

回溯法是暴力解法的一个主要实现手段。 我们遇到是很多问题要枚举其所有可能,如果不能使用简单的循环遍历的话,就需要使用这种回溯法。

大家可以思考下对于这个问题的特点是什么,特点是对于 n 是一个变量。如果它的长度是固定的,比如 8,那我们就可以使用 8 重循环来枚举所有的可能性。但是现在长度是动态的,我们就可以使用这种回溯法来枚举所有的可能性。我们后面会看到动态规划也是在回溯法的基础上构建的。

有些问题我们只能使用回溯法这种暴力解法来解决,但是在回溯的过程中我们可以通过 剪枝 不用到达所有的的叶子节点,从而提高算法效率,后面我们将接触到。

# 相关问题

93. 复原 IP 地址 (opens new window)中等

给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 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 地址。

/**
 * 回溯
 * 时间复杂度: 复杂,不会算
 * 空间复杂度: O(4),表示需要 4 个网段,递归深度是 4
 */
class Solution {
    private List<String> res;       // 存放所有可能的 ip 地址
    private List<String> segments;  // 存放已经拼入的网段

    public List<String> restoreIpAddresses(String s) {
        res = new ArrayList<>();
        segments = new ArrayList<>();
        dfs(s, 0);
        return res;
    }

    /**
     * 从 s 的 start 位置开始找出一个网段加入 segments 中,
     * 直到 segments 中够 4 个,并且 s 恰好用完就加入结果集 res 中
     *
     * @param s     原始字符串
     * @param start 新网段开始位置
     */
    private void dfs(String s, int start) {
        // 只要 segments 满 4 个了就要结束递归
        if (segments.size() == 4) {
            // 如果 s 正好用完,表示成功解析 ip,加入结果集
            if (start == s.length())
                res.add(String.join(".", segments));
            return;
        }

        int unallocated = s.length() - start;   // 剩余未分配的字符个数
        int need = 4 - segments.size();         // 还需要几个网段
        // 不够分配或者分配不下了,结束递归(剪枝)
        if (unallocated < need || unallocated > need * 3) return;

        // 继续拼入网段,长度 length 可取 1 到 Math.min(3, unallocated)
        for (int length = 1; length <= Math.min(3, unallocated); length++) {
            String segment = s.substring(start, start + length);  // 新网段
            // 判断 segment 是否为有效网段:不能有先导零且数值不大于 255
            if (!(length > 1 && segment.startsWith("0")) && Integer.parseInt(segment) <= 255) {
                segments.add(segment);  // 拼入网段
                dfs(s, start + length);  // 继续递归
                segments.remove(segments.size() - 1);  // 回溯,拿出网段
            }
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

131. 分割回文串 (opens new window)中等

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

回文串 是正着读和反着读都一样的字符串。

  1. 回溯 + 记忆化搜索

    /**
     * 回溯 + 记忆化搜索
     * 时间复杂度: O(n * 2^n)
     * 空间复杂度: O(n^2)
     */
    class Solution {
        // memo[l][r] 表示 s[l...r] 是否回文串,0:未计算,1:是,-1:不是
        private int[][] memo;
        // 存放结果
        private List<List<String>> res;
        
        public List<List<String>> partition(String s) {
            res = new ArrayList<>();
            memo = new int[s.length()][s.length()];
            dfs(s, 0, new LinkedList<>());
            return res;
        }
    
        /**
         * 尝试将从 s 的 start 位置开始拼入一个回文串,加入 list
         */
        private void dfs(String s, int start, LinkedList<String> list) {
            // s 遍历完了,存入结果返回
            if (start == s.length()) {
                res.add(new LinkedList<>(list));
                return;
            }
            // 尝试继续加入一个回文串
            for (int end = start; end < s.length(); end++) {
                if (isPalindrome(s, start, end) == 1) {   // 如果是回文串
                    list.addLast(s.substring(start, end + 1));  // 拼入
                    dfs(s, end + 1, list);  // 递归继续拼入
                    list.removeLast();  // 回溯,尝试下一个长度子串
                }
            }
        }
    
        /**
         * 验证 s[l...r] 是不是回文串
         */
        private int isPalindrome(String s, int l, int r) {
            if (memo[l][r] != 0) return memo[l][r];
            
            if (l >= r) {
                memo[l][r] = 1;
            } else {
                if (s.charAt(l) == s.charAt(r)) {
                    memo[l][r] = isPalindrome(s, l + 1, r - 1);
                } else {
                    memo[l][r] = -1;
                }
            }
            return memo[l][r];
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
  2. 回溯 + 动态规划

    // todo
    
    1

# 排列问题(全排列)

之前通过一个问题详细介绍了回溯算法,下面几个小节来看看回溯算法的应用,看看它能处理哪些问题。首先回溯算法能处理一类非常重要的问题—— 排列问题。

# 问题分析

46. 全排列 (opens new window)中等

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

解题分析可参考:回溯算法入门级详解 + 练习 (opens new window)

排列问题的递归回溯结构如下图所示。

image-20210902165504606

用表达式可以表示为:

Perms(nums) = {取出一个数字} + Perms(nums - 这个数字)

大家要注意这个问题和之前的 17. 电话号码的字母组合 (opens new window) 问题稍有不同,前一个问题每一个数字代表一个字母,数字和数字之间是不冲突的。对本题来说,每次取出一个数字,都影响下一递归调用的要处理的数据范围,为了解决这个问题,我们在编程的时候还需要借助一些辅助的数据结构。

/**
 * 回溯
 * 时间复杂度: O(n×n!)
 * 空间复杂度: O(n)
 */
class Solution {
    private List<List<Integer>> res;
    private boolean[] used;    // 记录 nums[i] 是否已经在排列中

    public List<List<Integer>> permute(int[] nums) {
        res = new LinkedList<>();
        used = new boolean[nums.length];
        dfs(nums, new LinkedList<>());
        return res;
    }

    // 将 nums 的排列逐个加入 list 中
    private void dfs(int[] nums, LinkedList<Integer> list) {
        // 排列满了,存入结果集
        if (list.size() == nums.length) {
            res.add(new LinkedList<>(list));
            return;
        }
        // 选出一个元素,加入排列中
        for (int i = 0; i < nums.length; i++) {
            if (!used[i]) {
                list.add(nums[i]);  // 加入排列
                used[i] = true;
                dfs(nums, list);    // 将下一个元素放到排列中
                list.removeLast();  // 回溯
                used[i] = false;
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 相关问题

47. 全排列 II (opens new window)中等

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

核心思想就是,保证在排列 idx 位置上填入数字的时候,重复数字只会被尝试填入 1 次。

  1. 解法一:记录 idx 位置尝试过的数字

    /**
     * 回溯
     * 时间复杂度: O(n×n!)
     * 空间复杂度: O(n)
     */
    class Solution1 {
        private List<List<Integer>> res;
        private boolean[] used;     // 记录 nums[i] 是否已经在排列中
    
        public List<List<Integer>> permuteUnique(int[] nums) {
            res = new ArrayList<>();
            used = new boolean[nums.length];
            dfs(nums, new LinkedList<>());
            return res;
        }
    
        // 将有序数组 nums 的排列逐个加入 list 中
        private void dfs(int[] nums, LinkedList<Integer> list) {
            if (list.size() == nums.length) {
                res.add(new LinkedList<>(list));
                return;
            }
    
            HashSet<Integer> tried = new HashSet<>();  // 本位置尝试过的数字
            for (int i = 0; i < nums.length; i++) {
                // (1) nums[i] 已经在排列中,跳过
                // (2) 和 nums[i] 的数字已经在本位置尝试填入过了,跳过,保证了重复数字只会被填入一次
                if (used[i] || tried.contains(nums[i]))
                    continue;
    
                // 记录选择的数字
                tried.add(nums[i]);
                // 填入排列中,并继续递归回溯
                list.add(nums[i]);
                used[i] = true;
                dfs(nums, list);
                list.removeLast();
                used[i] = false;
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
  2. 解法二:排序 nums,每次只记录前一次在 idx 位置尝试的数字

    /**
     * 回溯
     * 时间复杂度: O(n×n!)
     * 空间复杂度: O(n)
     */
    class Solution2 {
        private List<List<Integer>> res;
        private boolean[] used;     // 记录 nums[i] 是否已经在排列中
    
        public List<List<Integer>> permuteUnique(int[] nums) {
            res = new ArrayList<>();
            used = new boolean[nums.length];
            Arrays.sort(nums);  // 必须先排序
            dfs(nums, new LinkedList<>());
            return res;
        }
    
        // 将有序数组 nums 的排列逐个加入 list 中
        private void dfs(int[] nums, LinkedList<Integer> list) {
            if (list.size() == nums.length) {
                res.add(new LinkedList<>(list));
                return;
            }
            
            // 选出一个元素,填入排列的 list,位置是 list.size()
            Integer lastChoice = null;  // 记录最后选出尝试填入 list.size() 位置的元素,注意不是排列的上一个位置 list.size() - 1
            for (int i = 0; i < nums.length; i++) {
                // (1) nums[i] 已经在排列中
                // (2) 本位置没有尝试过和 nums[i] 相同的元素,利用 nums 有序的特点,重复元素是连续的,只有第一个会被选出尝试,
                //     后续的因为 lastChoice 的限制会跳过
                if (used[i] || (lastChoice != null && nums[i] == lastChoice))
                    continue;
    
                // 记录选择的元素
                lastChoice = nums[i];
                // 填入排列中,并继续递归回溯
                list.add(nums[i]);
                used[i] = true;
                dfs(nums, list);
                list.removeLast();
                used[i] = false;
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44

# 组合问题(组合)

这一节我们来看回溯算法解决的另一类问题——组合问题。

# 问题分析

77. 组合 (opens new window)中等

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

解题分析可参考:回溯算法 + 剪枝(Java) (opens new window)

组合问题的递归回溯结构如下图所示。

image-20210902175151911

上图即为从 4 个数中取两个数能得到的所有组合,这棵树看起来跟之前的树不一样,之前的树每个节点有集合孩子是固定的,这棵树不固定。但是它依然是这样一个树形的结构,我们依然可以使用递归解决,过程中我们依然能够看到回溯的过程。

上面的递归图跟我们小学做组合题目画的画有相似之处,可以结合起来理解。

image-20210903135715747

我们在每次回溯后再次进行递归操作时,只需要考虑这个元素之后的元素即可,因为前面的元素已经考虑过了。

  1. 根据上述递归树实现回溯(多叉树回溯)

    /**
     * 回溯 + 剪枝
     */
    class Solution {
        private List<List<Integer>> res;  // 存放结果
        public List<List<Integer>> combine(int n, int k) {
            res = new ArrayList<>();
            dfs(n, k, 1, new LinkedList<>());
            return res;
        }
        /**
         * 求解 C(n,k),当前已经找到的组合存储在 path 中,需要从 start 开始搜索新的元素
         *
         * @param n     区间大小 [1...n](固定)
         * @param k     求 k 个元素的组合(固定)
         * @param start 从 start 开始往后找(递增)
         * @param path  存放组合元素(元素个数递增)
         */
        private void dfs(int n, int k, int start, LinkedList<Integer> path) {
            // 如果已经找到了 k 个元素,保存返回
            if (path.size() == k) {
                res.add(new LinkedList<>(path));  // 必须存副本
                return;
            }
            
            // 还需要加入 k-path.size 个元素,还剩 n-i+1 个数字,需满足剩余数字个数大于还需个数,
            // 即 n - i + 1 >= k - path.size
            for (int i = start; n - i + 1 >= k - list.size(); i++) {
                path.addLast(i);    // 向组合中加入一个元素
                dfs(n, k, i + 1, path);  // 继续递归
                path.removeLast();  // 回溯,遍历下一个
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
  2. 对于每个数字,选择放入和不放入(二叉树回溯)

    class Solution {
        private List<List<Integer>> res;
        
        public List<List<Integer>> combine(int n, int k) {
            res = new ArrayList<>();
            dfs(n, k, new LinkedList<>());
            return res;
        }
    
        /**
         * 从 [1, n] 中选出 k 个数字,放入 list 中,直到放满。
         * 依次考察 n, n-1, n-2, ... , 1, 对于 n 考虑放入和不放入的情况。
         */
        private void dfs(int n, int k, LinkedList<Integer> list) {
            // 还需要放入 0 个元素,表示已经够了,加入结果集后返回
            if (k == 0) {
                res.add(new LinkedList<>(list));
                return;
            }
            // 如果 [1, n] 不够 k 个元素,直接返回
            if (n < k) return;
            
            // (1) n 不放入组合,去 [1, n-1] 中选 k 个数字放入组合
            dfs(n - 1, k, list);
            // (2) n 放入组合,去 [1, n-1] 中选 k-1 个数字放入组合
            list.add(n);
            dfs(n - 1, k - 1, list);
            list.removeLast();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30

# 相关问题

39. 组合总和 (opens new window)中等

给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。

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

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

  1. 二叉树回溯,思路:组合总和 (opens new window)

    思考的是对与每个元素加入或不加入

    /**
     * 回溯法,二叉树
     * 分叉为是否选取元素 candidates[i],有两种选择
     */
    class Solution {
        private List<List<Integer>> res;  // 存放结果
        public List<List<Integer>> combinationSum(int[] candidates, int target) {
            res = new ArrayList<>();
            dfs(candidates, target, 0, new LinkedList<>());
            return res;
        }
        /**
         * 考虑将 candidates[index] 加入组合 list 中,使得组合的总和为 target
         */
        private void dfs(int[] candidates, int target, int index, LinkedList<Integer> list) {
            if (index >= candidates.length)
                return;
            if (target == 0) {
                res.add(new LinkedList<>(list));
                return;
            }
            // 情况一:不包含 index 元素
            dfs(candidates, target, index + 1, list);
            // 情况二:包含 index 元素,加入到 list,需要回溯
            if (candidates[index] <= target) {
                list.addLast(candidates[index]);
                dfs(candidates, target - candidates[index], index, list);
                list.removeLast();
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
  2. 多叉树回溯,思路:回溯算法 + 剪枝(回溯经典例题详解) (opens new window)

    思考的是下一个加入组合的元素是谁

    /**
     * 回溯法,多叉树
     * 分叉为 candidates 的元素个数,每次选 i 及其之后的元素
     */
    class Solution2 {
        private List<List<Integer>> res;  // 存放结果
        public List<List<Integer>> combinationSum(int[] candidates, int target) {
            res = new ArrayList<>();
            dfs(candidates, target, 0, new LinkedList<>());
            return res;
        }
        /**
         * 考虑将 candidates 数组 start 及以后的元素加入组合 list 中,使得组合的总和为 target
         */
        private void dfs(int[] candidates, int target, int start, LinkedList<Integer> list) {
            if (target == 0) {
                res.add(new LinkedList<>(list));
                return;
            }
            
            for (int i = start; i < candidates.length; i++) {
                if (candidates[i] <= target) {
                    list.addLast(candidates[i]);
                    // 注意:由于每一个元素可以重复使用,下一轮搜索的起点依然是 i,这里非常容易弄错
                    dfs(candidates, target - candidates[i], i, list);
                    list.removeLast();  // 重置状态
                }
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30

40. 组合总和 II (opens new window)中等

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

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

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

216. 组合总和 III (opens new window)中等

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

  • 所有数字都是正整数。
  • 解集不能包含重复的组合。

78. 子集 (opens new window)中等

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

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

90. 子集 II (opens new window)中等

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

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

401. 二进制手表 (opens new window)简单

二进制手表顶部有 4 个 LED 代表 小时(0-11),底部的 6 个 LED 代表 分钟(0-59)。每个 LED 代表一个 0 或 1,最低位在右侧。

  • 例如,下面的二进制手表读取 "3:25" 。
img

给你一个整数 turnedOn ,表示当前亮着的 LED 的数量,返回二进制手表可以表示的所有可能时间。你可以 按任意顺序 返回答案。

小时不会以零开头:

  • 例如,"01:00" 是无效的时间,正确的写法应该是 "1:00" 。

分钟必须由两位数组成,可能会以零开头:

  • 例如,"10:2" 是无效的时间,正确的写法应该是 "10:02" 。

# 二维平面上的回溯法(单词搜索)

# 问题分析

79. 单词搜索 (opens new window)中等

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

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

class Solution {
    // used[i][j] == true 表示 board[i][j] 已经被使用
    private boolean[][] used;

    public boolean exist(char[][] board, String word) {
        int m = board.length;
        int n = board[0].length;
        used = new boolean[m][n];
        // 选取起点
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 从任一起点出发能找到符合的路径,就成功
                if (check(board, i, j, word, 0))
                    return true;
            }
        }
        return false;  // 所有路径都查找失败
    }

    /**
     * 从 board[i][j] 开始,匹配单词 word 从 k 到末尾的部分
     */
    private boolean check(char[][] board, int i, int j, String word, int k) {
        // 角标越界,不成功
        if (i < 0 || i >= board.length || j < 0 || j >= board[0].length)
            return false;
        // 字符已被使用,或字符不匹配,不成功
        if (used[i][j] || board[i][j] != word.charAt(k))
            return false;
        // 匹配且已经查找到单词末尾了,成功
        if (k == word.length() - 1)
            return true;

        used[i][j] = true;    // 标记已使用
        // 递归到上下左右匹配单词剩余部分,任一匹配成功则成功
        if (check(board, i + 1, j, word, k + 1)) return true;
        if (check(board, i - 1, j, word, k + 1)) return true;
        if (check(board, i, j + 1, word, k + 1)) return true;
        if (check(board, i, j - 1, word, k + 1)) return true;
        used[i][j] = false;   // 回溯,清除使用标志
        
        return false;  // 四个方向都没匹配成功,失败
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 相关问题

130. 被围绕的区域 (opens new window)中等

给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。

417. 太平洋大西洋水流问题 (opens new window)中等

给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。

规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。

请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。

# floodfill 算法,一类经典问题(岛屿数量)

# 问题分析

200. 岛屿数量 (opens new window)中等

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

# 回溯法是经典人工智能的基础(N 皇后)

# 问题分析

51. N 皇后 (opens new window)困难

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

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

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

# 相关问题

52. N皇后 II (opens new window)困难

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

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

37. 解数独 (opens new window)困难

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

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

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

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

编辑 (opens new window)
上次更新: 2023/06/04, 12:34:19
06-二叉树和递归
08-动态规划

← 06-二叉树和递归 08-动态规划→

最近更新
01
第01章-RabbitMQ导学
02-10
02
第02章-入门RabbitMQ核心概念
02-10
03
第03章-RabbitMQ高级特性
02-10
更多文章>
Theme by Vdoing | Copyright © 2020-2023 CodeAshen | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式