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

零基础数据结构与算法——第五章:高级算法-回溯算法子集全排列问题

5.3.2 经典回溯算法问题

1. N皇后问题
2. 子集问题

问题描述

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

生活例子

想象你是一名餐厅经理,有一份包含多种食材的菜单(如牛肉、鸡肉、蔬菜、米饭)。顾客可以选择任意组合的食材来定制自己的餐点。你需要列出所有可能的组合选择,包括不选任何食材(空盘子)、只选一种食材、选两种食材的组合,一直到选择所有食材的组合。

问题分析

对于一个长度为n的数组,其子集的数量为2^n个(包括空集)。这是因为对于每个元素,我们有两种选择:选或不选。

例如,对于数组[1,2,3]:

  • 空集:[]
  • 只包含一个元素的子集:[1], [2], [3]
  • 包含两个元素的子集:[1,2], [1,3], [2,3]
  • 包含三个元素的子集:[1,2,3]

回溯策略

  1. 从空集开始,逐步考虑每个元素是否加入当前子集
  2. 对于每个元素,我们有两个选择:加入当前子集或不加入
  3. 每次做出选择后,递归处理剩余元素
  4. 当考虑完所有元素后,将当前子集加入结果集

图解过程(以[1,2,3]为例):

                    []/  \/    \/      \[1]        []/  \       /  \/    \     /    \[1,2]  [1]  [2]    []/  \    / \  / \    / \[1,2,3][1,2][1,3][1][2,3][2][3][]

这个树形结构展示了回溯过程中的所有状态。每个节点表示一个子集,从根节点(空集)开始,每一层考虑一个元素是否加入。最终,所有叶子节点构成了所有可能的子集。

代码实现

public static List<List<Integer>> subsets(int[] nums) {List<List<Integer>> result = new ArrayList<>();// 从空集开始,逐步构建所有子集backtrack(nums, 0, new ArrayList<>(), result);return result;
}private static void backtrack(int[] nums, int start, List<Integer> current, List<List<Integer>> result) {// 将当前子集添加到结果中(每个状态都是一个有效的子集)result.add(new ArrayList<>(current));// 从start开始考虑每个元素,避免重复生成子集for (int i = start; i < nums.length; i++) {// 选择:将当前元素添加到子集中current.add(nums[i]);// 递归:处理剩余元素(只考虑当前元素之后的元素,避免重复)backtrack(nums, i + 1, current, result);// 回溯:将当前元素从子集中移除,尝试其他选择current.remove(current.size() - 1);}
}// 打印所有子集(用于调试)
private static void printSubsets(List<List<Integer>> subsets) {System.out.println("所有子集:");for (List<Integer> subset : subsets) {System.out.println(subset);}System.out.println("共 " + subsets.size() + " 个子集");
}
3. 全排列问题

问题描述

给定一个不含重复数字的数组nums,返回其所有可能的全排列。

生活例子

想象你是一名活动策划人,需要安排5位嘉宾的座次。每个座位只能坐一个人,每个人必须有座位。你需要列出所有可能的座位安排方式,以便选择最合适的一种。

问题分析

对于长度为n的数组,其全排列的数量为n!(n的阶乘)。这是因为:

  • 第一个位置有n种选择
  • 第二个位置有n-1种选择
  • 第三个位置有n-2种选择
  • 以此类推

例如,对于数组[1,2,3],其全排列有:
[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1],共6=3!种。

回溯策略

  1. 从空排列开始,逐步填入数字
  2. 对于每个位置,尝试填入尚未使用的每个数字
  3. 填入一个数字后,递归处理下一个位置
  4. 当所有位置都填满后,将当前排列加入结果集
  5. 回溯时,移除最后填入的数字,尝试其他可能

图解过程(以[1,2,3]为例):

                      []/  |  \/   |   \/    |    \[1]   [2]   [3]/  \   / \   / \/    \ /   \ /   \[1,2] [1,3] [2,1] [2,3] [3,1] [3,2]|      |     |     |     |     |[1,2,3] [1,3,2] [2,1,3] [2,3,1] [3,1,2] [3,2,1]

