目录
47.全排列 II
题目描述
解题思路
1. 减枝情况:需要考虑到如果这个序列存在重复数字,则可能出现两个[1,1,2]重复序列的情况【因为其中一个序列是第一个1在前,另一个序列是第二个1在前,代码原以为不重复】
2. 需要先把重复的元素通过排序放在相邻的位置这样有利于后序树的遍历
3. 树枝上(不同层纵向)是单次路径按照元素是否出现过的排列情况,树层上(同层横向)是按照位置索引的排列情况
4. 叶子节点是要返回的值
引用自代码随想录对应算法题的图来辅助理解
代码
全局变量:
path数组是单次路径的集合;【LinkedList】
用一个数组used来标记使得不能用重复的元素【用1和0代表是否用过:例如用的是元素2则对应的used数组是used[0, 0, 1]】【boolean的定长数组】
递归传入参数:
序列nums、数组used
终止条件:
如果叶子节点找到一个集合大小等于nums大小的结果则停止(path.size == nums.size)
将result添加当前这个path并返回result
单次取数递归的逻辑(从树层和树枝两个维度考虑):
挨个遍历取数(for (i=0; i<num.size,i++))【不需要像组合那样i从startIndex开始取而是0是因为排列不强调顺序】
1. 树层:考虑思路的第一条的减枝情况:由于提前排序把重复的元素放在相邻位置,先判断相邻位置元素是否相等(num[i] == num[i-1])【这时需要加上且条件i>0可以避免下标是负数】还要加上且条件是减枝的是树层而不是树枝(used[i-1] == false)【因为used数组定义的作用,因为开始递归的是首层,如果是树层的话则上一位索引位置一定是0,否则就是树枝了【树枝是不需要减枝的,因为本身一条路径里就是可以含重复元素的】】减枝完用continue【不用break是因为只需要减枝当前情况,不影响后序同树层遍历的情况】
2. 树枝:树枝的情况需要考虑如果当前索引取过则不能取【取过的数直接跳过,接下来能取的数的范围是没取过的剩下的数】(used[i] == true则continue)
如果没有取过则把当前的used数组的位置标记为true(used[i] = true),再用path把这个元素收集起来(path.add(num[i]))
接下来进行下一层递归:
(backTracking(num, used))
回溯:
(used[i] = false)(path.pop(num[i]))
class Solution {
// 数组result:若干个不定长数组的数组
List<List<Integer>> result = new ArrayList<>();
// 数组path:定长的记录单条路径(其中一个解)数组
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
// 用一个boolean类型的数组used:表示已经使用过的元素以避免顺序不同元素相同的情况
boolean[] used = new boolean[nums.length];
// 初始化used数组
Arrays.fill(used, false);
// 把数组nums先排序好使相同的元素相邻方便后续进行相邻元素的比较
Arrays.sort(nums);
backTracking(nums, used);
// 返回结果
return result;
}
private void backTracking(int[] nums, boolean[] used) {
// 终止条件:如果path的长度和nums一样则返回
if (path.size() == nums.length) {
// 要new一下创建副本不影响其他的path值,因为只把当前满足条件的path值加入result中
result.add(new LinkedList(path));
return;
}
// 遍历每个情况
for (int i = 0; i < nums.length; i++) {
// 考虑到树层需要减枝的情况:当相邻元素相等(此时加入i>0确保下标不是负数)且used数组对应索引的前一个元素是false(代表是树层而不是树枝)
if ( (i > 0) && nums[i] == nums[i-1] && used[i-1] == false) {
continue;
}
// 考虑树枝遍历的情况:控制从剩下没取过的元素范围开始取(跳过当前的元素)
if (used[i] == true) {
continue;
}
// 正常的情况
used[i] = true;
path.add(nums[i]);
// 进行下一个递归
backTracking(nums, used);
// 回溯
used[i] = false;
path.removeLast();
}
}
}
491.递增子序列
题目描述
解题思路
类似于求集合的子集
1. 树层上的去重:相同元素只能取一次否则会导致不同顺序相同值的情况;树枝上是可以取相同元素,因为本身数组里就有数值相同的不同元素【即树层上需要去重,树枝上不用】
2. 保证递增:每次递归取的数要比数组右边的元素大
代码
全局变量:
path数组是单次路径的集合;【LinkedList】
result数组是存放最终结果;【二维数组ArrayList】
传入参数:
nums原数组;
startIndex是控制下次可取值的范围的左区间【不用startIndex则可能可取范围会包含前面已经取过的元素会导致重复】
终止条件:
根据题目要求递增子序列至少有两个元素(path.size >= 2则result.add(path)不需要return是因为否则会错过后续的解)
【在单次递归逻辑开始之前需要定义一个集合used来存放已经用过的元素以后续进行取值:数值相同不同顺序的元素的规避】
单次递归的逻辑:
挨个取数(for (int i=startIndex, i<nums.size, i++))
取数过程中需要减枝的情况:
如果要取的数小于当前path中最右边的数【需要加上且条件path不为空】还需要加上或条件如果在前面定义的集合used取过则需要减枝进而continue
减枝完之后正常取数的情况:
used.add(nums[i])
path.add(nums[i])
下一次递归逻辑(backTracking(nums, i+1))【i+1是因为需要控制下次可取值的范围不是当前元素或当前元素之前的元素,从当前元素的下个元素开始取】
回溯:
path.pop(nums[i])
【这里used数组不需要回溯去掉当前元素是因为这个数组定义的时候只是用来限制当前树层不会出现重复元素,但是递归到下一层的时候因为新赋值会把used数组刷新覆盖】
class Solution {
// 二维数组:存放所有可能的一维数组解
List<List<Integer>> result = new ArrayList<>();
// 记录单条路径(树枝:其中可能的一种解)
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums, 0);
return result;
}
private void backTracking(int[] nums, int startIndex) {
// 终止条件:如果path大小至少为2
if (path.size() >= 2) {
result.add(new LinkedList(path));
}
// 存放用过的值
List<Integer> used = new ArrayList<>();
// 单层递归循环逻辑:
for (int i = startIndex; i < nums.length; i++) {
// 考虑减枝的情况:如果要取的元素小于数组最右边的元素或取了数值相同顺序不同的重复元素
if (!path.isEmpty() && nums[i] < path.getLast() || used.contains(nums[i])) continue;
// 正常取数的情况
used.add(nums[i]);
path.add(nums[i]);
// 下一次递归(i+1是为了从当前元素的下一位作为可取范围的右区间)
backTracking(nums, i+1);
// 回溯(不需要回溯used数组因为它只需要保证同一树层没有重复元素,等遍历下一层的时候会重新赋值覆盖)
path.removeLast();
}
}
}
332.重新安排行程
题目描述
解题思路
难点:
1. 字母考前排在前面,如何处理映射关系
2. 终止条件是什么
3. 如何遍历一个机场对应的所有机场
引用自代码随想录对应算法题的图辅助理解
代码
全局变量:
1. path记录当前的行程路径【树枝】(LinkedList)
2. map记录航班的映射信息:(Map <String, Map<String, Integer>>)【假设有以下机票信息列表:
- 机票1:从 "JFK" 到 "SFO"
- 机票2:从 "JFK" 到 "ATL"
- 机票3:从 "SFO" 到 "ATL"
- 机票4:从 "ATL" 到 "JFK"
- 其结构是{ "JFK": {"SFO": 1, "ATL": 1}, "SFO": {"ATL": 1}, "ATL": {"JFK": 1} }】
主函数中(为了初始化path和map):
1. 遍历所有题目已知机票:
【把所有已知机票信息放入map中:相当于初始化map】
先用TreeMap类型来创建映射<String, Integer>来保证目的地按照字典排序
后面的加入逻辑:
【假设我们有以下机票信息(每张机票由出发城市和目的地城市组成):
- ["JFK", "ATL"]
- ["JFK", "SFO"]
- ["ATL", "JFK"]
- ["ATL", "SFO"]
- ["SFO", "ATL"]
我们的目标是建立一个映射(map
),它可以告诉我们从每个出发城市可以飞往哪些目的地,以及有多少张机票可用。
现在,让我们逐步分析这些机票,并更新我们的 map
。
1. 第一张机票 ["JFK", "ATL"]
- 检查 "JFK" 是否已在
map
中。目前map
是空的,所以 "JFK" 不在其中。 - 创建一个新的
temp
映射,并添加 "ATL" 作为目的地,机票数量为 1。 - 更新
map
,现在map
是:{"JFK": {"ATL": 1}}
。
2. 第二张机票 ["JFK", "SFO"]
- 检查 "JFK" 是否已在
map
中。是的,它已经有了一个映射。 - 获取 "JFK" 的
temp
映射,它目前是{"ATL": 1}
。 - 在
temp
中添加或更新 "SFO" 作为目的地。现在temp
是{"ATL": 1, "SFO": 1}
。 - 更新
map
,现在map
是:{"JFK": {"ATL": 1, "SFO": 1}}
。】
初始化map完了再把JFK加入path【根据题目要求JFK作为出发点相当于初始化path】
开始递归函数
最后结束了返回path作为结果
递归函数和其传入参数:
函数的返回值是boolean【只需要找到一个行程,即通向叶子结点的唯一一条路线】,找到了这个叶子结点则直接返回【这样则可以当找到一个解即收敛终止】
传入参数里要有ticketNunm【表示有多少个航班,终止条件会用】
终止条件:
如果达到了遇到的机场数量等于航班数量加一就终止【即找到了一个行程把所有的航班都串起来了】
单层搜索的逻辑:
先取出res的最右侧赋值为target(res.getLast)【每次从末尾开始作为出发地开始遍历可能的行程】
如果map中有这个target则进行遍历;否则则return false
遍历【for-each循环:for循环的目标是map.get(target).entrySet()【因为此题中map是嵌套的键值对关系Map <String, Map<String, Integer>>,map.get(target)返回的只是Map<String, Integer>,所以需要再操作entrySet返回对应的目的地与这个行程的数量】;因为通过entrySet()得到,所以each的对象类型是Map.Entry而不是Map】
遍历的过程中如果target的值大于0【说明还有机票】则将target的键加入path作为结果,再把target对应的值减1;
然后做下一层的递归【此时为什么要用if(backTracking(ticketNum)) return true;而不像普遍回溯问题backTracking(...)是因为为了遍历所有可能的解决方案。然而,在这个问题中,我们的目标是找到一条有效的、使用了所有机票的特定行程。一旦找到这样的行程,就没有必要继续探索其他可能的行程了。】
回溯:
res.removeLast(); // 回溯:从行程中移除最后添加的城市。
target.setValue(count); // 恢复机票数量。
class Solution {
// 全局变量
// path:用来记录单条路径满足条件的行程连接所有机场【作为题目结果答案】
List<String> path = new LinkedList<>();
// map:用来记录映射信息:格式为{出发地{目的地:剩余的航班数量}}【用HashMap而不用TreeMap是因为注重映射关系而不注重顺序】
Map<String, Map<String, Integer>> map = new HashMap<>();
public List<String> findItinerary(List<List<String>> tickets) {
// 初始化path
path.add("JFK");
// 初始化map
// 遍历所有已知所有机票
for (List<String> flight : tickets) {
// 用TreeMap来创建一个Map格式来存放Map<String, Map<String, Integer>>中的内嵌套Map<String, Integer>,因为题目要求按字母顺序排序
Map<String, Integer> des = new TreeMap<>();
// 如果map中存在对应的键:flight的出发地
if (map.containsKey(flight.get(0))) {
// 获取到对应的目的地映射信息Map<String, Integer>
des = map.get(flight.get(0));
// 更新des的所有映射信息【因为可能出现新的目的地所以要用getOrDefault检查键是否存在,如果目的地已经存在则直接加一】
des.put(flight.get(1), des.getOrDefault(flight.get(1), 0) + 1);
// 如果map中不存在对应的键
} else {
// 创建一个新的目的地映射信息
des.put(flight.get(1), 1);
}
// 把des放入map中【初始化map】
map.put(flight.get(0), des);
}
// 递归遍历
backTracking(tickets.size());
// 返回结果path
return path;
}
// 返回值是boolean是因为这个题找到一种解则结束,而不像普遍的回溯问题返回值定义为void那样要找到所有满足的解才结束
private boolean backTracking(int ticketNum) {
// 终止条件:如果path的数量等于ticketNum+1则结束【相当于一个行程连接所有机场】
if (path.size() == ticketNum + 1) {
return true;
}
// 先判断path的最右位作为出发地是否存在于map中的键
String last = path.getLast();
if (map.containsKey(last)) {
// 遍历这个值对应的map的映射信息<目的地,剩余的航班数量>
for (Map.Entry<String, Integer> target : map.get(last).entrySet()) {
// 单层递归逻辑
// 先判断是否target的值【剩余的航班数量】大于0
int count = target.getValue();
if (count > 0) {
// 大于0才能执行单层递归
// 将目的地加入path行程
path.add(target.getKey());
// 目的地的对应剩余航班数量减一
target.setValue(count - 1);
// 下一层递归:为了早期终止,如果找到一组解则结束
if (backTracking(ticketNum)) return true;
// 回溯
path.removeLast();
target.setValue(count);
}
}
}
return false;
}
}