Leetcode算法训练日记 | day27

发布于:2024-04-20 ⋅ 阅读:(27) ⋅ 点赞:(0)

一、组合总和

1.题目

Leetcode:第 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 。
仅有这两种组合。

示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1
输出: []

2.解题思路

使用回溯算法来解决组合求和问题。backtracking 函数是一个递归函数,它尝试将每个可能的数字添加到当前路径中,并递归地继续添加下一个数字,直到找到所有可能的组合。每次递归调用时,都会检查当前路径是否满足条件(即当前和是否等于目标和),如果满足,则将其添加到结果中。combinationSum 函数是公共接口,它初始化结果和路径,然后开始递归过程。

3.实现代码

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 一、组合总和
class Solution1 {
public:
    vector<vector<int>> result; // 用于存储所有可能组合的结果集
    vector<int> path; // 用于记录当前组合的路径

    // 递归函数,用于生成所有可能的组合
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum > target) { // 如果当前和已经超过目标和,递归返回,不再继续扩展当前路径
            return;
        }

        if (sum == target) { // 如果当前和等于目标和,表示找到了一个有效的组合
            result.push_back(path); // 将当前路径添加到结果集中
            return; // 递归返回,不再继续扩展当前路径
        }

        // 从startIndex开始遍历候选数字数组
        for (int i = startIndex; i < candidates.size(); i++) {
            sum += candidates[i]; // 将当前数字添加到当前和中
            path.push_back(candidates[i]); // 将当前数字添加到当前路径中
            // 递归调用backtracking函数,尝试添加下一个数字
            backtracking(candidates, target, sum, i);
            // 回溯,从当前和中移除最后一个数字,并从当前路径中移除最后一个数字
            sum -= candidates[i];
            path.pop_back();
        }
    }

    // 主函数,用于生成给定目标和的所有可能组合
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear(); // 清空之前的组合结果集
        path.clear(); // 清空当前路径
        backtracking(candidates, target, 0, 0); // 调用递归函数,从索引0开始生成组合
        return result; // 返回所有可能的组合结果集
    }
};

// 二、组合总和(剪枝优化)
class Solution2 {
public:
    vector<vector<int>> result; // 用于存储所有可能组合的结果集
    vector<int> path; // 用于记录当前组合的路径

    // 递归函数,用于生成所有可能的组合
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        if (sum == target) { // 如果当前和等于目标和,表示找到了一个有效的组合
            result.push_back(path); // 将当前路径添加到结果集中
            return; // 递归返回,不再继续扩展当前路径
        }

        // 遍历候选数字数组,从startIndex开始,直到不能形成有效组合为止
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {//剪枝优化
            sum += candidates[i]; // 将当前数字添加到当前和中
            path.push_back(candidates[i]); // 将当前数字添加到当前路径中
            // 递归调用backtracking函数,尝试添加下一个数字
            backtracking(candidates, target, sum, i);
            // 回溯,从当前和中移除最后一个数字,并从当前路径中移除最后一个数字
            sum -= candidates[i];
            path.pop_back();
        }
    }

    // 主函数,用于生成给定目标和的所有可能组合
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear(); // 清空之前的组合结果集
        path.clear(); // 清空当前路径
        sort(candidates.begin(), candidates.end()); // 对候选数字数组进行排序,以便使用剪枝
        backtracking(candidates, target, 0, 0); // 调用递归函数,从索引0开始生成组合
        return result; // 返回所有可能的组合结果集
    }
};


//测试
int main()
{
    Solution1 s;
    vector<vector<int>> result;
    vector<int>candidates = { 2, 3, 6, 7 };
    int target = 7;
    result = s.combinationSum(candidates, target);
    cout << "所有的组合有:" << endl;
    for (auto & ans :result) {
        for (auto& i : ans) {
            cout << i << "  ";
        }
        cout << endl;
    }
    cout << endl;
    return 0;
}

 

二、组合Ⅱ

1.题目

Leetcode:第 40 题

给定一个候选人编号的集合 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]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
2.解题思路

使用回溯算法来解决组合求和问题。combinationSum2首先对候选数字进行排序,然后初始化路径和结果集,并开始回溯过程。backtracking函数是回溯算法的核心,它尝试将每个候选数字添加到当前路径中,并递归地继续寻找满足条件的组合。为了避免重复使用相同的数字,它会跳过与前一个元素相同的候选数字。当找到一个满足条件的组合时,它会将其添加到结果集中。通过这种方式,backtracking函数能够找到所有可能的组合。

