回溯法

背景

回溯法(backtrack)常用于遍历列表所有子集,是 DFS 深度搜索一种,一般用于全排列,穷尽所有可能,遍历的过程实际上是一个决策树的遍历过程。时间复杂度一般 O(N!),它不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。

模板

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

核心就是从选择列表里做一个选择,然后一直递归往下搜索答案,如果遇到路径不通,就返回来撤销这次选择。

示例

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

遍历过程

class Solution {
  List<List<int>> subsets(List<int> nums) {
    List<List<int>> result = []; // 存储最终结果的列表
    List<int> temp = []; // 存储当前子集的临时列表
    backtrack(nums, 0, temp, result); // 调用回溯函数生成所有子集
    return result; // 返回最终结果
  }

  void backtrack(
    List<int> nums, // 原始数组
    int index, // 当前遍历的索引
    List<int> temp, // 当前子集的临时列表
    List<List<int>> result, // 存储最终结果的列表
  ) {
    result.add([...temp]); // 将当前子集的拷贝添加到最终结果列表中

    for (int i = index; i < nums.length; i++) {
      temp.add(nums[i]); // 将当前元素添加到临时列表中

      // 递归调用回溯函数,遍历剩余元素
      backtrack(nums, i + 1, temp, result);

      temp.removeLast(); // 移除最后一个元素,回溯到上一层状态
    }
  }
}

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。说明:解集不能包含重复的子集。

class Solution {
  List<List<int>> subsetsWithDup(List<int> nums) {
    List<List<int>> result = []; // 存储最终结果的列表
    List<int> temp = []; // 存储当前子集的临时列表
    nums.sort(); // 对原始数组进行排序,以便处理重复元素
    backtrack(nums, 0, temp, result); // 调用回溯函数生成所有子集
    return result; // 返回最终结果
  }

  void backtrack(
    List<int> nums, // 给定的集合
    int index, // 下次添加到集合中的元素位置索引
    List<int> temp, // 临时结果集合(每次需要复制保存)
    List<List<int>> result, // 最终结果
  ) {
    result.add([...temp]); // 将当前子集的拷贝添加到最终结果列表中

    // 选择时需要剪枝、处理、撤销选择
    for (int i = index; i < nums.length; i++) {
      // 剪枝条件:跳过重复元素,但保留第一次出现的元素
      if (i > 0 && nums[i] == nums[i - 1] && i != index) continue;

      temp.add(nums[i]); // 将当前元素添加到临时列表中

      // 递归调用回溯函数,遍历剩余元素
      backtrack(nums, i + 1, temp, result);

      temp.removeLast(); // 移除最后一个元素,回溯到上一层状态
    }
  }
}

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

思路:需要记录已经选择过的元素,满足条件的结果才进行返回

class Solution {
  List<List<int>> permute(List<int> nums) {
    List<List<int>> res = []; // 存储所有排列结果
    List<int> temp = []; // 存储当前排列结果
    Set<int> used = Set(); // 存储已经使用过的数字

    // 递归函数,用来生成所有的排列结果
    void backtrack() {
      // 如果当前排列结果的长度等于nums的长度,则将其加入res中
      if (temp.length == nums.length) {
        res.add([...temp]); // 注意要将temp的拷贝加入res中,否则会影响后续的操作
        return;
      }

      // 遍历nums数组,尝试将每个数字加入排列中
      for (int i = 0; i < nums.length; i++) {
        if (!used.contains(nums[i])) {
          temp.add(nums[i]); // 将当前数字加入排列中
          used.add(nums[i]); // 将当前数字标记为已经使用过
          backtrack(); // 递归,继续生成下一个数字的排列结果
          temp.removeLast(); // 回溯,将当前数字从排列中移除
          used.remove(nums[i]); // 回溯,将当前数字标记为未使用过
        }
      }
    }

    // 调用递归函数,生成所有的排列结果
    backtrack();

    return res;
  }
}

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

class Solution {
  List<List<int>> permuteUnique(List<int> nums) {
    List<bool> isVisited = List<bool>.filled(nums.length, false);
      List<List<int>> result = [];
      List<int> temp = [];
      nums.sort();
      backtrack(nums,isVisited,temp,result);
      return result;
  }
	// nums 输入集合
	// visited 当前递归标记过的元素
	// list 临时结果集(路径)
	// result 最终结果
  void backtrack(List<int> nums,List<bool> isVisited,List<int> temp,List<List<int>> result){
	  // 返回条件:临时结果和输入集合长度一致 才是全排列
      if(temp.length == nums.length){
          result.add([...temp]);
          return;
      }
      for(int i = 0;i < nums.length;i++){
		// 已经添加过的元素,直接跳过
          if(isVisited[i]) continue;
		  if(i > 0 && nums[i-1] ==nums[i] && !isVisited[i-1]) continue;
          temp.add(nums[i]);
          isVisited[i] = true;
          backtrack(nums,isVisited,temp,result);
          isVisited[i] = false;
          temp.removeLast();
      }
  }
}

练习

挑战题目

Last updated