Leetcode刷题笔记-回溯|贪心

按照youngyangyang04总结的Leetcode刷题攻略进行整理,链接https://github.com/youngyangyang04/leetcode-master


目录



回溯

关键字:组合|分割|子集|排列|去重

1. 组合问题

题目:组合
https://leetcode-cn.com/problems/combinations/

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合,这里引入回溯法的模板:
两个变量,一个存最终的所有结果合集,一个存结果合集中的每一个结果(path)。回溯也是一种递归,和递归的模板类似,实现回溯法的函数backtracking中也先考虑终止条件,本题的终止题目是当单个结果的规模达到k时,认为已经找到一个解,那么把它放进result里,函数return。下面一个循环注意要从startIndex开始,每个循环内部先处理节点,再进入下一轮回溯,再撤销当前处理的节点的结果

class Solution {
private:
    vector<vector<int>> result; 		// 存放符合条件结果的集合
    vector<int> path; 							// 用来存放符合条件结果
    void backtracking(int n, int k, int startIndex) {
        if (path.size() == k) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i <= n; i++) {
            path.push_back(i); // 处理节点 
            backtracking(n, k, i + 1); // 递归
            path.pop_back(); // 回溯,撤销处理的节点
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        result.clear(); // 可以不写
        path.clear();   // 可以不写
        backtracking(n, k, 1);
        return result;
    }
};

剪枝策略:有些过程是多余的,比如从某些位置开始继续向下取数,剩下的数是不够填充一条path的,那么就没有必要去进行这些情况,比如n=4,k=4时:
在这里插入图片描述
可以发现,当i + k - path.size() >= n时的取数是没有意义的,i是起始位置,k - path.size()是当前还差几个数需要找,如果加上去已经超过了最大索引位置(n),所以需要保证i + k - path.size() < n,即i < n - (k - path.size()),可以把这个条件加进循环的判断条件中,进行剪枝。


2. 求组合总和

题目:组合总和III
https://leetcode-cn.com/problems/combinations/

从1-9中找到和为n的k个数的组合,加一个判断条件判断单个path元素总和是否==n即可,符合条件才会加入result中。


3. 多个集合求组合

题目:电话号码的字母组合
https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/

注意回溯过程写成二重循环

    void backtracking(int di, string digits) {
        if (single.size() == digits.size()) {
            ans.push_back(single);
            return;
        }
        for (int i = di; i < digits.size(); i++) {
            for (int j = 0; j < rec[digits[i] - '1'].size(); j++) {
                single += rec[digits[i] - '1'][j];
                backtracking(i+1, digits);
                single.pop_back();
            }
        }
    }

4. 求组合总和(数组无重复元素)

题目:组合总和
https://leetcode-cn.com/problems/combination-sum/

给定无重复元素的数组,从数组中找到所有和为target的组合,注意可以多次选取同一个数。
注意:

  1. 可以重复选取元素,那么回溯过程的startIndex不需要自增。自增是不重复选取。
  2. 注意结束条件是sum>target
  3. sum结果可以复用
class Solution {
public:
    vector<int> single;
    vector<vector<int>> res;

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backTracking(candidates, target, 0, 0);
        return res;
    }

    void backTracking(vector<int>& candidates, int target, int sum, int si) {
        if (sum > target) return;
        if (sum == target) res.push_back(single);
        for (int i = si; i < candidates.size(); i++) {
            sum += candidates[i];
            single.push_back(candidates[i]);
            backTracking(candidates, target, sum, i);
            single.pop_back();
            sum -= candidates[i];
        }        
    }
};

5. 求组合总和(数组有重复元素,要求解集无重复)

题目:组合总和II
https://leetcode-cn.com/problems/combination-sum-ii/