3.实现代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 一、组合总和II
class Solution {
public:
    // 定义一个二维数组result,用于存储所有满足条件的组合
    vector<vector<int>> result;
    // 定义一个一维数组path,用于存储当前的组合路径
    vector<int> path;

    // 定义backtracking函数,用于实现回溯算法
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
        // 如果当前和sum等于目标值target,则将当前路径path添加到结果集result中
        if (sum == target) {
            result.push_back(path);
            return;
        }

        // 遍历候选数字candidates,从startIndex开始,直到遍历完或者当前和超过目标值
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            // 如果当前元素与前一个元素相同,跳过当前循环,避免重复使用相同的数字
            if (i > startIndex && candidates[i] == candidates[i - 1]) {
                continue;
            }
            // 将当前候选数字加入到当前和sum中,并将其加入到当前路径path中
            sum += candidates[i];
            path.push_back(candidates[i]);
            // 递归调用backtracking函数,尝试使用下一个候选数字
            backtracking(candidates, target, sum, i + 1);
            // 回溯:从当前和中减去当前候选数字,并从当前路径中移除它
            sum -= candidates[i];
            path.pop_back();
        }
    }

    // 定义combinationSum2函数,用于求解组合求和问题
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        // 清空当前路径path和结果集result
        path.clear();
        result.clear();
        // 将候选数字candidates按升序排序,以便在回溯过程中可以跳过重复的数字
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0); // 调用backtracking函数,开始回溯过程
        return result;// 返回结果集result,其中包含了所有满足条件的组合
    }
};

//测试
int main()
{
    Solution s;
    vector<vector<int>> result;
    vector<int>candidates = { 10,1,2,7,6,1,5 };
    int target = 8;
    result = s.combinationSum2(candidates, target);
    cout << "所有的组合有:" << endl;
    for (auto& ans : result) {
        for (auto& i : ans) {
            cout << i << "  ";
        }
        cout << endl;
    }
    cout << endl;
    return 0;
}

 

三、分割回文串

1.题目

Leetcode:第 131 题

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 

回文串

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

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

输入:s = "a"
输出:[["a"]]
2.解题思路

使用回溯算法来解决分割回文串问题。partition是主函数,它初始化路径和结果集,并开始回溯过程。backtracking函数是回溯算法的核心,它尝试将字符串分割为多个子字符串,并递归地继续寻找满足条件的分区。isPalindrome函数用于检查一个子字符串是否是回文串。通过这种方式,backtracking函数能够找到所有可能的满足条件的分区。

3.实现代码
#include <iostream>
#include <vector>
using namespace std;

// 一、分割回文串
class Solution1 {
public:
    // 定义一个二维字符串数组result,用于存储所有满足条件的分区结果
    vector<vector<string>> result;
    // 定义一个一维字符串数组path,用于存储当前的分区路径
    vector<string> path;

    // 定义backtracking函数,用于实现回溯算法
    void backtracking(const string& s, int startIndex) {
        // 如果当前起始索引startIndex已经到达字符串s的末尾,说明已经处理完字符串
        if (startIndex >= s.size()) {
            result.push_back(path);// 将当前路径path添加到结果集result中
            return;
        }

        // 遍历字符串s,从startIndex开始,直到遍历完字符串
        for (int i = startIndex; i < s.size(); i++) {
            // 检查从startIndex到i的子字符串是否是回文串
            if (isPalindrome(s, startIndex, i)) {
                // 从字符串s中截取从startIndex到i的子字符串,并保存到局部变量str中
                string str = s.substr(startIndex, i - startIndex + 1);
                path.push_back(str);// 将子字符串str添加到当前路径path中
            }
            else {
                continue;
            }
            backtracking(s, i + 1);// 递归调用backtracking函数,尝试使用下一个分区
            path.pop_back(); // 回溯:从当前路径中移除最后一个元素
        }
    }

