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

LeetCode39

2025-02-20 15:45:16 星期四

题目描述:力扣39
文档讲解:代码随想录(programmercarl)39. 组合总和
视频讲解:《代码随想录》算法视频公开课:Leetcode:39. 组合总和讲解

代码随想录视频内容简记

要点1

注意,这个39有两个点需要分清楚,就是在确定单层递归的逻辑中,for循环中i = index和递归中给index参数赋值i的作用是不同的,二者是配合起来使用的


for (int i = index; i < candidates.size(); i++) {
	...
	backtracking(candidates, target, i, sum);
	...
}
  1. 在for循环中i = index是为了在每次取元素时,只能取index及以后或index以后的元素。例如,如果给index参数赋值i,那么[2, 5, 3]取完2之后,就还能取[2, 5, 3];给index参数赋值i + 1[2, 5, 3]取完2之后,就只能取[5, 3]。一种是可以重复取,一种是不可以重复取,但都是为了避免出现一个组合不同排列的情况。

如果写错写成i = 0,那么index参数失效,不再控制取数的位置。那么此时递归函数的i也就会失效,没有意义,所以这么写是很离谱的。比如取3之后,又可以从[2, 5, 3]里面取,就会出现下面的情况


for (int i = 0; i < candidates.size(); i++) {
	sum += candidates[i];
	path.push_back(candidates[i]);
	backtracking(candidates, target, i, sum);
	sum -= candidates[i];
	path.pop_back();
}
  1. 给index参数赋值i的作用,在之前的77组合中,是i + 1的,他的作用就是控制一个相同的元素能不能取多次,也只有给在本题中可以,所以不用+1

如果写成i + 1就是不能重复取,就是下面的这种结果,如果写成i,就是可以重复取,他的目的就是给下一次递归的for循环的i = index创造条件


for (int i = index; i < candidates.size(); i++) {
	sum += candidates[i];
	path.push_back(candidates[i]);
	backtracking(candidates, target, i + 1, sum);
	sum -= candidates[i];
	path.pop_back();
}

所以,for循环中的i = index一定是和递归中的参数index同时存在的,如果没有了i = index,那么参数index也就失去了其意义。只有当i = index,才能去给index赋值i或者i + 1来决定是否可以取index所指的重复元素

要点2

在本题中,因为是可以一直取重复的元素的,那么在一条path上只要不加限制,就会无限添加,一开始的时候总会报stackoverflow。

那么,就是终止条件有问题,这道题需要加上的终止条件有这个if (sum > target) return;其实看着很像之前的剪枝操作,但确实是一个终止条件,因为不加上就终止不了了。

要点3

这道题和之前的很像,一开始写过了的,又看k哥视频的时候,就着重看了一下剪枝的部分,感觉这个剪枝法确实有点出乎意料。

虽然有sum > target的条件卡着,但是这样的递归本身是无效的,也一定是无效的,所以本题的剪枝就索性跳过递归的过程。对candidates先进行排序,之后如上图,后面的部分进行剪枝,其实我感觉也没有剪多少嘿嘿😅。之后直接在for循环中控制即可。

因为是sum + candidates[i] <= target,那么终止条件应该加上candidates[i] <= target - sum;,变成i < candidatse.size() && candidates[i] <= target - sum;这个是一个数值类的条件限制,不是77组合优化那个数量类的条件限制,所以不用考虑什么i加不加1的问题。说白了就是少进入一次递归,一进去也就返回了。

LeetCode测试

力扣的执行用时和内存分布每次都不太一样,其实想看一下剪不剪差了多少的。又崩掉一回😀

未剪枝

点击查看代码
class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking (vector<int>& candidates, int target, int index, int sum) {
        if (sum > target) return;
        if (sum == target) {
            result.push_back(path);
            return;
        }
        for (int i = index; i < candidates.size(); i++) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, i, sum);
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

剪枝

点击查看代码
class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking (vector<int>& candidates, int target, int index, int sum) {
        if (sum > target) return;
        if (sum == target) {
            result.push_back(path);
            return;
        }
        for (int i = index; i < candidates.size() && candidates[i] <= target - sum; i++) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, i, sum);
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

LeetCode40

题目描述:力扣40
文档讲解:代码随想录(programmercarl)40.组合总和II
视频讲解:《代码随想录》算法视频公开课:回溯算法中的去重,树层去重树枝去重,你弄清楚了没?| LeetCode:40.组合总和II

代码随想录视频内容简记

这个题其实是很巧妙的,和216,77还有上面的39不同的地方在于,40是有重复元素的。这个写着写着就发现不对劲了,一开始写发现总是这样的结果

画出树形图看了一下,发现确实是k哥说的“树层”有重复的元素。不太会操作,认真听k哥讲了一下,用到了核心的used数组,这个数组就是用来进行树层去重

什么是树层去重?什么又是树枝去重?

注:图中蓝色的是used数组

可以看到,红色的三个结果,因为2的数量的关系,其在递归中位于同一层,就是所谓的同一“树层”。

下面所说的“重复的元素”都指:是不同的元素,但是数值相同,所以看着是重复的

