39. 组合总和 40.组合总和II 131.分割回文串

本文介绍了编程问题中的组合总和和组合总和II两种情况,涉及回溯算法和去重技巧,同时讨论了分割回文串问题,展示了递归实现及优化策略,重点分析了时间复杂度和空间复杂度。
摘要由CSDN通过智能技术生成

39. 组合总和 40.组合总和II 131.分割回文串

39. 组合总和

力扣题目链接(opens new window)

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

  • 所有数字(包括 target)都是正整数。
  • 解集不能包含重复的组合。

示例 1:

  • 输入:candidates = [2,3,6,7], target = 7,
  • 所求解集为: [ [7], [2,2,3] ]

示例 2:

  • 输入:candidates = [2,3,5], target = 8,
  • 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]

思路:回溯

题目要求【candidates 中的数字可以无限制重复被选取】
那么在for循环遍历时,需要增加子树被重复选取的情况。

递归方法设置startIndex 不用i+1了,表示可以重复读取当前的数.将startIndex 设置为i即可

本题需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?
如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window),216.组合总和III (opens new window)。
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合(opens new window)
注意以上只是说求组合的情况,如果是排列问题,又是另一套分析的套路

时间复杂度O(n * 2^n)

其中 n 是数组 candidates 的长度。在大部分递归 + 回溯的题目中,我们无法给出一个严格的渐进紧界,故这里只分析一个较为宽松的渐进上界。
在最坏的情况下,数组中的每个数都不相同。在递归时,每个位置可以选或不选,如果数组中所有数的和不超过 target,那么
 2^n种组合都会被枚举到;在 target 小于数组中所有数的和时,我们并不能解析地算出满足题目要求的组合的数量,但我们知道每得到一个满足要求的组合,需要 O(n) 的时间将其放入答案中,因此我们将 O(2^n)
与 O(n)相乘,即可估算出一个宽松的时间复杂度上界。

空间复杂度 o(target)

除答案数组外,空间复杂度取决于递归的栈深度,在最差情况下需要递归 O(target)层。

代码如下

public List<List<Integer>> combinationSum(int[] candidates, int target) {
    backTracking(candidates, target, 0);
    return result;
}
List<Integer> path = new ArrayList<>();
List<List<Integer>> result = new ArrayList<>();

public void backTracking(int[] candidates, int target, int startIndex) {
    int sum = 0;
    for (int i = 0; i < path.size(); i++) {
        sum = sum + path.get(i);
    }
    if (sum == target) {
        result.add(new ArrayList<>(path));
        return;
    }
    if(sum > target)
        return;

    if (startIndex >= candidates.length)
        return;

    for (int i = startIndex; i < candidates.length; i++) {
        path.add(candidates[i]);
        backTracking(candidates, target, i);// 关键点:不用i+1了,表示可以重复读取当前的数
        path.remove(path.size() - 1);
    }
}

减枝优化

在没优化代码中,终止条件存在if(sum > target) return;
意味什么呢? sum的值在大于target,还会进入递归。这时有人肯定会有疑问,进入递归又怎么了,能消耗多少时间复杂度呢?

举个例子
有10万个数字,其中有9万个数字对应的sum和都大于target,如果做了限制,可减少大量数字进入递归.

对数字进行升序排序,排序后for循环中,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历。

代码如下

public List<List<Integer>> combinationSum(int[] candidates, int target) {
    Arrays.sort(candidates);
    backTracking(candidates, target, 0);
    return result;
}

List<Integer> path = new ArrayList<>();
List<List<Integer>> result = new ArrayList<>();
int sum = 0;

public void backTracking(int[] candidates, int target, int startIndex) {

    if (sum == target) {
        result.add(new ArrayList<>(path));
        return;
    }
    if (startIndex >= candidates.length)
        return;

    for (int i = startIndex; i < candidates.length; i++) {
        sum = sum + candidates[i];
        if(sum > target){ // 减枝优化
            sum = sum-candidates[i];
            break;
        }

        path.add(candidates[i]);
        backTracking(candidates, target, i);// 关键点:不用i+1了,表示可以重复读取当前的数
        path.remove(path.size() - 1);
        sum = sum - candidates[i];
    }
}

40.组合总和II

力扣题目链接(opens new window)

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

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

说明: 所有数字(包括目标数)都是正整数。解集不能包含重复的组合。

  • 示例 1:
  • 输入: candidates = [10,1,2,7,6,1,5], target = 8,
  • 所求解集为:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]
  • 示例 2:
  • 输入: candidates = [2,5,2,1,2], target = 5,
  • 所求解集为:
