代码随想录算法训练营 回溯算法part03

一、组合总和

39. 组合总和 - 力扣(LeetCode)

因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!

定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。

终止只有两种情况,sum大于target和sum等于target。

sum等于target的时候,需要收集结果。

// 剪枝优化
class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(candidates); // 先进行排序
        backtracking(res, new ArrayList<>(), candidates, target, 0, 0);
        return res;
    }

    public void backtracking(List<List<Integer>> res, List<Integer> path, int[] candidates, int target, int sum, int idx) {
        // 找到了数字和为 target 的组合
        if (sum == target) {
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i = idx; i < candidates.length; i++) {
            // 如果 sum + candidates[i] > target 就终止遍历
            if (sum + candidates[i] > target) break;
            path.add(candidates[i]);
            backtracking(res, path, candidates, target, sum + candidates[i], i);
            path.remove(path.size() - 1); // 回溯,移除路径 path 最后一个元素
        }
    }
}

二、组合总和II

40. 组合总和 II - 力扣(LeetCode)

相等元素剪枝:
需要限制相等元素在每一轮中只被选择一次。实现方式比较巧妙:由于数组是已排序的,因此相等元素都是相邻的。这意味着在某轮选择中,若当前元素与其左边元素相等,则说明它已经被选择过,因此直接跳过当前元素。

与此同时,本题规定中的每个数组元素只能被选择一次。幸运的是,我们也可以利用变量 start 来满足该约束:当做出选择 xi后,设定下一轮从索引 i+1开始向后遍历。这样即能去除重复子集,也能避免重复选择元素

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        def backtrack(
            state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]
        ):
            """回溯算法:子集和 II"""
            # 子集和等于 target 时,记录解
            if target == 0:
                res.append(list(state))
                return
            # 遍历所有选择
            # 剪枝二:从 start 开始遍历,避免生成重复子集
            # 剪枝三:从 start 开始遍历,避免重复选择同一元素
            for i in range(start, len(choices)):
                # 剪枝一:若子集和超过 target ,则直接结束循环
                # 这是因为数组已排序,后边元素更大,子集和一定超过 target
                if target - choices[i] < 0:
                    break
                # 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
                if i > start and choices[i] == choices[i - 1]:
                    continue
                # 尝试:做出选择,更新 target, start
                state.append(choices[i])
                # 进行下一轮选择
                backtrack(state, target - choices[i], choices, i + 1, res)
                # 回退:撤销选择,恢复到之前的状态
                state.pop()

        state = []  # 状态(子集)
        candidates.sort()  # 对 candidates 进行排序
        start = 0  # 遍历起始点
        res = []  # 结果列表(子集列表)
        backtrack(state, target, candidates, start, res)
        return res

三、分割回文串

131. 分割回文串 - 力扣(LeetCode)

每一个结点表示剩余没有扫描到的字符串,产生分支是截取了剩余字符串的前缀;
产生前缀字符串的时候,判断前缀字符串是否是回文。
如果前缀字符串是回文,则可以产生分支和结点;
如果前缀字符串不是回文,则不产生分支和结点,这一步是剪枝操作。
在叶子结点是空字符串的时候结算,此时 从根结点到叶子结点的路径,就是结果集里的一个结果,使用深度优先遍历,记录下所有可能的结果。
使用一个路径变量 path 搜索,path 全局使用一个(注意结算的时候,要生成一个拷贝),因此在递归执行方法结束以后需要回溯,即将递归之前添加进来的元素拿出去;
path 的操作只在列表的末端,因此合适的数据结构是栈。

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

public class Solution {

    public List<List<String>> partition(String s) {
        int len = s.length();
        List<List<String>> res = new ArrayList<>();
        if (len == 0) {
            return res;
        }

        // Stack 这个类 Java 的文档里推荐写成 Deque<Integer> stack = new ArrayDeque<Integer>();
        // 注意:只使用 stack 相关的接口
        Deque<String> stack = new ArrayDeque<>();
        char[] charArray = s.toCharArray();
        dfs(charArray, 0, len, stack, res);
        return res;
    }

    /**
     * @param charArray
     * @param index     起始字符的索引
     * @param len       字符串 s 的长度,可以设置为全局变量
     * @param path      记录从根结点到叶子结点的路径
     * @param res       记录所有的结果
     */
    private void dfs(char[] charArray, int index, int len, Deque<String> path, List<List<String>> res) {
        if (index == len) {
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i = index; i < len; i++) {
            // 因为截取字符串是消耗性能的,因此,采用传子串下标的方式判断一个子串是否是回文子串
            if (!checkPalindrome(charArray, index, i)) {
                continue;
            }
            path.addLast(new String(charArray, index, i + 1 - index));
            dfs(charArray, i + 1, len, path, res);
            path.removeLast();
        }
    }

    /**
     * 这一步的时间复杂度是 O(N),优化的解法是,先采用动态规划,把回文子串的结果记录在一个表格里
     *
     * @param charArray
     * @param left      子串的左边界,可以取到
     * @param right     子串的右边界,可以取到
     * @return
     */
    private boolean checkPalindrome(char[] charArray, int left, int right) {
        while (left < right) {
            if (charArray[left] != charArray[right]) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿玛兰妲。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值