Day25| 491.递增子序列、46.全排列、47.全排列 II、回溯总结
文章链接
491.递增子序列
哈希去重:用 HashSet
保证:每一层只尝试一个值。
剪枝逻辑:
-
如果当前
nums[i]
比 path 中最后一个元素小,说明不是递增序列,跳过; -
如果
nums[i]
已经在当前 for 循环层尝试过了,也跳过(避免重复分支)。
回溯核心思想是:从左到右选择一个数,然后递归往后选更多数,形成子序列。
-
对于每一个位置
i
,有两种选择: -
选:将
nums[i]
加入 path; -
不选:跳过
nums[i]
,进入下一个元素; -
递归时带一个
startIndex
,保证从左到右、不回头。
public class Solution {public IList<IList<int>> res=new List<IList<int>>();public IList<int> path=new List<int>();public IList<IList<int>> FindSubsequences(int[] nums) {BackTracking(nums,0);return res;}public void BackTracking(int[] nums,int startIndex){if(path.Count>=2){res.Add(new List<int>(path));}HashSet<int> hs=new HashSet<int>();for(int i=startIndex;i<nums.Length;i++){if(path.Count>0&&path[path.Count-1]>nums[i]||hs.Contains(nums[i])){continue;}hs.Add(nums[i]);path.Add(nums[i]);BackTracking(nums,i+1);path.RemoveAt(path.Count-1);}}
}
46.全排列
步骤 | 内容 |
回溯结构 | 逐个选取数字,构造出一个完整路径 |
状态表示 | path 保存当前路径,used[] 表示哪些数被选了 |
终止条件 | 当 path.Count == nums.Length,加入结果集 |
剪枝 | 如果 used[i] == true,跳过当前数字 |
回溯 | 回到上一步时撤销选择(used[i] = false,RemoveAt) |
1️⃣ 使用回溯法(Backtracking)
-
回溯法用于逐步构造解空间,尝试所有可能路径。
-
每个位置都尝试一个未使用过的数字,直到构成一个完整排列。
2️⃣ 路径构建
-
使用
List<int> path
表示当前正在构造的排列路径。 -
使用
bool[] used
数组标记哪些数字已经被使用,避免重复。
3️⃣ 终止条件
- 当
path.Count == nums.Length
时,说明已经选满一个排列,将其加入结果集。
4️⃣ 遍历选择列表
-
遍历
nums
中每一个数字: -
若未被使用,则加入 path;
-
递归处理下一层;
-
回溯时撤销选择(移除 path 最后一个元素,并将
used[i] = false
)。
5️⃣ 回溯关键点
-
做选择 → 递归 → 撤销选择(回溯)
-
依赖于系统栈自动回溯到上一层状态
public class Solution {public IList<IList<int>> res=new List<IList<int>>();public IList<int> path=new List<int>();public IList<IList<int>> Permute(int[] nums) {var used=new bool[nums.Length] ;BackTracking(nums,used);return res;}public void BackTracking(int[] nums,bool[] used){if(path.Count==nums.Length){res.Add(new List<int>(path));return;}for(int i=0;i<nums.Length;i++){if(used[i]) continue;used[i]=true;path.Add(nums[i]);BackTracking(nums,used);used[i]=false;path.RemoveAt(path.Count-1);}}}
47.全排列 II
思路:
-
排序。排序的目的是为了在后面判断重复时能够比较相邻元素,从而避免生成重复排列。
-
used数组。记录某个元素是否在当前排列中已经被使用,防止重复使用同一个索引的元素。
-
剪枝去重条件
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;
这句代码是关键的去重操作,意思是:
-
如果当前数字
nums[i]
和前一个数字nums[i - 1]
相同, -
并且
nums[i - 1]
在当前递归层级没有被使用(即还没进入 path), -
那么跳过这个数字,避免重复排列。
public class Solution {public IList<IList<int>> res=new List<IList<int>>();public IList<int> path=new List<int>();public IList<IList<int>> PermuteUnique(int[] nums) {Array.Sort(nums);var used=new bool[nums.Length];BackTracking(nums,used);return res;}public void BackTracking(int[] nums,bool[] used){if(nums.Length==path.Count){res.Add(new List<int>(path));return;}for(int i=0;i<nums.Length;i++){if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;if(used[i]) continue;path.Add(nums[i]);used[i]=true;BackTracking(nums,used);used[i]=false;path.RemoveAt(path.Count-1);}}
}
回溯总结
- 回溯算法能解决如下问题:
-
组合问题:N个数里面按一定规则找出k个数的集合
-
排列问题:N个数按一定规则全排列,有几种排列方式
-
切割问题:一个字符串按一定规则有几种切割方式
-
子集问题:一个N个数的集合里有多少符合条件的子集
-
棋盘问题:N皇后,解数独等等
- 回溯模板
void backtracking(参数) {if (终止条件) {存放结果;return;}for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {处理节点;backtracking(路径,选择列表); // 递归回溯,撤销处理结果} }
-
回溯问题可以抽象为树形结构
-
for循环横向遍历,递归纵向遍历,回溯不断调整结果集
-
剪枝操作:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了。
-
startIndex:如果是一个集合来求组合的话,就需要startIndex;如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
-
去重:树枝去重/树层去重