当前位置: 首页 > news >正文

【力扣】Go语言回溯算法详细实现与方法论提炼

文章目录

    • 一、引言
    • 二、回溯算法的核心概念
    • 三、组合问题
      • 1. LeetCode 77. 组合
      • 2. LeetCode 216. 组合总和III
      • 3. LeetCode 17. 电话号码的字母组合
      • 4. LeetCode 39. 组合总和
      • 5. LeetCode 40. 组合总和 II
      • 小结
    • 四、分割问题
      • 6. LeetCode 131. 分割回文串
      • 7. LeetCode 93. 复原IP地址
      • 小结
    • 五、子集问题
      • **8. LeetCode 78. 子集**
      • 9. LeetCode 90. 子集 II
      • 小结
    • 六、排列问题
      • 10. LeetCode 46. 全排列
      • 11. LeetCode 47. 全排列 II
      • 小结
    • 七、棋盘问题
      • 12. LeetCode 51. N皇后
      • 13. LeetCode 37. 解数独
      • 小结
    • 八、其他复杂问
      • 14. LeetCode 491. 递增子序列
      • 15. LeetCode 332. 重新安排行程
      • 小结
    • 总结


回溯


一、引言

回溯算法是一种利用递归解决搜索问题的常见方法,它在多个编程题型中展现出独特的优势。无论是组合问题、分割问题、子集问题,还是排列、棋盘问题,回溯算法的核心都是在构建路径的同时,动态地选择、撤销操作,最终完成一个全面的搜索。本系列文章将通过15道经典的LeetCode题目,用Go语言实现并深入解析回溯算法的设计模式,帮助您系统化掌握回溯算法在各类问题中的应用。

二、回溯算法的核心概念

在学习回溯算法前,理解其核心概念非常重要。在所有回溯算法的实现中,都离不开以下几个关键部分:

  1. 递归与回溯的关系

    • 回溯算法是一种基于递归的搜索算法。递归逐层深入,构建一个状态空间树(即递归树),每层递归都代表决策路径的一个选择。
    • 一旦递归到达终止条件,回溯将会逐层返回,撤销不符合条件的选择,形成“退一步再试其他路径”的机制。
  2. 终止条件

    • 每个回溯算法都有一个明确的终止条件,当条件满足时,算法会返回或将结果存储下来。典型的例子包括达成某个组合的长度、和为目标值、或者生成所有排列。
  3. 结果收集条件

    • 在递归过程中,只有满足条件的路径才会被收集。条件可能是固定的组合大小、具体的和、路径的合法性等。
    • 例如在N皇后问题中,结果收集条件是所有皇后均放置完毕且不冲突。
  4. 去重逻辑

    • 对于一些包含重复元素的问题(如排列或子集问题),去重是关键。常用的去重策略包括提前排序和跳过相同元素。
  5. 递归控制

    • 循环顺序决定了递归树的遍历路径;递归顺序决定了函数返回的优先顺序。一般来说,组合问题在递归内部通过for循环控制选择的顺序,而排列问题则会基于深度优先搜索实现路径的构建。
  6. 回溯算法整体框架流程图

开始
选择元素
是否满足条件?
回溯
记录结果
是否所有元素均尝试?
结束

在接下来的章节中,我们将具体分析这15道力扣题目,深入解读回溯算法在不同问题场景下的应用与技巧。


三、组合问题

组合问题的特点是每个解都是一个子集或集合,且不要求集合中的元素有顺序性。下面是具体题目实现与解析。


1. LeetCode 77. 组合

题目分析
题目要求从1n中选出k个数字的组合。通过递归逐步构建组合,若达到所需长度则收集结果。此题中的路径长度就是终止条件。

组合问题的回溯流程

开始组合生成
选择第一个元素
递归选择下一个元素
达到目标组合长度?
记录组合
回溯,撤销选择
是否有更多元素可选?
结束

Go语言实现

func combine(n int, k int) [][]int {var res [][]int        // 存储最终结果var path []int         // 存储当前组合路径var backTrack func(s int)  // 定义回溯函数backTrack = func(s int) {// 当路径长度达到k时,保存当前路径并返回if len(path) == k {temp := make([]int, k)copy(temp, path)res = append(res, temp)return}// 从s开始选择元素for i := s; i <= n; i++ {path = append(path, i)    // 选择元素i加入路径backTrack(i + 1)          // 递归调用下一层path = path[:len(path)-1] // 撤销选择}}backTrack(1)  // 从1开始递归return res
}

详细解读

  1. 递归与回溯backTrack函数是递归的核心,每次选择后续数字中的一个加入路径。
  2. 终止条件:当路径长度等于k时,保存当前路径(组合)。
  3. 结果收集条件:在路径长度等于k时,将组合结果收集到res中。
  4. 去重逻辑:题目不允许重复选择,因此每次递归从sn,每层递归仅选择一次。
  5. 递归控制:循环顺序由sn,确保每次递归的元素都是唯一且不重复的。

2. LeetCode 216. 组合总和III

题目分析
从数字1-9中选出k个数字,使它们的和等于目标值n。在组合过程中需保持路径长度和总和满足条件。

Go语言实现

func combinationSum(k int, n int) [][]int {var res [][]intvar path []intvar sum int          // 当前路径的总和var backTrack func(s int)backTrack = func(s int) {// 若路径长度和总和同时满足要求,保存结果if len(path) == k && sum == n {temp := make([]int, k)copy(temp, path)res = append(res, temp)return}// 剪枝条件:路径长度超过k或总和超过n时直接返回if len(path) > k || sum > n {return}// 从s到9逐一选择元素for i := s; i <= 9; i++ {path = append(path, i)    // 选择i加入路径sum += i                  // 更新路径总和backTrack(i + 1)          // 递归调用下一层sum -= i                  // 撤销选择ipath = path[:len(path)-1] // 回溯到上层状态}}backTrack(1)return res
}

