9.子集II
分析
- “去重”操作是在同层中进行的,不同层的相同元素不能“去重”。
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if(nums.length < 1) return res;
Deque<Integer> path = new ArrayDeque<>();
Arrays.sort(nums);
dfs(nums, 0, path, res);
return res;
}
public void dfs(int[] nums, int start, Deque<Integer> path, List<List<Integer>> res) {
res.add(new ArrayList<>(path));
for(int i = start, i < nums.length; i++) {
//这是判断同层分支节点不能有相同的值
if(i > start && nums[i] == nums[i - 1]) continue;
//if(i > 0 && nums[i] == nums[i - 1]) continue; 如果这样判断,把子节点和父节点相同的情况也会删去,这样是不对的,不同层元素允许相同
path.addLast(nums[i]); //做出选择
dfs(nums, i + 1, path, res); //进入下一轮循环
path.removeLast(); //撤销选择
}
}
}
————————————————————————————————————————
10.复原IP地址
分析
在画树形图的过程中,会有些枝叶是没有必要的,把没有必要的枝叶剪去的操作就是剪枝,在代码中一般通过 break 或者 continue 和 return (表示递归终止)实现。
剪枝条件:
1、一开始,字符串的长度小于 4 或者大于 12 ,一定不能拼凑出合法的 ip 地址(这一点可以一般化到中间结点的判断中,以产生剪枝行为);
2、每一个结点可以选择截取的方法只有 3 种:截 1 位、截 2 位、截 3 位,因此每一个结点可以生长出的分支最多只有 3 条分支;
根据截取出来的字符串判断是否是合理的 ip 段,这里写法比较多,可以先截取,再转换成 int ,再判断。这里采用的做法是先转成 int,是合法的 ip 段数值以后,再截取。
3、由于 ip 段最多就 4 个段,因此这棵三叉树最多 4 层,这个条件作为递归终止条件之一;
4、每一个结点表示了求解这个问题的不同阶段,需要的状态变量有:
- rest:还剩下多少个待分割 ip 字段
- start:截取 ip 段的起始位置;
- path:记录从根结点到叶子结点的一个路径(回溯算法常规变量,是一个栈);
回溯要点一:选择
-
以 “25525511135” 为例,做第一步时我们有几种选择?
- 选 “2” 作为第一个片段
- 选 “25” 作为第一个片段
- 选 “255” 作为第一个片段
-
能切三种不同的长度,切第二个片段时,又面临三种选择。
-
这会向下分支形成一棵树,我们用 DFS 去遍历所有选择,必要时提前回溯。
- 因为某一步的选择可能是错的,得不到正确的结果,不要往下做了。撤销最后一个选择,回到选择前的状态,去试另一个选择。
回溯要点二 :约束
- 约束条件限制了当前的选项,这道题的约束条件是:
- 一个片段的长度是 1~3
- 片段的值范围是 0~255
- 不能是 “0x”、“0xx” 形式(测试用例告诉我们的)
- 用这些约束进行充分地剪枝,去掉一些选择,避免搜索「不会产生正确答案」的分支。
回溯要点三 :目标
- 目标决定了什么时候捕获答案,什么时候砍掉死支,回溯。
- 目标是生成 4 个有效片段,并且要耗尽 IP 的字符。
- 当满足该条件时,说明生成了一个有效组合,加入解集,结束当前递归,继续探索别的分支。
- 如果满4个有效片段,但没耗尽字符,不是想要的解,不继续往下递归,提前回溯。
class Solution {
int len;
public List<String> restoreIpAddresses(String s) {
List<String> res = new ArrayList<>();
len = s.length();
if(len < 4 || len > 12) return res;
Deque<String> path = new ArrayDeque<>(4);
}
public void dfs(String s, int rest, int start, Deque<String> path, List<String> res) {
//回溯算法的步骤
//1.回溯结束的条件和将路径加入到结果集中
if(start == len) {
if(rest == 0) res.add(String.join(".", path));
return;
}
//2.在选择列表中做出选择
for(int i = start; i < start + 3; i++) { //这里i表示要截取的位置,从start开始,长度最多为3
if(i >= len) break; //截取的位置超出了字符串的长度,直接结束循环
//if(rest * 3 < len - i) continue; //还需要截取的IP字段的最大总长度rest*3,
//小于实际所剩的IP字段的长度,直接进入下一轮循环,直到最大总长度大于等于所剩长度
if((rest - 1) * 3 < len - i - 1) continue; //判断除去当前的IP字段后,还剩下要截取rest-1个字段,再与之剩下的字符进行长度比较
if(IpLegel(s, start, i)) {
//3.做选择
path.addLast(s.substring(start, i + 1);
dfs(s, rest - 1, i + 1, path, res);
//4.撤销选择
path.removeLast();
}
}
}
//判断该IP是否是合法的
public boolean IpLegel(String s, int left, int right) {
int len = right - left + 1;
if(len > 1 && s.charAt(0) == '0') return false;
int res = 0;
for(int i = left; i <= right; i++) {
res = res * 10 + s.charAt(i) - '0';
}
return res >= 0 && res <= 255;
}
}
原文地址
————————————————————————————————————————
11.路径总和II
分析
问完成一件事情的所有解决方案,一般采用回溯算法(深度优先遍历)完成。
根据这个问题的特点,我们可以采用 先序遍历 的方式:先使用 sum 减去当前结点(如果非空)的值,然后再递归处理左子树和右子树。如果到了叶子结点,sum 恰好等于叶子结点的值,我们就得到了一个符合条件的列表(从根结点到当前叶子结点的路径)。
递归终止条件:
- 如果遍历到的结点为空结点,返回;
- 如果遍历到的叶子结点,且 sum 恰好等于叶子结点的值。
class Solution {
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> res = new ArrayList<>();
if(root == null) return res;
Deuqe<Integer> path = new ArrayDeque<>();
dfs(root, targetSum, path, res);
return res;
}
public void dfs(TreeNode root, int targetSum, Deque<Integer> path, List<List<Integer>> res) {
//递归终止条件1
if(root == null) return;
//递归终止条件2
if(root.left == null && root.right == null && targetSum == root.val) {
// 当前结点的值还没添加到列表中,所以要先添加,然后再移除
path.addLast(root.val);
res.add(new ArrayList<>(path));
path.removeLast();
return;
}
path.addLast(root.val);
dfs(root.left, targetSum - root.val, path, res);
dfs(root.right, targetSum - root.val, path, res);
path.removeLast();
}
}
原文地址
————————————————————————————————————————
12.分割回文串
分析
回溯算法思考的步骤:
1.画出树型结构,本题的递归树模型是一棵二叉树;
2.编码;
-
每一个结点表示剩余没有扫描到的字符串,产生分支是截取了剩余字符串的前缀;
-
产生前缀字符串的时候,判断前缀字符串是否是回文:
- 如果前缀字符串是回文,则可以产生分支和结点;
- 如果前缀字符串不是回文,则不产生分支和结点,这一步是剪枝操作。
-
在叶子结点是空字符串的时候结算,此时 从根结点到叶子结点的路径,就是结果集里的一个结果,使用深度优先遍历,记录下所有可能的结果。
-
使用一个路径变量 path 搜索,path 全局使用一个(注意结算的时候,要生成一个拷贝),因此在递归执行方法结束以后需要回溯,即将递归之前添加进来的元素拿出去。
-
path 的操作只在列表的末端,因此合适的数据结构是栈。
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
if(s.length() < 1) return res;
Deque<String> path = new ArrayDeque<>();
dfs(s, 0, path, res);
return res;
}
/**
* @param s
* @param start 起始字符的索引
* @param path 记录从根结点到叶子结点的路径
* @param res 记录所有的结果
*/
public void dfs(String s, int start, Deque<String> path, List<List<String>> res) {
if(start == s.length()) {
res.add(new ArrayList<>(path));
return;
}
for(int i = start; i < s.length(); i++) {
// 因为截取字符串是消耗性能的,因此,采用传子串下标的方式判断一个子串是否是回文子串
if(!check(s, start, i)) continue;
path.addLast(s.substring(start, i + 1));
dfs(s, i + 1, path, res);
path.removeLast();
}
}
/**
* 检查当前字符串是否为回文串
* 这一步的时间复杂度是 O(N),优化的解法是,先采用动态规划,把回文子串的结果记录在一个表格里
* @param s
* @param start 子串的左边界,可以取到
* @param end 子串的右边界,可以取到
* @return
*/
public boolean check(String s, int start, int end) {
while(start <= end) {
if(s.charAt(start) != s.charAt(end)) return false;
start++;
end--;
}
return true;
}
}
原文地址
————————————————————————————————————————