代码随想录算法训练营第二十七天 | 39. 组合总和,40. 组合总和II,131. 分割回文串

39. 组合总和

本题是 集合里元素可以用无数次,那么和组合问题的差别 其实仅在于 startIndex上的控制

题目链接/文章讲解:代码随想录

重点:

1. startIndex的作用

startIndex是为了控制candidates的大小,如果没有startIndex,那么candidates的长度将一直保持为原始长度,并且后续的递归也会得出前面递归的相同的结果,出现重复的结果。

2. 什么时候需要使用startIndex?

只对于求组合来说。如果是一个集合来求组合的话,就需要startIndex。如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex。

3. 递归+回溯法+剪枝

思路:

1. 确定参数及返回值

需要startIndex来规避重复结果

private void backtracking(int[] candidates, int target, int pathSum, int startIndex)

2. 确定终止条件

判断目前路径的总和是否大于target

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

3. 确定单层递归的逻辑

startIndex需要包含当前选取的元素,因为每个元素可以重复选取。for循环可以进行剪枝操作,可以提前判断当前层加上下一层的元素是否大于了target,大于了就没必要去下一层了

for (int i = startIndex; i < candidates.length && pathSum + candidates[i] <= target; i++) {
    path.add(candidates[i]);
    pathSum += candidates[i];
    // 这里startIndex依旧为i,因为candidates每个元素可以重复选取
    backtracking(candidates, target, pathSum, i);
    // 回溯
    path.remove(path.size() - 1);
    pathSum -= candidates[i];
}
List<List<Integer>> result;
List<Integer> path;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
    result = new ArrayList<>();
    path = new ArrayList<>();
    // 先把candidates从小到大排序
    Arrays.sort(candidates);
    backtracking(candidates, target, 0, 0);
    return result;
}

private void backtracking(int[] candidates, int target, int pathSum, int startIndex) {
    // 终止条件
    if (pathSum >= target) {
        if (pathSum == target) {
            result.add(new ArrayList<>(path));
        }
        return;
    }

    // pathSum + candidates[i] <= target,发现下一层的和不大于target就可以去下一层,
    // 大于的话就直接剪枝,因为candidates已经排好序了,后面的数只会更大
    for (int i = startIndex; i < candidates.length && pathSum + candidates[i] <= target; i++) {
        path.add(candidates[i]);
        pathSum += candidates[i];
        // 这里startIndex依旧为i,因为candidates每个元素可以重复选取
        backtracking(candidates, target, pathSum, i);
        // 回溯
        path.remove(path.size() - 1);
        pathSum -= candidates[i];
    }
}

40. 组合总和II

本题开始涉及到一个问题了:去重。

注意题目中给我们 集合是有重复元素的,那么求出来的 组合有可能重复,但题目要求不能有重复组合。 

题目链接/文章讲解:代码随想录

重点:

1.递归+回溯

2. 树层去重和树枝去重

3. 我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。

思路:

递归+回溯法:

1. 确定参数及返回值

需要用used来记录是是否需要树层去重

private void backtracking(int[] candidates, int target, int sum, int startIndex, Boolean[] used)

2. 确定终止条件

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

3. 确定单层递归的逻辑

首先数组的从小到大排序的。因为我们要进行的是树层去重,也就是说同一层的同一个数字不需要重复取,即使取了也会得到相同的结果,可是不同层的相同数字是可以取的。我们用used来记录,如果前一个相同的数字取过了,证明是不同层的,如果没取过,说明是相同层的。

还有因为数组的排好序的,如果当前数字加上之前的sum以及超出了范围,可以直接剪枝。

for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; i++) {
    // 使用used == false来达到树层去重
    if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {
        continue;
    }
    sum += candidates[i];
    path.add(candidates[i]);
    // used == true说明了是在不同层用的
    used[i] = true;
    backtracking(candidates, target, sum, i + 1, used);
    sum -= candidates[i];
    path.remove(path.size() - 1);
    used[i] = false;
}

List<Integer> path;
List<List<Integer>> result;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    path = new ArrayList<>();
    result = new ArrayList<>();
    Boolean[] used = new Boolean[candidates.length];
    Arrays.fill(used, false);
    Arrays.sort(candidates);
    backtracking(candidates, target, 0, 0, used);
    return result;
}

private void backtracking(int[] candidates, int target, int sum, int startIndex, Boolean[] used) {
    // 终止条件
    if (sum > target) {
        return;
    }
    if (sum == target) {
        result.add(new ArrayList<>(path));
        return;
    }

    for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; i++) {
        // 使用used == false来达到树层去重
        if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {
            continue;
        }
        sum += candidates[i];
        path.add(candidates[i]);
        // used == true说明了是在不同层用的
        used[i] = true;
        backtracking(candidates, target, sum, i + 1, used);
        sum -= candidates[i];
        path.remove(path.size() - 1);
        used[i] = false;
    }
}

131. 分割回文串

本题较难,大家先看视频来理解 分割问题,明天还会有一道分割问题,先打打基础。 

题目链接/文章讲解:代码随想录

重点:

1. 用startIndex来模拟切割线

思路:

递归+回溯:

1. 确定参数及返回值

private void backtracking(String s, int startIndex)

2. 确定终止条件

切割线已经到最末尾了

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

3. 确定单层递归的逻辑

判断当前字串是否为回文串,是的话就加入到path中

for (int i = startIndex; i < s.length(); i++) {
    if (isPalindrome(s, startIndex, i)) {
        path.add(s.substring(startIndex, i + 1));
    } else {
        continue;
    }
    backtracking(s, i + 1);
    path.remove(path.size() - 1);
}

总结:

  • 切割问题可以抽象为组合问题
  • 如何模拟那些切割线
  • 切割问题中递归如何终止
  • 在递归循环中如何截取子串
  • 如何判断回文
List<String> path;
List<List<String>> result;
public List<List<String>> partition(String s) {
    path = new ArrayList<>();
    result = new ArrayList<>();
    backtracking(s, 0);
    return result;
}

private void backtracking(String s, int startIndex) {
    // 终止条件
    if (startIndex >= s.length()) {
        result.add(new ArrayList<>(path));
        return;
    }
    // 树的当前层
    for (int i = startIndex; i < s.length(); i++) {
        // 判断当前子串是否为回文串
        if (isPalindrome(s, startIndex, i)) {
            path.add(s.substring(startIndex, i + 1));
        } else {
            continue;
        }
        // 去树的下一层
        backtracking(s, i + 1);
        // 回溯
        path.remove(path.size() - 1);
    }
}

private boolean isPalindrome(String s, int start, int end) {
    // 双指针判断回文串
    while (start < end) {
        if (s.charAt(start) != s.charAt(end)) {
            return false;
        }
        start++;
        end--;
    }
    return true;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值