    // 定义isPalindrome函数,用于检查一个子字符串是否是回文串
    bool isPalindrome(const string& s, int start, int end) {
        // 使用双指针方法,从字符串的两端开始向中间遍历
        for (int i = start, j = end; i < j; i++, j--) {
            // 如果在遍历过程中发现字符不相等,则该子字符串不是回文串
            if (s[i] != s[j]) {
                return false;
            }
        }
        return true;// 如果所有字符都相等,则该子字符串是回文串
    }

    // 定义partition函数,用于求解字符串分区问题
    vector<vector<string>> partition(string s) {
        // 清空当前路径path和结果集result
        result.clear();
        path.clear();
        backtracking(s, 0); // 调用backtracking函数,开始回溯过程
        return result; // 返回结果集result,其中包含了所有满足条件的分区结果
    }
};

// 二、分割回文串(优化)
class Solution2 {
public:
    // 定义一个二维字符串数组result,用于存储所有满足条件的分区结果
    vector<vector<string>> result;
    // 定义一个一维字符串数组path,用于存储当前的分区路径,即已经找到的回文子串
    vector<string> path;
    // 定义一个二维布尔数组isPalindrome,用于存储字符串中每个子串是否是回文串的预先计算结果
    vector<vector<bool>> isPalindrome;

    // 定义backtracking函数,用于实现回溯算法
    void backtracking(const string& s, int startIndex) {
        // 如果当前起始索引startIndex已经到达或超过字符串s的末尾,说明已经找到了一组分区方案
        if (startIndex >= s.size()) {
            // 将当前路径path添加到结果集result中
            result.push_back(path);
            return;
        }
        // 遍历字符串s,从startIndex开始,直到遍历完字符串
        for (int i = startIndex; i < s.size(); i++) {
            // 如果从startIndex到i的子串是回文串(根据预先计算的isPalindrome向量)
            if (isPalindrome[startIndex][i]) {
                // 从字符串s中截取从startIndex到i的子串,并保存到局部变量str中
                string str = s.substr(startIndex, i - startIndex + 1);
                // 将子串str添加到当前路径path中
                path.push_back(str);
            } 
            else {
                continue;
            }
            backtracking(s, i + 1);// 递归调用backtracking函数,尝试使用下一个起始位置i+1进行分区
            path.pop_back();// 回溯过程,从当前路径中移除最后一个元素
        }
    }

    // 定义computePalindrome函数,用于预先计算字符串中所有子串是否是回文串
    void computePalindrome(const string& s) {
        // 根据字符串s的大小,初始化isPalindrome二维向量,所有元素初始为false
        isPalindrome.resize(s.size(), vector<bool>(s.size(), false));
        // 从后向前遍历字符串s,计算每个子串是否是回文串
        for (int i = s.size() - 1; i >= 0; i--) {
            // 由于是检查子串,所以需要从后向前计算,以确保在计算i行时,i+1行已经计算好了
            for (int j = i; j < s.size(); j++) {
                // 如果子串的长度为1,则它是回文串
                if (j == i) {
                    isPalindrome[i][j] = true;
                }
                // 如果子串的长度为2,则检查两个字符是否相等
                else if (j - i == 1) {
                    isPalindrome[i][j] = (s[i] == s[j]);
                }
                // 如果子串的长度大于2,则检查首尾字符是否相等,并且中间子串是否是回文串
                else {
                    isPalindrome[i][j] = (s[i] == s[j] && isPalindrome[i + 1][j - 1]);
                }
            }
        }
    }

    // 定义partition函数,用于求解字符串分区问题
    vector<vector<string>> partition(string s) {
        // 清空当前路径path和结果集result
        result.clear();
        path.clear();
        computePalindrome(s);// 预先计算字符串s中所有子串是否是回文串
        backtracking(s, 0); // 调用backtracking函数,开始回溯过程,以0作为起始位置
        return result;// 返回结果集result,其中包含了所有满足条件的分区结果
    }
};

//测试
int main()
{
    Solution1 p;
    vector<vector<string>> result;
    string s = "aab";
    result = p.partition(s);
    cout << "所有的组合有:" << endl;
    for (auto& ans : result) {
        for (auto& i : ans) {
            cout << i << "  ";
        }
        cout << endl;
    }
    cout << endl;
    return 0;
}

ps:以上皆是本人在探索算法旅途中的浅薄见解,诚挚地希望得到各位的宝贵意见与悉心指导,若有不足或谬误之处,还请多多指教。