详细解读

  1. 终止条件:当路径长度达到k且总和等于n时,保存组合。
  2. 结果收集条件:在满足kn的条件时,记录路径。
  3. 去重逻辑:依次递归选取1-9的数字,通过s递归参数限制重复选择。
  4. 剪枝条件:提前检查路径长度和总和,避免无效递归。
  5. 递归控制:使用s9的递增顺序,保证路径中无重复数字。

3. LeetCode 17. 电话号码的字母组合

题目分析
给定一个仅包含数字2-9的字符串,返回所有它能表示的字母组合。每个数字代表一组字母,要求构造出所有可能的字母排列组合。

思路解析
这道题的关键在于逐层递归处理每个数字对应的字母组合,并在满足条件后将路径保存下来。递归深度等于数字长度,而每层递归中的循环则负责选择不同的字母。通过回溯删除不符合的路径,从而覆盖所有组合。

Go语言实现

func letterCombinations(digits string) []string {letters := map[byte][]byte{'2': {'a', 'b', 'c'},'3': {'d', 'e', 'f'},'4': {'g', 'h', 'i'},'5': {'j', 'k', 'l'},'6': {'m', 'n', 'o'},'7': {'p', 'q', 'r', 's'},'8': {'t', 'u', 'v'},'9': {'w', 'x', 'y', 'z'},}var res []stringvar path []bytel := len(digits)if l == 0 {return res}var backTrack func(s int)backTrack = func(s int) {// 当路径长度达到数字串长度时,将路径转换为字符串并保存if s == l {res = append(res, string(path))return}// 选择当前数字对应的每个字母for _, ch := range letters[digits[s]] {path = append(path, ch)   // 选择一个字母backTrack(s + 1)          // 递归到下一数字path = path[:len(path)-1] // 撤销选择,回溯到上层状态}}backTrack(0)return res
}

详细解读

  1. 终止条件

    • path长度与digits长度一致时,将当前路径拼接成字符串,并存入结果集。
  2. 结果收集条件

    • path长度等于digits时,收集路径。
  3. 去重逻辑

    • 每个数字的字母都是固定且不重复的,因此不需要特殊去重处理。
  4. 递归控制

    • 外层递归控制数字索引;内层循环处理每个数字对应的字母组合。

4. LeetCode 39. 组合总和

题目分析
从给定的正整数数组candidates中找到所有可能的组合,使得组合中的元素之和等于目标值target。数组中元素可以重复使用。

思路解析
为保证路径中的数字和达到target且避免重复组合,需要在递归中控制好路径的总和。每层递归会从当前位置到数组结尾进行选择,允许重复选择。若总和超出目标值target,立即停止递归。

Go语言实现

func combinationSum(candidates []int, target int) [][]int {var res [][]intvar path []intvar sum intvar backTrack func(s int)backTrack = func(s int) {// 当总和超出目标时,停止递归if sum > target {return}// 当总和达到目标时,记录当前路径if sum == target {temp := make([]int, len(path))copy(temp, path)res = append(res, temp)return}// 从s到数组末尾尝试每个候选数for i := s; i < len(candidates); i++ {path = append(path, candidates[i]) // 选择候选数sum += candidates[i]               // 更新路径总和backTrack(i)                       // 递归允许重复选择sum -= candidates[i]               // 回溯到上层path = path[:len(path)-1]          // 撤销选择}}backTrack(0)return res
}

详细解读

  1. 终止条件

    • 总和等于目标target时,将路径存入结果集;
    • 若总和超过target,则提前返回,避免无效递归。
  2. 结果收集条件

    • sum == target时,保存路径。
  3. 去重逻辑

    • 候选元素位置递增,无需额外去重。
  4. 递归控制

    • 每层递归从s开始,允许同元素在递归中多次选择,形成重复组合的路径。

5. LeetCode 40. 组合总和 II

题目分析
本题和上一题类似,但要求组合中的每个数字只能使用一次,并且不能有重复组合。即在数组中选取不同组合使其和为target,每个组合内部元素可以重复,但组合间不能有重复。

思路解析
通过提前对数组排序以及递归中的剪枝处理来去重,从而确保不会重复选择同一个元素。每层递归的选择会跳过重复元素,避免组合重复。

Go语言实现

func combinationSum2(candidates []int, target int) [][]int {var res [][]intvar path []intvar sum intsort.Ints(candidates)     // 预排序以方便去重var backTrack func(s int)backTrack = func(s int) {if sum == target {temp := make([]int, len(path))copy(temp, path)res = append(res, temp)return}if sum > target {return}for i := s; i < len(candidates); i++ {// 去重:若当前数等于上一个,且在同一层递归中未被选过,则跳过if i > s && candidates[i] == candidates[i-1] {continue}path = append(path, candidates[i]) // 选择候选数sum += candidates[i]               // 更新路径总和backTrack(i + 1)                   // 从下一个元素递归sum -= candidates[i]               // 回溯到上层path = path[:len(path)-1]          // 撤销选择}}backTrack(0)return res
}

详细解读

  1. 终止条件

    • sum等于target时,将路径加入结果集;
    • sum超过target时,提前返回,停止当前递归。
  2. 结果收集条件

    • sum == target时,保存路径。
  3. 去重逻辑

    • 排序后在循环中跳过重复元素candidates[i] == candidates[i-1],保证每个组合的唯一性。
  4. 递归控制

    • 每层递归选择当前或后续元素,确保每个数只能选一次并避免重复组合。

小结

在组合问题中,回溯算法的核心在于控制组合生成的方式和路径。结合剪枝、去重等方法,不仅能提高算法效率,还能避免重复组合的生成。在实现上,终止条件和结果收集条件的正确设置十分关键。此外,通过for循环控制递归深度和选择范围,能够生成符合题目要求的组合解。


四、分割问题

分割问题的核心在于将给定的字符串分解成多个满足条件的部分。通过回溯逐层递归地探索各个可能的分割点,确保每一层递归选择的部分满足题目要求。以下是具体题目的解析与实现。


6. LeetCode 131. 分割回文串

题目分析
给定一个字符串s,要求将其分割,使每个分割后的子串都是回文串。返回所有可能的回文分割方案。

思路解析
该题目需要使用回溯和分治的思想,逐层递归探索字符串的每一个分割位置。对于每一个可能的分割点,检查其是否是回文串,如果是,则继续递归剩下的部分;否则跳过。

Go语言实现

// 辅助函数:检查是否为回文串
func isPalindrome(s string) bool {for l, r := 0, len(s)-1; l < r; l, r = l+1, r-1 {if s[l] != s[r] {return false}}return true
}func partition(s string) [][]string {var res [][]stringvar path []stringl := len(s)var backTrack func(start int)backTrack = func(start int) {// 终止条件:到达字符串末尾时,记录当前分割路径if start == l {temp := make([]string, len(path))copy(temp, path)res = append(res, temp)return}// 从start位置开始尝试分割每个可能的子串for i := start; i < l; i++ {if !isPalindrome(s[start : i+1]) { // 判断子串是否为回文continue}path = append(path, s[start:i+1]) // 选择当前回文子串backTrack(i + 1)                  // 递归处理剩余部分path = path[:len(path)-1]         // 撤销选择,回溯到上层}}backTrack(0)return res
}

详细解读

  1. 终止条件

    • 当遍历到字符串的末尾时(start == l),将当前路径path存入结果集中。
  2. 结果收集条件

    • 若当前分割路径到达字符串末尾,将其存入结果集。
  3. 去重逻辑

    • 本题不存在重复组合的问题,因为每次选择都基于字符串的索引位置,保证了结果的唯一性。
  4. 递归控制

    • 通过for循环控制每一层的分割起始位置,逐层向后分割,确保各个分割部分为回文串。

7. LeetCode 93. 复原IP地址

题目分析
给定一个只含数字的字符串,要求将其复原成可能的有效IP地址。IP地址由四个十进制部分组成,每部分在0到255之间,且不能包含前导零。

思路解析
该题目使用回溯尝试将字符串分割为4部分,每部分代表IP地址的一个段。需要特别注意的是,IP地址每部分的范围限定为0-255,且不能有前导零。递归过程中,当达到4段且字符串用完即为有效分割。

Go语言实现

import "strconv"// 辅助函数:检查是否为有效IP地址段
func isValidSegment(s string) bool {if len(s) == 0 || (len(s) > 1 && s[0] == '0') {return false}num, _ := strconv.Atoi(s)return num >= 0 && num <= 255
}func restoreIpAddresses(s string) []string {var res []stringvar path []stringl := len(s)var backTrack func(start int)backTrack = func(start int) {// 当path有4段且字符串用完,表示找到一个有效IPif len(path) == 4 && start == l {res = append(res, path[0]+"."+path[1]+"."+path[2]+"."+path[3])return}// 若path已达4段但未用完字符串,提前结束if len(path) == 4 {return}// 遍历每个可能的分割点,每段最多3位for i := start; i < min(start+3, l); i++ {segment := s[start : i+1]if !isValidSegment(segment) {continue}path = append(path, segment) // 选择有效IP段backTrack(i + 1)             // 递归处理剩余字符串path = path[:len(path)-1]    // 回溯,撤销选择}}backTrack(0)return res
}func min(a, b int) int {if a < b {return a}return b
}

详细解读

  1. 终止条件

    • path长度等于4段且字符串用完时,表示找到一个有效的IP地址,将其存入结果集。
    • path已达4段,但未用完字符串,则提前结束当前路径。
  2. 结果收集条件

    • 在满足4段IP和完整字符串条件时,将路径加入结果集。
  3. 去重逻辑

    • 本题不存在重复组合,因为每段分割仅对应一个合法的IP段。
  4. 递归控制

    • 递归逐层分割,每层递归最多选择3个字符作为IP地址段,避免超过有效长度。

小结

分割问题的回溯算法设计关键在于:

  1. 分割点的选择与递归控制:每一层递归都尝试不同的分割点,以探索多种可能的路径。
  2. 有效性校验:分割后的子串必须符合特定条件(如回文或IP段的范围),在递归选择时及时进行校验,避免无效路径。
  3. 路径收集条件:只有满足条件的路径(如达到分割段数)才会收集到最终结果集中。

五、子集问题

子集问题需要在给定数组中找到所有可能的子集组合。这里的关键是每个元素的选择与不选择,形成不同的组合方案。子集问题的核心思路是通过递归回溯,逐步生成所有可能的子集组合。


8. LeetCode 78. 子集

题目分析
给定一个不含重复元素的整数数组nums,求所有的子集(包括空集)。该题要求返回所有可能的子集,因此每个元素都有加入和不加入当前组合的两种选择。

思路解析
通过递归遍历数组,分别处理每个元素的“加入”和“不加入”操作,逐步构建出所有的子集。由于子集中没有顺序要求,递归中可以按顺序加入新元素,不必考虑排列顺序。

Go语言实现

func subsets(nums []int) [][]int {var res [][]intvar path []intl := len(nums)var backTrack func(start int)backTrack = func(start int) {// 将当前路径加入结果集中temp := make([]int, len(path))copy(temp, path)res = append(res, temp)// 从当前元素开始,递归构建子集for i := start; i < l; i++ {path = append(path, nums[i]) // 选择当前元素backTrack(i + 1)             // 递归生成后续子集path = path[:len(path)-1]    // 撤销选择,回溯}}backTrack(0)return res
}

详细解读

  1. 终止条件

    • 每次递归调用都会将当前路径加入结果集,无需特定终止条件。
  2. 结果收集条件

    • 在每层递归中将路径path加入res,表示当前组合就是一个子集。
  3. 去重逻辑

    • 因为数组不包含重复元素,每次递归中不需要去重操作。
  4. 递归控制

    • 每层递归的for循环从当前位置开始,确保每个组合无重复顺序,形成不同的子集。

9. LeetCode 90. 子集 II

题目分析
与上一题不同的是,给定的数组nums中包含重复元素,求所有可能的子集。由于存在重复元素,需要确保相同的元素不会生成重复的子集。

思路解析
为避免重复组合,首先对数组进行排序,这样在递归过程中可以跳过重复的元素。若当前元素与前一个元素相同,且前一个元素未被选择,则跳过当前元素,确保唯一性。

Go语言实现

import "sort"func subsetsWithDup(nums []int) [][]int {var res [][]intvar path []intsort.Ints(nums) // 预排序以便去重var backTrack func(start int)backTrack = func(start int) {// 保存当前路径作为子集temp := make([]int, len(path))copy(temp, path)res = append(res, temp)// 从start位置开始递归for i := start; i < len(nums); i++ {// 去重:若当前元素等于上一个且未被选过,则跳过if i > start && nums[i] == nums[i-1] {continue}path = append(path, nums[i]) // 选择当前元素backTrack(i + 1)             // 递归到下一个位置path = path[:len(path)-1]    // 回溯,撤销选择}}backTrack(0)return res
}

详细解读

  1. 终止条件

    • 每次递归调用都会将当前路径加入结果集,形成一个子集。
  2. 结果收集条件

    • 在每层递归中将当前路径path加入res,即收集当前形成的子集。
  3. 去重逻辑

    • 数组排序后,利用条件if i > start && nums[i] == nums[i-1]跳过重复的元素,防止生成重复子集。
  4. 递归控制

    • 每层递归的for循环从start位置开始,以保证子集生成的顺序不变且不重复。

小结

子集问题通过递归生成所有组合,关键在于:

  1. 选择与不选择的决策:每个元素都面临“加入”或“不加入”的选择,这构成了回溯递归的基本框架。
  2. 去重逻辑的实现:在含重复元素的子集问题中,提前排序并在递归过程中跳过相同元素,确保生成的子集组合唯一。
  3. 路径收集的时机:每个递归点的路径即为一个子集,因此在每次递归时都保存当前路径。

通过对比7890两道题,可以看到在处理重复元素和去重逻辑上的不同应用。接下来我们将进入排列问题的分析与实现。

六、排列问题

排列问题要求对给定数组生成所有可能的排列,特别是每个排列中元素的顺序必须唯一。排列问题的核心思路是使用递归回溯,从给定数组逐个选择元素加入路径,直到路径长度等于数组长度时生成一个排列。


10. LeetCode 46. 全排列

题目分析
给定一个不含重复数字的数组nums,要求返回其所有可能的排列。该题与组合和子集问题的区别在于,排列中的顺序不同会产生不同的排列结果,因此递归过程中需要处理元素的交换和位置选择。

思路解析
使用回溯法,在每层递归中按顺序选择一个未使用的元素加入路径,直到路径长度与数组长度一致。每次递归结束后回退选择,恢复现场,确保所有位置都被遍历到。

Go语言实现

func permute(nums []int) [][]int {var res [][]intvar path []intl := len(nums)used := make(map[int]bool) // 记录每个元素是否已在当前排列中使用var backTrack func()backTrack = func() {// 当路径长度等于数组长度时,保存当前排列if len(path) == l {temp := make([]int, l)copy(temp, path)res = append(res, temp)return}// 遍历每个元素for i := 0; i < l; i++ {if used[nums[i]] {continue // 跳过已使用的元素}used[nums[i]] = true       // 标记元素已使用path = append(path, nums[i]) // 选择当前元素backTrack()                 // 递归生成后续排列path = path[:len(path)-1]   // 撤销选择,回溯used[nums[i]] = false       // 取消标记}}backTrack()return res
}

详细解读

  1. 终止条件

    • 当路径长度等于数组长度时,表示形成了一个完整的排列,加入结果集。
  2. 结果收集条件

    • 在递归层次达到数组长度时,将当前路径path加入结果集。
  3. 去重逻辑

    • 每次递归中使用used字典标记每个元素是否已在当前排列路径中,避免重复选择。
  4. 递归控制

    • for循环遍历数组中每一个元素,并在递归中控制元素选择与撤销,从而生成所有排列。

11. LeetCode 47. 全排列 II

题目分析
与前一题不同的是,给定的数组nums中可能包含重复元素,要求生成的排列中不应有重复项。因此在递归中需要确保相同的元素不会导致重复排列。

思路解析
先对数组排序,这样在递归时可以通过判断跳过相同元素以去重。此外,若前一个相同的元素未被选过,则当前元素也不能选,确保每层递归中生成的排列唯一。

Go语言实现

import "sort"func permuteUnique(nums []int) [][]int {var res [][]intvar path []intl := len(nums)used := make([]bool, l) // 记录每个位置的元素是否已被选中sort.Ints(nums)         // 排序以便去重var backTrack func()backTrack = func() {// 当路径长度等于数组长度时,保存当前排列if len(path) == l {temp := make([]int, l)copy(temp, path)res = append(res, temp)return}// 遍历每个元素for i := 0; i < l; i++ {// 去重逻辑:跳过重复元素if i > 0 && nums[i] == nums[i-1] && !used[i-1] {continue}if used[i] {continue}used[i] = true              // 标记元素已使用path = append(path, nums[i]) // 选择当前元素backTrack()                 // 递归生成后续排列path = path[:len(path)-1]   // 撤销选择,回溯used[i] = false             // 取消标记}}backTrack()return res
}

详细解读

  1. 终止条件

    • 路径长度等于数组长度时,生成一个完整排列,加入结果集。
  2. 结果收集条件

    • 在递归层次等于数组长度时,将当前排列路径path加入res
  3. 去重逻辑

    • 排序后,跳过已选过的相同元素(nums[i] == nums[i-1] && !used[i-1]),避免重复排列的生成,去重操作仅作用于当前递归层。
  4. 递归控制

    • for循环控制排列的顺序,used数组记录每个元素的使用状态,并在递归和回溯中交替使用。

小结

在排列问题中,通过逐层递归生成所有可能的排列,递归过程中确保唯一性至关重要:

  1. 路径收集条件:递归深度等于数组长度时表示一个完整的排列,及时收集。
  2. 去重策略的应用:排序和跳过相同元素的逻辑可有效去除重复排列。
  3. 递归结构:每层递归中控制元素的选择、加入、撤销,通过递归的深度优先遍历实现所有排列的组合。

在比较4647题时可以看到,包含重复元素的排列需要在递归中加入去重策略,确保排列唯一性。接下来将进入棋盘问题的解析与实现。


七、棋盘问题

棋盘问题通常涉及在棋盘上放置棋子(如皇后或数字),满足特定规则。回溯算法在棋盘问题中广泛应用,因为回溯能够有效地处理棋子的放置、撤回以及合法性检测,从而找到所有可能的棋盘解法。


12. LeetCode 51. N皇后

题目分析
n x n的棋盘上放置n个皇后,使得任意两个皇后都不在同一行、同一列或同一条对角线上。要求返回所有可能的放置方案。

思路解析
该题可以通过回溯解决,每次递归在棋盘的某一行放置皇后,然后递归到下一行,检查每个位置是否会与已放置的皇后冲突。若冲突则跳过该位置,若无冲突则放置皇后并继续递归。递归完成后通过回溯撤销选择,以便探索下一种可能方案。

N皇后问题的棋盘递归流程

开始N皇后
在第1行选择位置
是否符合规则?
递归到下一行
是否完成所有行放置?
记录棋盘解法
回溯撤销选择

Go语言实现

func solveNQueens(n int) [][]string {var res [][]stringboard := make([][]string, n) // 创建棋盘for i := range board {board[i] = make([]string, n)for j := range board[i] {board[i][j] = "." // 初始化棋盘为空位}}// 检查当前放置是否会与已有皇后冲突var isValid func(row, col int) boolisValid = func(row, col int) bool {// 检查列是否有皇后for i := 0; i < row; i++ {if board[i][col] == "Q" {return false}}// 检查左上对角线是否有皇后for i, j := row-1, col-1; i >= 0 && j >= 0; i, j = i-1, j-1 {if board[i][j] == "Q" {return false}}// 检查右上对角线是否有皇后for i, j := row-1, col+1; i >= 0 && j < n; i, j = i-1, j+1 {if board[i][j] == "Q" {return false}}return true}// 将当前棋盘状态转换为字符串形式var formatBoard func() []stringformatBoard = func() []string {var result []stringfor _, row := range board {result = append(result, strings.Join(row, ""))}return result}// 回溯函数var backTrack func(row int)backTrack = func(row int) {if row == n {res = append(res, formatBoard()) // 所有行已放置完成,记录当前棋盘return}for col := 0; col < n; col++ {if !isValid(row, col) {continue}board[row][col] = "Q" // 放置皇后backTrack(row + 1)    // 递归到下一行board[row][col] = "." // 回溯,撤销放置}}backTrack(0) // 从第0行开始递归return res
}

详细解读

  1. 终止条件

    • 当所有行都放置皇后后(即row == n),将当前棋盘状态加入结果集。
  2. 结果收集条件

    • 在放置完所有行的皇后后,将当前棋盘状态board转换为字符串格式,并存入res
  3. 去重逻辑

    • 不存在重复,因为每次递归基于当前棋盘状态进行放置,确保唯一性。
  4. 递归控制

    • 每行递归尝试所有列的放置,通过isValid函数判断是否冲突,递归完成后回溯撤销放置。

13. LeetCode 37. 解数独

题目分析
给定一个部分填充的9x9数独,要求填充空白位置,使得每行、每列和每个3x3子区域均包含1到9的数字。数独谜题要求唯一解,因此需要在递归中保证填充的数字唯一合法。

思路解析
通过回溯逐个填充每一个空白位置,每个位置从1到9逐一尝试,检查当前数字是否符合数独规则,若符合则递归填充下一个空白格。填充完成后进行回溯,以探索其他可能解。

解数独的回溯流程图

开始解数独
选择空白格
尝试填入1-9
数字是否合法?
递归到下一个空白格
是否全部填充完成?
记录解法
回溯撤销

Go语言实现

func solveSudoku(board [][]byte) {var backTrack func() boolbackTrack = func() bool {for i := 0; i < 9; i++ {for j := 0; j < 9; j++ {// 跳过已填充的格子if board[i][j] != '.' {continue}// 尝试填充1到9for k := '1'; k <= '9'; k++ {if isValidSudoku(board, i, j, byte(k)) {board[i][j] = byte(k) // 填充数字if backTrack() {      // 递归处理下一空格return true}board[i][j] = '.' // 回溯,撤销填充}}return false // 若无合适数字则返回,回溯上层}}return true // 全部填充完成,返回true}backTrack()
}// 检查填充是否符合数独规则
func isValidSudoku(board [][]byte, row, col int, num byte) bool {for i := 0; i < 9; i++ {// 检查行和列是否有相同数字if board[row][i] == num || board[i][col] == num {return false}}// 检查3x3子区域是否有相同数字startRow, startCol := (row/3)*3, (col/3)*3for i := startRow; i < startRow+3; i++ {for j := startCol; j < startCol+3; j++ {if board[i][j] == num {return false}}}return true
}

详细解读

  1. 终止条件

    • 数独填充完所有空格时返回true,表示解数独成功。
  2. 结果收集条件

    • 当所有空格均填充有效数字时,返回true表示找到解。
  3. 去重逻辑

    • 每次填充数字前通过isValidSudoku检查是否有重复,确保合法。
  4. 递归控制

    • 递归顺序按行列填充,若当前格子无合适数字,则返回上层重新选择。

小结

棋盘问题的解法在回溯过程中充分利用了位置冲突检测和条件校验:

  1. 位置合法性检查:每次递归前进行条件校验,确保放置的元素(皇后或数字)符合问题规则。
  2. 递归路径的回溯:递归完成后通过回溯撤销选择,恢复状态以便尝试新的解法。
  3. 多层次条件判断:棋盘问题的递归逻辑中条件判断多,尤其是涉及行、列和对角线或3x3区域的冲突检测。

以上棋盘问题展示了如何在递归过程中处理复杂的棋盘状态和规则检测。接下来我们将继续分析其他复杂问题的解法与实现。


八、其他复杂问

这一类问题虽然不直接涉及棋盘或固定的数组结构,但仍可以使用回溯算法,通过递归探索不同的路径组合,满足特定的条件限制。以下分别是递增子序列和行程重排问题的分析与实现。


14. LeetCode 491. 递增子序列

题目分析
给定一个整数组nums,要求找到所有长度大于等于2的递增子序列,使得子序列中的元素按原数组中的顺序排列。结果集中不能有重复的子序列。

思路解析
递归生成子序列的每个元素时,确保新加入的元素满足递增要求,并且通过去重逻辑防止重复子序列的生成。具体来说,递归过程中会跳过当前层级的重复元素,同时用一个临时哈希表避免在同一层中选取相同的数字。

递增子序列生成的回溯流程图

开始生成递增子序列
选择一个元素加入序列
是否满足递增条件?
跳过,尝试下一个
递归到下一元素
是否到达序列末尾?
记录序列
回溯撤销选择

Go语言实现

func findSubsequences(nums []int) [][]int {var res [][]intvar path []intl := len(nums)var backTrack func(start int)backTrack = func(start int) {if len(path) >= 2 { // 只记录长度大于等于2的递增子序列temp := make([]int, len(path))copy(temp, path)res = append(res, temp)}used := make(map[int]bool) // 当前层级去重for i := start; i < l; i++ {// 检查递增性及去重条件if (len(path) > 0 && nums[i] < path[len(path)-1]) || used[nums[i]] {continue}used[nums[i]] = true          // 标记当前元素已使用path = append(path, nums[i])  // 选择当前元素backTrack(i + 1)              // 递归到下一个位置path = path[:len(path)-1]     // 撤销选择,回溯}}backTrack(0)return res
}

详细解读

  1. 终止条件

    • 没有固定终止条件,每次递归的路径path长度满足>=2时将路径加入结果集。
  2. 结果收集条件

    • 每次递归进入时判断路径长度,若大于等于2则将路径加入结果。
  3. 去重逻辑

    • 每一层递归中使用used哈希表记录当前层已选元素,避免重复选择。
  4. 递增性校验

    • 仅当当前元素大于等于路径末尾元素时才加入路径,确保子序列递增。

15. LeetCode 332. 重新安排行程

题目分析
给定一组行程票据,每张票据表示从某个机场出发,前往另一个机场。要求使用所有票据,按字典序返回从起点JFK开始的行程。

思路解析
先对票据进行字典序排序,然后通过回溯找到符合条件的行程路径。递归过程中每次选择一个目的地前往,若路径完整即为解。此外,在使用票据时需要标记票据是否被使用,确保每张票仅用一次。

重新安排行程的递归流程

起点:JFK
选择目的地按字典序
递归到下一目的地
是否所有票据使用完毕?
记录行程
回溯,恢复上次选择

Go语言实现

import "sort"func findItinerary(tickets [][]string) []string {var res []string// 构建邻接表,按字典序排序每个出发地的目的地flights := make(map[string][]string)for _, ticket := range tickets {from, to := ticket[0], ticket[1]flights[from] = append(flights[from], to)}for from := range flights {sort.Strings(flights[from])}// 回溯函数var backTrack func(airport string)backTrack = func(airport string) {for len(flights[airport]) > 0 {// 每次选择当前机场的第一个目的地,确保字典序next := flights[airport][0]flights[airport] = flights[airport][1:] // 从邻接表中移除已使用票backTrack(next)                         // 递归到下一目的地}res = append([]string{airport}, res...) // 将当前机场插入到行程的开头}backTrack("JFK")return res
}

详细解读

  1. 终止条件

    • 在所有票据用完时终止递归。路径构建完成后将递归路径res返回。
  2. 结果收集条件

    • 每次递归回溯时将当前机场插入到行程路径res开头,形成最终的行程顺序。
  3. 去重逻辑

    • 每次递归选择时,使用并移除当前目的地,避免重复使用票据。
  4. 字典序控制

    • 对每个出发地的目的地进行字典序排序,确保按字典序优先构建路径。

小结

在这一类复杂问题中,回溯算法需要更多的条件判断和路径控制来满足题目要求:

  1. 路径的唯一性和有效性:借助条件判断和状态记录,确保路径符合题目要求。
  2. 字典序或顺序控制:对于顺序敏感的问题,通过预排序或字典序遍历确保路径符合要求。
  3. 递归回溯与去重策略:有效的去重方法可以减少重复计算,尤其是在路径中存在重复元素时。

本章通过递增子序列和行程安排问题展示了回溯算法在复杂条件控制中的强大灵活性。

总结

在本文中,我们详细探讨了回溯算法在LeetCode上多类问题的应用。回溯算法通过递归与路径回退的组合,能够灵活地解决多种组合、分割、子集、排列、棋盘问题以及其他复杂场景的问题。对于每一道题目,我们不仅提供了具体的解题思路,还重点分析了回溯算法中的关键概念:终止条件、路径收集、去重逻辑和递归控制。

通过对每道题目的详细解读,本文总结出回溯算法在应对不同题型时的共性和差异性。对于组合和子集问题,回溯算法的主要任务是控制元素的选择顺序;在排列问题中,算法必须关注排列的顺序性;在棋盘问题中,递归条件更加复杂,需要多层次冲突检测;而在复杂问题中,算法往往需动态地控制路径合法性和顺序。

希望通过这篇文章,读者能够更加系统地理解回溯算法在不同问题类型中的应用,掌握其在路径选择、条件判断、状态回退等方面的通用模式,并能够在遇到类似问题时举一反三,设计出高效的回溯解法。


关注我

相关文章:

【力扣】Go语言回溯算法详细实现与方法论提炼

文章目录 一、引言二、回溯算法的核心概念三、组合问题1. LeetCode 77. 组合2. LeetCode 216. 组合总和III3. LeetCode 17. 电话号码的字母组合4. LeetCode 39. 组合总和5. LeetCode 40. 组合总和 II小结 四、分割问题6. LeetCode 131. 分割回文串7. LeetCode 93. 复原IP地址小…...

「C/C++」C/C++ 之 第三方库使用规范

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「C/C」C/C程序设计&#x1f4da;全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasoli…...

六、元素应用CSS的习题

题目一&#xff1a; 使用CSS样式对页面元素加以修饰&#xff0c;制作“ 旅游攻略 ”网站。如下图所示 运行效果&#xff1a; 代码&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>旅游攻略</title><…...

正式入驻!上海斯歌BPM PaaS管理软件等产品入选华为云联营商品

近日&#xff0c;上海斯歌旗下BPM PaaS管理软件&#xff08;NBS&#xff09;等多款产品入选华为云云商店联营商品&#xff0c;上海斯歌正式成为华为云联营商品合作伙伴。用户登录华为云云商店即可采购上海斯歌的BPM PaaS产品及配套服务。通过联营模式&#xff0c;双方合作能够深…...

使用 Axios 上传大文件分片上传

背景 在上传大文件时&#xff0c;分片上传是一种常见且有效的策略。由于大文件在上传过程中可能会遇到内存溢出、网络不稳定等问题&#xff0c;分片上传可以显著提高上传的可靠性和效率。通过将大文件分割成多个小分片&#xff0c;不仅可以减少单次上传的数据量&#xff0c;降…...

Nginx+Lua脚本+Redis 实现自动封禁访问频率过高IP

1 、安装OpenResty 安装使用 OpenResty&#xff0c;这是一个集成了各种 Lua 模块的 Nginx 服务器&#xff0c;是一个以Nginx为核心同时包含很多第三方模块的Web应用服务器&#xff0c;使用Nginx的同时又能使用lua等模块实现复杂的控制。 &#xff08;1&#xff09;安装编译工具…...

PART 1 数据挖掘概论 — 数据挖掘方法论

目录 数据库知识发掘步骤 数据挖掘技术的产业标准 CRISP-DM SEMMA 数据库知识发掘步骤 数据库知识发掘(Knowledge Discovery in Database,KDD)是从数据库中的大量数据中发现不明显、之前未知、可能有用的知识。 知识发掘流程(Knowledge Discovery Process)包括属性选择…...

Centos安装ffmpeg的方法

推荐第一个,不要自己编译安装,太难了,坑多。 在 CentOS 上安装 FFmpeg 有几种方法,以下是两种常见的方法: ### 方法一:使用 RPM Fusion 仓库安装 1. **启用 RPM Fusion 仓库**: RPM Fusion 是一个第三方仓库,提供了许多 CentOS 官方仓库中没有的软件包。 ```bash…...

理解SQL中通配符的使用

前言 SQL 是一种标准化的结构化查询语言&#xff0c;涉及结构化查询时&#xff0c;高效地检索数据至关重要。而通配符是SQL中模式匹配的有效的方法。使用通配符可以更轻松地检索到所需的确切数据。通配符允许我们定义多功能查询条件。本文将 介绍SQL通配符的基础知识及用法。 …...

SpringBoot篇(简化操作的原理)

目录 一、代码位置 二、统一版本管理&#xff08;parent&#xff09; 三、提供 starter简化 Maven 配置 四、自动配置 Spring&#xff08;引导类&#xff09; 五、嵌入式 servlet 容器 一、代码位置 二、统一版本管理&#xff08;parent&#xff09; SpringBoot项目都会继…...

Cesium的模型(ModelVS)顶点着色器浅析

来自glTF和3D Tiles的模型会走ModelVS.glsl。这个文件不单独是把模型顶点转换为屏幕坐标&#xff0c;还包含了丰富的处理过程。 Cesium是根据定义的Define判断某个行为是否需要被执行&#xff0c;比如#define HAS_SILHOUETTE&#xff0c;说明需要计算模型外轮廓线。 Cesium的…...

机器人领域中的scaling law:通过复现斯坦福机器人UMI——探讨数据规模化定律(含UMI的复现关键)

前言 在24年10.26/10.27两天&#xff0c;我司七月在线举办的七月大模型机器人线下营时&#xff0c;我们带着大家一步步复现UMI&#xff0c;比如把杯子摆到杯盘上(其中1-2位学员朋友还亲自自身成功做到该任务) 此外&#xff0c;我还特地邀请了针对UMI做了改进工作的fastumi作者…...

C++之多态的深度剖析

目录 前言 1.多态的概念 2.多态的定义及实现 2.1多态的构成条件 2.1.1重要条件 2.1.2 虚函数 2.1.3 虚函数的重写/覆盖 2.1.4 选择题 2.1.5 虚函数其他知识 协变&#xff08;了解&#xff09; 析构函数的重写 override 和 final关键字 3. 重载&#xff0c;重写&…...

Microsoft Office PowerPoint制作科研论文用图

Microsoft Office PowerPoint制作科研论文用图 1. 获取高清图片2. 导入PPT3. 另存为“增强型windows元文件”emf格式4. 画图剪裁 1. 获取高清图片 这里指通过绘图软件画分辨率高的图片&#xff0c;我一般使用python画dpi600的图片。 2. 导入PPT 新建一个PPT&#xff08;注意&a…...

go语言进阶之并发基础

并发 什么是并发&#xff0c;也就是我们常说的多线程&#xff0c;多个程序同时执行。 并发的基础 线程和进程 进程 进程是操作系统中一个重要的概念&#xff0c;指的是一个正在运行的程序的实例。它包含程序代码、当前活动的状态、变量、程序计数器和内存等资源。进程是系…...

po、dto、vo的使用场景

现在项目中有两类模型类&#xff1a;DTO数据传输对象、PO持久化对象&#xff0c;DTO用于接口层向业务层之间传输数据&#xff0c;PO用于业务层与持久层之间传输数据&#xff0c;有些项目还会设置VO对象&#xff0c;VO对象用在前端与接口层之间传输数据&#xff0c;如下图&#…...

聊一聊Elasticsearch的一些基本信息

一、Elasticsearch是什么 Elasticsearch简称ES&#xff0c;是一款分布式搜索引擎。它是在Apache Lucene基础之上采用Java语言开发的。 Elasticsearch的官方网站对它的解释是&#xff1a;Elasticsearch是一个分布式、RESTful的搜索和数据分析引擎。 通过上边的官方解释&#…...

Unity 两篇文章熟悉所有编辑器拓展关键类 (上)

本专栏基础资源来自唐老狮和siki学院&#xff0c;仅作学习交流使用&#xff0c;不作任何商业用途&#xff0c;吃水不忘打井人&#xff0c;谨遵教诲 编辑器扩展内容实在是太多太多了&#xff08;本篇就有五千字&#xff09; 所以分为两个篇章而且只用一些常用api举例&#xff0c…...

Spring SPI、Solon SPI 有点儿像(Maven 与 Gradle)

一、什么是 SPI SPI 全名 Service Provider interface&#xff0c;翻译过来就是“服务提供接口”。基本效果是&#xff0c;申明一个接口&#xff0c;然后通过配置获取它的实现&#xff0c;进而实现动态扩展。 Java SPI 是 JDK 内置的一种动态加载扩展点的实现。 一般的业务代…...

合并排序算法(C语言版)

#include <stdio.h> void Copy(int *a, int *b, int left, int right) { int i; for(i0;i<right-left1;i) { a[ileft] b[i]; } } // 将 a[left,middle] 和 a[middle1,right]合并到 b[left, right]中 void Merge(int *a, int left, int midd…...

CMake 从 GitHub 下载第三方库并使用

有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...

人机融合智能 | “人智交互”跨学科新领域

本文系统地提出基于“以人为中心AI(HCAI)”理念的人-人工智能交互(人智交互)这一跨学科新领域及框架,定义人智交互领域的理念、基本理论和关键问题、方法、开发流程和参与团队等,阐述提出人智交互新领域的意义。然后,提出人智交互研究的三种新范式取向以及它们的意义。最后,总结…...

Caliper 配置文件解析:fisco-bcos.json

config.yaml 文件 config.yaml 是 Caliper 的主配置文件,通常包含以下内容: test:name: fisco-bcos-test # 测试名称description: Performance test of FISCO-BCOS # 测试描述workers:type: local # 工作进程类型number: 5 # 工作进程数量monitor:type: - docker- pro…...

Python 训练营打卡 Day 47

注意力热力图可视化 在day 46代码的基础上&#xff0c;对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...

基于鸿蒙(HarmonyOS5)的打车小程序

1. 开发环境准备 安装DevEco Studio (鸿蒙官方IDE)配置HarmonyOS SDK申请开发者账号和必要的API密钥 2. 项目结构设计 ├── entry │ ├── src │ │ ├── main │ │ │ ├── ets │ │ │ │ ├── pages │ │ │ │ │ ├── H…...

用 FFmpeg 实现 RTMP 推流直播

RTMP&#xff08;Real-Time Messaging Protocol&#xff09; 是直播行业中常用的传输协议。 一般来说&#xff0c;直播服务商会给你&#xff1a; ✅ 一个 RTMP 推流地址&#xff08;你推视频上去&#xff09; ✅ 一个 HLS 或 FLV 拉流地址&#xff08;观众观看用&#xff09;…...

免费批量Markdown转Word工具

免费批量Markdown转Word工具 一款简单易用的批量Markdown文档转换工具&#xff0c;支持将多个Markdown文件一键转换为Word文档。完全免费&#xff0c;无需安装&#xff0c;解压即用&#xff01; 官方网站 访问官方展示页面了解更多信息&#xff1a;http://mutou888.com/pro…...

Ubuntu 可执行程序自启动方法

使用 autostart&#xff08;适用于桌面环境&#xff09; 适用于 GNOME/KDE 桌面环境&#xff08;如 Ubuntu 图形界面&#xff09; 1. 创建 .desktop 文件 sudo vi ~/.config/autostart/my_laser.desktop[Desktop Entry] TypeApplication NameMy Laser Program Execbash -c &…...

JS面试常见问题——数据类型篇

这几周在进行系统的复习&#xff0c;这一篇来说一下自己复习的JS数据结构的常见面试题中比较重要的一部分 文章目录 一、JavaScript有哪些数据类型二、数据类型检测的方法1. typeof2. instanceof3. constructor4. Object.prototype.toString.call()5. type null会被判断为Obje…...

2025-06-08-深度学习网络介绍(语义分割,实例分割,目标检测)

深度学习网络介绍(语义分割,实例分割,目标检测) 前言 在开始这篇文章之前&#xff0c;我们得首先弄明白&#xff0c;什么是图像分割&#xff1f; 我们知道一个图像只不过是许多像素的集合。图像分割分类是对图像中属于特定类别的像素进行分类的过程&#xff0c;即像素级别的…...