首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

掌握回溯算法(Backtracking):从 LeetCode 到实际应用

  • 24-12-16 16:07
  • 2110
  • 343856
juejin.cn

回溯算法是计算机科学中最强大的技术之一,专门用于系统地探索多种可能性以解决问题。从解决谜题到生成排列组合,回溯算法在竞赛编程和实际应用中都不可或缺。在这篇博客中,我们将深入了解回溯算法的核心原理,探讨其递归和迭代的实现,并通过经典的 LeetCode 问题举例分析其实际应用。


什么是回溯算法?

回溯是一种问题求解技术,它通过逐步构建候选解的方式来探索所有可能的解决方案,并在确定当前候选解无法导向有效解时立刻放弃该路径。回溯算法通过以下模式系统地探索所有可能性:

  1. 探索:选择一条路径或做出一个决策。
  2. 撤销(回溯) :撤回上一步的决策。
  3. 继续探索:尝试另一条路径或决策。

回溯算法常用于解决以下类型的问题:

  • 生成排列或组合。
  • 解决数独等谜题。
  • 在图或网格中寻找路径。

回溯算法通过剪枝那些无法导向有效解的路径,简化了需要穷举的问题。尽管回溯算法未必总是最高效的解决方案,但它通常是生成所有可能性的最简单方法,这使得它在实际问题中非常实用。


LeetCode 回溯算法题目

LeetCode 46 - 全排列

我们从经典的 全排列 问题开始。给定一个由不同整数组成的数组,我们需要返回所有可能的排列。

csharp
代码解读
复制代码
public class Solution { public IListint>> Permute(int[] nums) { var ans = new Listint>>(); Backtrack(nums, new List<int>(), new HashSet<int>(), ans); return ans; } private void Backtrack(int[] nums, List<int> current, HashSet<int> used, Listint>> ans) { // 基本情况:如果当前排列已完成 if (current.Count == nums.Length) { ans.Add(new List<int>(current)); return; } // 探索所有可能性 for (int i = 0; i < nums.Length; i++) { if (used.Contains(nums[i])) continue; // 跳过已使用的数字 // 选择 current.Add(nums[i]); used.Add(nums[i]); // 继续探索 Backtrack(nums, current, used, ans); // 撤销选择(回溯) current.RemoveAt(current.Count - 1); used.Remove(nums[i]); } } }

关键点

  • 探索后撤销选择:一旦探索完一条路径,我们撤销上一步的决策以尝试其他路径。
  • 递归树:递归的每一层对应于当前排列中选择下一个数字的操作。

LeetCode 39 - 组合总和

给定一个由正整数组成的数组(candidates)和一个目标值(target),我们需要找到所有唯一的组合,使得组合中的数字之和等于目标值。

csharp
代码解读
复制代码
public class Solution { public IListint>> CombinationSum(int[] candidates, int target) { var ans = new Listint>>(); Backtrack(candidates, target, 0, new List<int>(), ans); return ans; } private void Backtrack(int[] candidates, int target, int start, List<int> current, Listint>> ans) { if (target == 0) { ans.Add(new List<int>(current)); return; } for (int i = start; i < candidates.Length; i++) { if (candidates[i] > target) continue; // 选择 current.Add(candidates[i]); // 继续探索 Backtrack(candidates, target - candidates[i], i, current, ans); // 撤销选择(回溯) current.RemoveAt(current.Count - 1); } } }

迭代回溯方法与DFS 比较:

与递归方法相比,我们可以使用显式栈来管理状态:

csharp
代码解读
复制代码
// LeetCode 46 public class Solution { public IListint>> Permute(int[] nums) { var ans = new Listint>>(); var stack = new Stack<(List<int> current, HashSet<int> used)>(); stack.Push((new List<int>(), new HashSet<int>())); while (stack.Count > 0) { var (current, used) = stack.Pop(); if (current.Count == nums.Length) { ans.Add(new List<int>(current)); continue; } foreach (var num in nums) { if (used.Contains(num)) continue; var nextCurrent = new List<int>(current) { num }; var nextUsed = new HashSet<int>(used) { num }; stack.Push((nextCurrent, nextUsed)); } } return ans; } }
csharp
代码解读
复制代码
// LeetCode 39 public class Solution { public IListint>> CombinationSum(int[] candidates, int target) { var ans = new Listint>>(); var stack = new Stack<(List<int> current, int remaining, int index)>(); stack.Push((new List<int>(), target, 0)); while (stack.Count > 0) { var (current, remaining, index) = stack.Pop(); if (remaining == 0) { ans.Add(new List<int>(current)); continue; } for (int i = index; i < candidates.Length; i++) { if (candidates[i] > remaining) continue; var nextCurrent = new List<int>(current) { candidates[i] }; stack.Push((nextCurrent, remaining - candidates[i], i)); } } return ans; } }

无论是递归还是迭代,回溯的核心思想都是一致的:通过 推进(探索) 和 回退(撤销) 系统地访问所有解决方案。

通过对比递归和迭代实现,我们可以发现回溯与深度优先搜索(DFS)原则紧密相连。两种方法都依赖于维护状态信息来追踪进度。在递归回溯中,这些状态由函数调用栈隐式管理;而在迭代版本中,需要显式的数据结构(例如Stack或Queue)来模拟这种行为。

迭代方法的优势