这里总结一下求总和为目标值的几种类型题:
两数之和 - 哈希表实现
四个独立集合各自取数 - 哈希表实现,因为不需要去重
单集合取无重复三数/四数之和 - 双指针实现,哈希表难以实现去重
不限个数的组合,不需去重 - 回溯
不限个数的组合,需要去重 - 回溯,注意要先排序,让相等的元素挨在一起,并且增加used数组辅助,注意观察一下下面代码里used出现的几个位置和用法(说实话挺难懂的,我就背吧)

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
        if (sum == target) {
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            // used[i - 1] == true,说明同一树支candidates[i - 1]使用过
            // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
            // 要对同一树层使用过的元素进行跳过
            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, sum, i + 1, used); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次
            used[i] = false;
            sum -= candidates[i];
            path.pop_back();
        }
    }

public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<bool> used(candidates.size(), false);
        path.clear();
        result.clear();
        // 首先把给candidates排序,让其相同的元素都挨在一起。
        sort(candidates.begin(), candidates.end());
        backtracking(candidates, target, 0, 0, used);
        return result;
    }
};

6. 分割问题

题目:分割回文串
https://leetcode-cn.com/problems/palindrome-partitioning/

用startIndex表示起始位置,i表示截取到的位置,对startIndex到i之间的部分检查是否是回文串,若是则继续进行下一步分割,位置从i+1开始,进入递归。


7. 求所有子集(无重复元素)

题目: 子集
https://leetcode-cn.com/problems/subsets/

和求固定数量元素的组合不同的是,本题是求所有子集,所以需要把所有path都放进result中,故这个push_back的过程没有判断条件。注意终止条件是startIndex >= nums.size()

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex) {
        result.push_back(path); // 收集子集
        if (startIndex >= nums.size()) { // 终止条件可以不加
            return;
        }
        for (int i = startIndex; i < nums.size(); i++) {
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }
};


8. 求所有子集(含重复元素,要求结果无重复)

题目: 子集II
https://leetcode-cn.com/problems/subsets-ii/

本题和7的区别同5和4的区别,需要排序后借助used数组。


9. 排列(无重复元素)

题目:全排列
https://leetcode-cn.com/problems/permutations/

排列问题与组合问题的不同:

  • 每层都是从0开始搜索而不是startIndex
  • 终止条件为单path的规模达到整个数组的规模,即出现了一个完整的排列
  • 需要used数组记录path里都放了哪些元素了,也是需要记一下used数组是怎么用的
class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking (vector<int>& nums, vector<bool>& used) {
        // 此时说明找到了一组
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (used[i] == true) continue; // path里已经收录的元素,直接跳过
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        result.clear();
        path.clear();
        vector<bool> used(nums.size(), false);
        backtracking(nums, used);
        return result;
    }
};

10. 排列(含重复元素,要求结果无重复)

题目:全排列II
https://leetcode-cn.com/problems/permutations-ii/

排列问题本身需要用used来记录已收录的元素避免重复,而去重操作需要排序+used数组辅助,本题是二者的结合。


贪心

「贪心的本质是选择每一阶段的局部最优,从而达到全局最优」

1. 分发饼干

题目:分发饼干
https://leetcode-cn.com/problems/assign-cookies/

可以尝试使用贪心策略,先将饼干数组和小孩数组排序。然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。


2. 摆动序列

题目:摆动序列
https://leetcode-cn.com/problems/wiggle-subsequence/

让峰值尽可能的保持峰值,然后删除单一坡度上的节点。实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)。


3. 最大子序和

题目:最大子序和
https://leetcode-cn.com/problems/maximum-subarray/

局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。


4. 买卖股票(只允许单次买卖/允许多次买入卖出)

题目:买卖股票的最佳时机
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/
题目:买卖股票的最佳时机II
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/

“买卖股票的最佳时机II”是允许多次买入卖出的,可以用贪心算法,计算每两天之间的利润,只收集正利润即可。“买卖股票的最佳时机”则只允许单次的买卖,可以计算一个最小值数组,计算到该位置所经历过的最小值,那么用该位置的值减去当前经历的最小值就是在该位置抛售所可能得到的最大收益,对每一个位置计算一下收益,取最大,即为整体最大可能的收益。


5. 跳跃游戏(求是否能跳到终点&最短步数)

题目:跳跃游戏
https://leetcode-cn.com/problems/jump-game/
题目:跳跃游戏II
https://leetcode-cn.com/problems/jump-game-ii/