[
  [1,2,2],
  [5]
]

思路:回溯

本题开始涉及到一个问题了:去重。
注意题目中给我们 集合是有重复元素的,那么求出来的 组合有可能重复,但题目要求不能有重复组合。
最直接的思路,求出集合后,对集合去重
但是容易超时,所以要在遍历过程中去重
下面代码是求出集合后去重
时间复杂度O(n * 2^n)

其中 n 是数组 candidates 的长度。在大部分递归 + 回溯的题目中,我们无法给出一个严格的渐进紧界,故这里只分析一个较为宽松的渐进上界。
在最坏的情况下,数组中的每个数都不相同。在递归时,每个位置可以选或不选,如果数组中所有数的和不超过 target,那么
 2^n种组合都会被枚举到;在 target 小于数组中所有数的和时,我们并不能解析地算出满足题目要求的组合的数量,但我们知道每得到一个满足要求的组合,需要 O(n) 的时间将其放入答案中,因此我们将 O(2^n)
与 O(n)相乘,即可估算出一个宽松的时间复杂度上界。

空间复杂度o(n)

除了存储答案的数组外,我们需要 O(n)的空间存储result、递归中存储当前选择的数的列表、以及递归需要的栈。

代码如下

List<Integer> path = new ArrayList<>();
List<List<Integer>> result = new ArrayList<>();
List<List<Integer>> distinctResult = new ArrayList<>();
int sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    Arrays.sort(candidates);
    backTracking(candidates, target, 0);
    HashSet<List<Integer>> set = new HashSet<>(result);
    for(List<Integer> list : set){
        distinctResult.add(list);
    }
    return distinctResult;
}


public void backTracking(int[] candidates, int target, int startIndex) {

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

    if (startIndex >= candidates.length)
        return;

    for (int i = startIndex; i < candidates.length; i++) {
        sum = sum + candidates[i];
        if (sum > target) {
            sum = sum - candidates[i];
            break;
        }

        path.add(candidates[i]);
        backTracking(candidates, target, i + 1);// 关键点:不用i+1了,表示可以重复读取当前的数
        path.remove(path.size() - 1);
        sum = sum - candidates[i];
    }
}

思路:回溯优化版本

本题开始涉及到一个问题了:去重。
注意题目中给我们 集合是有重复元素的,那么求出来的 组合有可能重复,但题目要求不能有重复组合。
最直接的思路,求出集合后,对集合去重
但是容易超时,所以要在遍历过程中去重
所谓的去重,是指元素不能重复
组合问题可以转换为树型的结构。树型结构存在树枝和树层两个维度,那么应该在哪个维度去重呢?
树枝是一个组合,本题组合元素允许重复,不用去重
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
举例如下【1,1,2】 target = 3,

在这里插入图片描述

本题符合条件的集合有【1,2】【1,2】,可以发现在for循环遍历时(同一树层),遍历了两次1.那么如果在此处限制去重,那么最终符合条件的集合只有【1,2】一个集合
所以最终目的是在for循环处,对前后遍历的结点val进行判断,如果结点的val遍历过,则跳过
但是还有前提,数组要有序

代码如下

> 时间复杂度O(n * 2^n)
> 其中 n 是数组 candidates 的长度。在大部分递归 + 回溯的题目中,我们无法给出一个严格的渐进紧界,故这里只分析一个较为宽松的渐进上界。
> 在最坏的情况下,数组中的每个数都不相同。在递归时,每个位置可以选或不选,如果数组中所有数的和不超过 target,那么
>  2^n种组合都会被枚举到;在 target 小于数组中所有数的和时,我们并不能解析地算出满足题目要求的组合的数量,但我们知道每得到一个满足要求的组合,需要 O(n) 的时间将其放入答案中,因此我们将 O(2^n)
>O(n)相乘,即可估算出一个宽松的时间复杂度上界。

> 空间复杂度o(n)
> 除了存储答案的数组外,我们需要 O(n)的空间存储result、递归中存储当前选择的数的列表、以及递归需要的栈。



static List<Integer> path = new ArrayList<>();
static List<List<Integer>> result = new ArrayList<>();
static int sum = 0;

public static List<List<Integer>> combinationSum2(int[] candidates, int target) {
    Arrays.sort(candidates);
    backTracking(candidates, target, 0);
    return result;
}