那么树枝去重,可以理解为深度优先搜索,就是向深处递归的过程中,出现了重复的元素,举个例子,比如在39题中,可以重复取元素,像[2,2,3]就是同一树枝,不需要去重。但在本题中,题目中明确要求不能重复选一个元素,所以需要进行去重,这个其实就是for循环递归中的i + 1就可以去


其实也就是,需要对[1,2,2,2,5]中的树层进行去重,当然这里的数组是排过序的,至于为什么需要排序,因为used数组使用的特殊性,就先当成特殊用法记住好了

梳理

  1. 确定函数的参数和返回值,其他不变,参数会多一个used数组

  2. 确定递归的终止条件,这个和之前几个题都是一样的

  3. 确定单层递归的逻辑。要想对树层去重,最直观的就是数组元素相等,就是重复的了呗。但是还有一点,就是这里要用到核心的used数组,used数组会将每一个取过的元素的位置置为1,只有i的当前一位的used值为0才能去重,前一位为0,表示这是刚刚回溯过的。因为在本题中,递归中出现了一次,那么回溯时出现的重复元素,就一定包含在前面的递归中,可能会出现相同的结果集path,所以只有回溯的(used[i - 1]为0)元素相等,才能去去重。

LeetCode测试

在书写的时候注意一个小细节

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

这里正常来说是要书写i - 1的,我刚开始写的i--,感觉应该是一样的。

# include<iostream>
using namespace std;
int main() {
	int nums[5] = {0, 1, 2, 3, 4};
	int i = 4;
	if (nums[i--]) cout << i << endl;
}

这段代码输出结果i = 3

# include<iostream>
using namespace std;
int main() {
	int nums[5] = {0, 1, 2, 3, 4};
	int i = 4;
	if (nums[i - 1]) cout << i << endl;
}

这段代码输出结果i = 4,可见,当用i--的时候,它实际多了一步给i赋值的操作,忽略了这点,所以i的值被改变了,就肯定不对了。

剪枝

这道题不剪枝的话是会在提交的时候报“超出时间限制”的,卡在第125个测试用例。还是两个剪枝的地方

第一个是if (sum > target) return;,加上这一个就可以通过了

还有一个是在for循环中,candidates[i] + sum <= target;,其实就是少进一层递归


感觉这个题比之前的77,216,39琢磨头都多,有点绞尽脑汁的感觉,确实感谢k哥的刷题顺序,要不然按照里扣的题号做估计得头秃了。哈哈😂完整代码如下:

点击查看代码
class Solution {
private:
    vector<int> path;
    vector<vector<int>> result;
    
    void backtracking (vector<int>& candidates, int target, int index, int sum, vector<bool> used) {
        if (sum > target) return;
        if (sum == target) {
            result.push_back(path);
            return;
        }
        for (int i = index; i < candidates.size() && candidates[i] + sum <= target; i++) {
            if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
                continue;
            }
            sum += candidates[i];
            path.push_back(candidates[i]);
            used[i] = true;
            backtracking(candidates, target, i + 1, sum, used);
            sum -= candidates[i];
            path.pop_back();
            used[i] = false;
        }
    }
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<bool> used(candidates.size(), false);
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0, used);
        return result;
    }
};

LeetCode131

题目描述:力扣131
文档讲解:代码随想录(programmgercarl)131.分割回文串
视频讲解:《代码随想录》算法视频公开课:131.分割回文串

代码随想录视频内容简记

  1. 这是一个分割问题,其实和前面的组合是类似的,在字符串的分割中,其“划分”就是通过之前的index来进行标注

  2. 另外,关于回文串的判断

回文 串是向前和向后读都相同的字符串。

直接用双指针一个从前向后,一个从后向前遍历判断相等即可

  1. 关于substr(pos, len)函数,其pos表示起始位置,len表示从pos开始的长度为len的字符。举个例子,"abcd",我要取前面字符abc,应该用s.substr(0, 3)

梳理

  1. 确定函数的参数和返回值

  2. 确定递归的终止条件

  3. 确定单层递归的逻辑

大致代码内容

  1. void backtracking (string s, int index)

  2. 确定递归的终止条件,if (index == s.size()) result.push_back(path); return;

  3. 确定单层递归的逻辑,for (int i = index; i <= s.size(); i++),之后要紧跟一个判断,就是if (isPalindrome(s, index, i)) string str = s.substr(index, i - index + 1)之后添加path.push_back(str),否则直接return

LeetCode测试

点击查看代码
class Solution {
private:
    bool isPalindrome(string s, int start, int end) {
        int left;
        int right;
        for (left = start, right = end; left <= right; left++) {
            if (s[left] != s[right]) return false;
            right--;
        }
        return true;
    }

    vector<vector<string>> result;
    vector<string> path;
    void backtracking (string s, int index) {
        if (index == s.size()) {
            result.push_back(path);
            return;
        }
        for (int i = index; i < s.size(); i++) {
            if (isPalindrome(s, index, i)) {
                string str = s.substr(index, i - index + 1);
                path.push_back(str);
            } else continue;
            backtracking(s, i + 1);
            path.pop_back();
        }
    }

public:
    vector<vector<string>> partition(string s) {
        backtracking(s, 0);
        return result;
    }
};
posted on   bnbncch  阅读(956)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
点击右上角即可分享
微信分享提示