代码随想录(八)——回溯算法刷题

回溯算法重要的是能理清楚回溯的树形结构,优化时一般使用剪枝操作。

        回溯三部曲:(1)递归函数的参数 (2)递归的终止条件 (3)单层搜索的逻辑

一、集合里面的元素每个只可以使用一次的组合问题

1.  组合

77. 组合

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

树形结构:

       

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>(); // 这个就是要在末尾插入元素与删除元素
    public List<List<Integer>> combine(int n, int k) {
        // 返回的是组合,就不需要考虑元素的顺序
        // 返回1 - 4中所有元素的组合
        backTracking(n,k,1); 
        return result;

    }
    private void backTracking(int n, int k, int startIndex) {
        // 终止条件
        if(path.size() == k) {  // 收集
        // 创建了一个新的ArrayList对象然后存入 result中
            result.add(new ArrayList<>(path));
            return ;
        }
        // 当前回溯函数开始的位置,即到第几个元素了
        //  for(int i = startIndex; i <= n; i++)  
        // 剪枝:必须现在集合中需要4个元素,path中已经收集了1元素,之后只剩两个元素可以选择了,此时很明显不能成功
        //          所以可以把这一段给砍掉,防止它继续往下面搜素
        for(int i = startIndex; i <= n-(k-path.size()) + 1; i++) {
            path.add(i);
            backTracking(n,k,i+1);
            path.removeLast(); // 移除最后一个元素
        }
    }
}

2.  组合总和Ⅲ

216. 组合总和 III

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次 

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

        这里的变量sum也可以使用n不断减去每个元素来代替,也可以把sum放到回溯函数的参数中。

class Solution {
    private List<List<Integer>> result = new ArrayList<>();
    private List<Integer> path = new ArrayList<>();
    int sum = 0;
    public List<List<Integer>> combinationSum3(int k, int n) {
        // 每个数字只使用一次,求的还是组合
        backTracking(n,k,1);
        return result;
    }

    // 参数; n为相加之和, 用sum记录当前元素的和
    private void backTracking(int n, int k, int startIndex) {
        if(sum > n) {
                return;
        }
        if(sum == n && path.size() == k) { 
            result.add(new ArrayList<>(path));
            return ;
        }
        for(int i=startIndex; i <= 9 - (k - path.size()) + 1; i++){
            // 如果当前这轮sum 大于9就return
            sum += i;
            path.add(i);
            backTracking(n,k,i+1);
            sum -= i;
            path.removeLast();
        }
    }
}
class Solution {
	List<List<Integer>> result = new ArrayList<>();
	LinkedList<Integer> path = new LinkedList<>();

	public List<List<Integer>> combinationSum3(int k, int n) {
		backTracking(n, k, 1, 0);
		return result;
	}

	private void backTracking(int targetSum, int k, int startIndex, int sum) {
		// 减枝
		if (sum > targetSum) {
			return;
		}

		if (path.size() == k) {
			if (sum == targetSum) result.add(new ArrayList<>(path));
			return;
		}

		// 减枝 9 - (k - path.size()) + 1
		for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
			path.add(i);
			sum += i;
			backTracking(targetSum, k, i + 1, sum);
			//回溯
			path.removeLast();
			//回溯
			sum -= i;
		}
	}
}

// 上面剪枝 i <= 9 - (k - path.size()) + 1; 如果还是不清楚
// 也可以改为 if (path.size() > k) return; 执行效率上是一样的
class Solution {
    LinkedList<Integer> path = new LinkedList<>();
    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        build(k, n, 1, 0);
        return ans;
    }

    private void build(int k, int n, int startIndex, int sum) {

        if (sum > n) return;

        if (path.size() > k) return;

        if (sum == n && path.size() == k) {
            ans.add(new ArrayList<>(path));
            return;
        }

        for(int i = startIndex; i <= 9; i++) {
            path.add(i);
            sum += i;
            build(k, n, i + 1, sum);
            sum -= i;
            path.removeLast();
        }
    }
}

3. 电话号码的字母组合

集合里面的元素每个可以使用多次的组合问题

4. 组合总和 

39. 组合总和

-- 无重复元素数组,可无限使用,目的是相加获得目标整数

        给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

注意:1. 主要是要学会如何处理重复的情况,因为如果设置了startIndex ,那样就变成了每一个元素只可以使用一次;如果不设置startIndex 而是让起始位置为下标0,就会出现重复集合的情况,比如[2,2,3] 和 [2,3,2]会同时出现;

       如何解决这种情况呢,就是要让程序选择到当前元素时就不可以选择之前的元素,这种操作需要通过startIndex来进行控制;而对于当前元素可以重复选择操作则是需要通过递归使用dfs时传入的startIndex参数来控制,如果传入的i+1,则变成了上一类的问题,这里应当设置为i,当前元素索引即可。

        2. 本题中的剪枝操作是容易出错的,在剪枝前应当先进行排序。具体原因见代码注释。

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>(); // 收集单层结果
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        /**
            1. 回溯函数的参数
            2. 递归终止条件
            3. 单层搜索的逻辑
         */
         // 为什么要先排序呢?
         // 这里进行排序是为了后面的剪枝操作:(sum+candidates[i] <= target);
         // 如果不排序,虽然sum + candidates[i] 大于 target ,但是不可以保证 sum+candidates[i+1]以及后面的数也会超过target
         Arrays.sort(candidates);
         dfs(candidates,target,0,0);
         return result;
    }   

    // 要传入数组,目标值,单结合操作(加上startIndex), 当前的sum和
    private void dfs(int[] candidates, int target, int startIndex, int sum) {
        if(sum == target) {
            // 收集结果
            // 注意,这里不可以使用下面的代码
            // result.add(path);  // 会出现所有的path都是空 
            //在收集结果时,result.add(path)只是将path的引用添加到结果列表中。
            // 这可能会导致在后续的回溯过程中,path的变化会影响已经添加到结果列表中的内容。
            // 解决方法是在添加到结果列表时,创建一个新的列表并复制path的内容。修改后的代码如下:
            result.add(new ArrayList<>(path));
            return ;
        }
        if(sum > target) {
            return ; // 终止
        }
        // sum < target
        // 这里不可以从 startIndex开始了吧,因为每个元素都可以选择;
        // 如果不设置startIndex 又会出现一个重复的问题
        for(int i = startIndex;i < candidates.length && (sum+candidates[i] <= target); i++) {
            // 选择元素
            sum += candidates[i];
            path.add(candidates[i]);

            dfs(candidates,target,i,sum);
            // 回溯
            sum -= candidates[i];
            path.remove(path.size() - 1);
        }
    }
}

集合里面的元素只能使用一次,但是集合中有重复元素——去重的逻辑

5. 组合总和Ⅱ

40. 组合总和 II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。 

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

由于数组中存在重复的元素,如果只使用startindex(像1.组合问题一样), 那么最后就会出现重复集合的问题


以下有两种去重的方式:

一. 保留startIndex,但是得先将数组排序,这样做的目的是为了让相同的元素挨到一起,然后使用

candidates[i] == candidates[i - 1]

。。。 回来再写 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值