回溯算法题(二)

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” 为例,做第一步时我们有几种选择?

    1. 选 “2” 作为第一个片段
    2. 选 “25” 作为第一个片段
    3. 选 “255” 作为第一个片段
  • 能切三种不同的长度,切第二个片段时,又面临三种选择。

  • 这会向下分支形成一棵树,我们用 DFS 去遍历所有选择,必要时提前回溯。

    • 因为某一步的选择可能是错的,得不到正确的结果,不要往下做了。撤销最后一个选择,回到选择前的状态,去试另一个选择。

回溯要点二 :约束

  • 约束条件限制了当前的选项,这道题的约束条件是:
    1. 一个片段的长度是 1~3
    2. 片段的值范围是 0~255
    3. 不能是 “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;
    }
}

原文地址

————————————————————————————————————————

13.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值