这个树形结构展示了回溯过程中的所有状态。从根节点(空排列)开始,每一层考虑一个位置应该填入哪个数字。最终,所有叶子节点构成了所有可能的全排列。

代码实现

public static List<List<Integer>> permute(int[] nums) {List<List<Integer>> result = new ArrayList<>();backtrack(nums, new ArrayList<>(), result);return result;
}private static void backtrack(int[] nums, List<Integer> current, List<List<Integer>> result) {// 如果当前排列的长度等于数组长度,说明找到了一个完整排列if (current.size() == nums.length) {// 将当前排列添加到结果中result.add(new ArrayList<>(current));return;}// 尝试在当前位置放置每个还未使用的数字for (int i = 0; i < nums.length; i++) {// 跳过已经使用的元素if (current.contains(nums[i])) continue;// 选择:将当前元素添加到排列中current.add(nums[i]);// 递归:处理下一个位置backtrack(nums, current, result);// 回溯:将当前元素从排列中移除,尝试其他选择current.remove(current.size() - 1);}
}// 优化版本:使用布尔数组标记元素是否已使用,避免重复检查
public static List<List<Integer>> permuteOptimized(int[] nums) {List<List<Integer>> result = new ArrayList<>();boolean[] used = new boolean[nums.length];backtrackOptimized(nums, new ArrayList<>(), used, result);return result;
}private static void backtrackOptimized(int[] nums, List<Integer> current, boolean[] used, List<List<Integer>> result) {if (current.size() == nums.length) {result.add(new ArrayList<>(current));return;}for (int i = 0; i < nums.length; i++) {// 使用布尔数组检查元素是否已使用,比contains()更高效if (used[i]) continue;current.add(nums[i]);used[i] = true;backtrackOptimized(nums, current, used, result);current.remove(current.size() - 1);used[i] = false;}
}// 打印所有排列(用于调试)
private static void printPermutations(List<List<Integer>> permutations) {System.out.println("所有排列:");for (List<Integer> perm : permutations) {System.out.println(perm);}System.out.println("共 " + permutations.size() + " 个排列");
}
http://www.dtcms.com/a/292513.html

相关文章:

  • ZooKeeper学习专栏(六):集群模式部署与解析
  • C++ new 创建数组的内在原理详解
  • linux 环境服务发生文件句柄泄漏导致服务不可用
  • ELF 文件操作手册
  • python学习-读取csv文件
  • 如何验证分类模型输出概率P值的“好坏”:评估与校准示例
  • GitHub 上的开源项目 ticktick(滴答清单)
  • recvmsg函数的用法
  • 算法学习--滑动窗口
  • 学习python中离线安装pip及下载package的方法
  • C语言:函数基础
  • day059-zabbix自定义监控与自动发现
  • Node.js:Web模块、Express框架
  • es6中的symbol基础知识
  • 在Android开发中,如何获取到手机设备的PIN码?
  • 如何安装CMake较新的版本
  • Apache Ignite 长事务终止机制
  • 精密全波整流电路(一)
  • torchvision.transforms 与 MONAI 数据增强的异同
  • Cloud 与 VPS 的区别:如何选择最适合你的服务器解决方案?
  • stream流入门
  • 【打怪升级 - 01】保姆级机器视觉入门指南:硬件选型 + CUDA/cuDNN/Miniconda/PyTorch 安装全流程(附版本匹配秘籍)
  • vmware 克隆虚拟机,报错:克隆时出错:指定不存在的设备。然后电脑卡死,只能强制关机再开机。
  • FastDFS 6.11.0 单机环境搭建与测试(附 Nginx 集成)+ docker构建+k8s启动文件
  • 用org.apache.pdfbox 转换 PDF 到 图片格式
  • KafkaMQ 日志采集最佳实践
  • Python 正则表达式:入门到实战
  • 日常随笔-React摘要
  • 【ROS/DDS】FastDDS :编写FastDDS程序实现与ROS2 通讯(四)
  • 深入浅出理解 TCP 与 UDP:网络传输协议的核心差异与应用