public static void backTracking(int[] candidates, int target, int startIndex) {

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

    for (int i = startIndex; i < candidates.length; i++) {
        if (i > startIndex && candidates[i] == candidates[i - 1]) {
            continue;
        }
        sum = sum + candidates[i];
        if (sum > target) {
            sum = sum - candidates[i];
            break;
        }

        path.add(candidates[i]);
        backTracking(candidates, target, i + 1);
        path.remove(path.size() - 1);
        sum = sum - candidates[i];
    }
}

问题

去重代码有误。我的写法为i > 0,本意想过滤树层重复元素

错误代码如下

if (i > 0 && candidates[i] == candidates[i - 1]) {
               continue;
}

但是这种写法,无意中过滤【1,1,2,5,6,7,10】 target = 8 中存在重复元素组合的情况。

比如【1,1,6】组合
1.当 i = 0,元素A为1,调用方法递归,将i + 1,元素B为1加入递归
2.如果判断条件为i > 0 && candidates[i] == candidates[i - 1],那么将跳过B = 1,相当于对树枝存在重复元素的情况也过滤,所以最终答案不存在【1,1,6】组合

正确写法:这样会避免过滤树枝的元素

if (i > startIndex && candidates[i] == candidates[i - 1]) {
               continue;
}

131.分割回文串

力扣题目链接(opens new window)

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回 s 所有可能的分割方案。

示例: 输入: “aab” 输出: [ [“aa”,“b”], [“a”,“a”,“b”] ]

思路:回溯

题目要求每个子串都是回文串。
能想到的方法是双指针法定义左右指针,左指针指向最左边元素,右指针指向左指针右侧元素
1.若左右指针指向元素相同,则右指针向后移动
2.若左右指针指向元素不同,将【左指针,右指针-1】字符串截取,左指针移动到右指针的位置
但该方法存在局限,不能找出字符串所有可能的分割方案,只能找出一种

题解描述很清楚

本地有两个问题需要解决

1.如何判断一个字符串是否为回文?

  • 定义左右双指针指向字符串两侧。若双指针指向元素不相同,则字符串不是回文。若双指针指向元素相同,则左指针+1,右指针-1比较。直至左指针下标大于右指针。

2.如何切割字符串?

该题目不仅要切割字符串,还要有多种方式切割字符串,很难想到

这种切割字符串,找出符合条件的子字符串。其实很类似从遍历一个集合,从中找出符合某些条件的组合。

因此切割问题类似组合问题

所以也可以将切割的过程比作一棵树

131.分割回文串

红色的切割线位置,是for循环中,startIndex所在位置

如果切割线startIndex能够移动到字符串最后并切割,那么就是合适的子串集合

回溯法三要素

1.方法入参和返回值:入参String str,int startIndex 返回值void

2.中止条件:切割线startIndex移动到字符串最后切割,path加入result

3.核心逻辑

递归循环中截取字符串。在for循环for(int i = startIndex;i<s.size;i++)中,起始位置为startIndex

每次切割的字符串范围为【startIndex,i】,然后判断字符串是否为回文

是回文则加入path,否则continue跳出该循环

代码如下

    // 时间复杂度O(n * 2^n) n表示字符串字符个数
    // 空间复杂度o(n)
List<List<String>> result = new ArrayList<>();
List<String> path = new ArrayList<>();

public List<List<String>> partition(String s) {
    backTracking(s, 0);
    return result;
}

public void backTracking(String s, int startIndex) {
    if (startIndex >= s.length()) {// 中止条件:分割线startIndex到达字符串最后
        result.add(new ArrayList<>(path));
        return;
    }

    for (int i = startIndex; i < s.length(); i++) {
        String str = s.substring(startIndex, i + 1);// 分割区间【start,i】字符串
        if (!isPlalindrome(str)) {
            continue;
        }
        path.add(str);
        backTracking(s, i + 1);// 当前字符不能重复切割
        path.remove(path.size() - 1);// 回溯
    }

}

private boolean isPlalindrome(String str) {
    int left = 0;
    int right = str.length() - 1;
    while (left < right) {
        if (str.charAt(left) == str.charAt(right)) {
            left++;
            right--;
        } else {
            return false;
        }
    }
    return true;
}

问题

边缘条件判断有误
中止条件:需要让切割线移动到字符串的最后,并完成切割
但我的代码在切割线移动到字符串的最后时,就中止,并没有完成切割
导致结果缺少子集

在这里插入图片描述

错误代码

if (startIndex == s.length() -1) {// 中止条件:分割线startIndex到达字符串最后
           result.add(new ArrayList<>(path));
           return;
}

正确代码

if (startIndex >= s.length()) {// 中止条件:分割线startIndex到达字符串最后
           result.add(new ArrayList<>(path));
           return;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值