第一题:其实跳几步无所谓,关键在于可跳的覆盖范围!不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。如果范围已经覆盖了整个数组,那么说明一定有办法跳到数组的终点。
第二题:如果移动下标达到了当前这一步的最大覆盖最远距离了,还没有到终点的话,那么就必须再走一步来增加覆盖范围,直到覆盖范围覆盖了终点。
在这里插入图片描述

class Solution {
public:
    int jump(vector<int>& nums) {
        int ans = 0;
        int cover = 0;
        int cur = 0;
        for (int i = 0; i < nums.size(); i++) {
            // 这是由该位置继续走下一步能够覆盖的范围
            cover = max(cover, i + nums[i]);
            if (i == cur) {
                if (i == nums.size() - 1) break;
                else {
                    ans++;
                    cur = cover;
                }
           }
        }
        return ans;
    }
};

6. 加油站

题目:加油站
https://leetcode-cn.com/problems/gas-station/

left[i] = gas[i] - cost[i],sum(left)为非负就可以跑下来。同时,计算从起点到当前位置的left之和,如果突然从正值变为负值:
比如:“ 7, 3, -12, 1, 2”,第0-2个元素之和为负,那么说明从索引0出发是不可行的,那么从索引1出发可行吗?同样不可行,因为在计算过程中已经保证前面的计算和是正的了,寻找的是这个由正转负的过程,所以去掉前面不论多长的子序列,也就是从中间哪个位置出发,都不可行。那么这时应该把起点放在第3个元素位置,继续如上的检查。


7. 二维排序(分解维度,否则顾此失彼)***

题目:分发糖果*****
https://leetcode-cn.com/problems/candy/

先确定右边评分大于左边的情况(也就是从前向后遍历),只要右边评分比左边大,右边的孩子就多一个糖果。再确定左孩子大于右孩子的情况(从后向前遍历)。如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。
那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量即大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。
所以就取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,「candyVec[i]只有取最大的才能既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多」。
简单来说,正序来一遍,倒序来一遍,倒序的时候要保证分配结果不小于正序的结果。

class Solution {
public:
    int candy(vector<int>& ratings) {
        vector<int> candyVec(ratings.size(), 1);
        // 从前向后
        for (int i = 1; i < ratings.size(); i++) {
            if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1;
        }
        // 从后向前
        for (int i = ratings.size() - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1] ) {
                candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1);
            }
        }
        // 统计结果
        int result = 0;
        for (int i = 0; i < candyVec.size(); i++) result += candyVec[i];
        return result;
    }
};
题目:根据身高重建队列*****
https://leetcode-cn.com/problems/queue-reconstruction-by-height/

要求每个人前面正好有k个身高>=他的身高的人。
先按照身高升序排序,相同身高则按k升序排序。
排序完的people:
[[7,0], [7,1], [6,1], [5,0], [5,2],[4,4]]

插入的过程:插入[7,0]:[[7,0]]
插入[7,1]:[[7,0],[7,1]]
插入[6,1]:[[7,0],[6,1],[7,1]]
插入[5,0]:[[5,0],[7,0],[6,1],[7,1]]
插入[5,2]:[[5,0],[7,0],[5,2],[6,1],[7,1]]
插入[4,4]:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]


8. 找零问题

题目:柠檬水找零
https://mp.weixin.qq.com/s/0kT4P-hzY7H6Ae0kjQqnZg

收到20元时优先找10元+5元,如果10不够再消耗3个5元。


9. 射坐标系上的气球

题目: 用最少数量的箭引爆气球
https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/

注意求交集的写法,所有重叠即有交集的区间用一根箭射完。找到不相交的区间了计数加1。

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if (points.empty()) return 0;
        sort(points.begin(), points.end()); // x, y升序
        int res = 1, L = points[0][0], R = points[0][1];
        for (int i = 1; i < points.size(); i++) {
            // 有交集
            if (points[i][0] <= R) {
                L = points[i][0];
                R = min(R, points[i][1]);
                continue;
            } else {
                res++;
                L = points[i][0];
                R = points[i][1];
            }
        }
        return res;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值