  1. 控制调用栈:
    在递归中,调用栈的深度与递归树的深度相关,如果递归过深可能会导致栈溢出。迭代回溯通过显式栈避免了这一问题,并对大型问题空间提供了更强的控制和可扩展性。
  2. 可自定义的状态管理:
    使用显式栈时,你可以在每个状态中存储额外的信息(如元数据或历史记录),而无需依赖全局变量或函数参数。
  3. 更易调试:
    迭代实现的所有操作都是显式的,状态转换更加清晰,这使得调试比递归方法更容易,因为递归中的“隐式”状态更难追踪。

迭代方法的缺点

  1. 代码冗长:
    迭代解决方案通常需要更多的样板代码来管理状态,比如栈的推入和弹出,以及处理辅助数据结构(例如visited数组或哈希集)。
  2. 直观性较低:
    递归解决方案更符合回溯的概念模型,其中每个递归调用代表一个新的探索层次。对于初学者来说,迭代解决方案可能显得不够直观。

实际考量

  • 在以下情况下选择递归回溯:

    • 问题的深度可控,调用栈可以承受递归深度。
    • 简洁和可读性优先。递归解决方案通常更简洁、更易理解。
  • 在以下情况下选择迭代回溯:

    • 递归深度可能超过栈限制,导致栈溢出错误。
    • 需要对状态进行精细控制,比如跟踪附加数据。
    • 问题涉及动态约束或需要频繁修改状态。

在实际问题中,当需要更强的控制或递归深度成为问题时(例如解决大型图问题),迭代方法可能更实用。


更多 LeetCode 练习题

以下是一些可以巩固回溯理解的 LeetCode 题目:

  • 78. 子集
  • 79. 单词搜索
  • 90. 子集 II
  • 131. 分割回文串
  • 51. N 皇后问题
  • 52. N 皇后 II

结论

回溯是一种优雅且系统化的求解探索和决策问题的方法。尽管递归是实现回溯的最直观方式,但迭代方法提供了宝贵的见解和替代方案。掌握这种技术不仅有助于解决 LeetCode 问题,还能应用于现实场景中的问题,如约束满足问题和路径查找问题。

通过练习,你将逐渐培养识别回溯问题的直觉,并实现高效的解决方案——这是一项每个程序员都必须掌握的重要技能。

注:本文转载自juejin.cn的一只拉古的文章"https://juejin.cn/post/7448571674030014518"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

103
后端
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top