算法学习|贪心算法

该博客主要围绕贪心算法展开学习。先介绍贪心算法是局部最优导向全局最优,可用数学归纳法证明。接着给出多道习题,如分发饼干、摆动序列等,博主分享自己的解题思路,部分思路超时,同时也记录了代码随想录里的贪心思路,包括排序、记录覆盖范围等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

学习材料声明

所有知识点都来自互联网,进行总结和梳理,侵权必删。
引用来源:基本参考代码随想录的刷题顺序和讲解。

理论基础

贪心是什么?

局部最优导向全局最优。
但是,没有套路。
如何证明?数学归纳法。

习题

1|455. 分发饼干

我的思路就是先排序,最小胃口的先满足。

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int sum=0;
        int i=0, j=0;
        for(; i<g.size(); i++){
            for(; j<s.size(); j++){
                if(g[i] <= s[j]){
                    sum++;
                    s[j] = 0;
                    break;
                }
            }
        }
        return sum;
    }
};

而代码随想录的一些文字我没看懂。

2|376. 摆动序列

想不到。
这题的解法图好有趣。
代码随想录
代码随想录
贪心解法(不是很理解。)

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if((nums.size()==1)||(nums.size()==2 && nums[0]==nums[2])){
            return 1;
        }
        if(nums.size()==2 && nums[0]!=nums[2]){
            return 2;
        }
        int curDiff=0, preDiff=0, ans=1;
        for(int i=0; i<nums.size()-1; i++){
            curDiff = nums[i+1] - nums[i];
            if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) {
                ans++;
                preDiff = curDiff; 
            }
        }
        return ans;

    }
};

3|53. 最大子数组和

这会让我想到动态规划。动态规划代码如下:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        int dp[n+1];
        memset(dp, 0, sizeof(dp));
        dp[0] = nums[0];
        int ma=dp[0];
        for(int i=1; i<n; i++){
            dp[i] = max(nums[i], dp[i-1]+nums[i]);
            if(ma<dp[i]){
                ma = dp[i];
            }
        }
        for(int i=0; i<n; i++){
            cout<<dp[i]<<" ";
        }
        return ma;

    }
};

贪心思路:记录count,如果count值为负就立马归零。因为这样肯定会拉低总和。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        //贪心算法
        int result = INT32_MIN;
        int count = 0;
        for (int i = 0; i < nums.size(); i++) {
            count += nums[i];
            if (count > result) { // 取区间累计的最大值(相当于不断确定最大子序终止位置)
                result = count;
            }
            if (count <= 0) count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
        }
        return result;
    }
};

----------------------------------------------------------------------------2023年10月29日----------------------------------------------------------

4|122. 买卖股票的最佳时机 II

这不就是动态规划的状态转移嘛?嘿嘿,让我大展拳脚的时候到了。艾府!!!

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        int dp[n+1][2];
        memset(dp, 0, sizeof(dp));
        dp[0][0] = 0;//第一天未持有
        dp[0][1] = -prices[0];//第一天持有
        for(int i=1; i<n; i++){
            dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]);
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]);
        }
        return max(dp[n-1][0], dp[n-1][1]);

    }
};

贪心思路:贪心的思路真的好难想。利润拆解:
来自代码随想录

class Solution {
    public int maxProfit(int[] prices) {
        int ans = 0;
        int n = prices.length;
        for(int i=1; i<n; i++){
            ans += Math.max(prices[i]-prices[i-1], 0);
        }
        return ans;
    }
}

5|55. 跳跃游戏

第一想法就是回溯。哇,我是不是脑子不灵光,想不到贪心的规则。超时了。

class Solution {
    boolean flag=false;
    int n;
    public void backTracing(int[] nums, int startIndex){
        if(startIndex == n-1){
            flag = true;
            return;
        }
        for(int i=startIndex+1; i<=nums[startIndex]+startIndex&&i<n; i++){
            System.out.print(startIndex + " " + i + "\n");
            if(flag){
                break;
            }
            backTracing(nums, i);
        }
    }
    public boolean canJump(int[] nums) {
        n = nums.length;
        backTracing(nums, 0);
        return flag;
    }
}

贪心思路:记录覆盖范围。
来自代码随想录

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int cover = 0;
        if (nums.size() == 1) return true; // 只有一个元素,就是能达到
        for (int i = 0; i <= cover; i++) { // 注意这里是小于等于cover
            cover = max(i + nums[i], cover);
            cout<<cover<<" ";
            if (cover >= nums.size() - 1) return true; // 说明可以覆盖到终点了
        }
        return false;
    }
};

6|45. 跳跃游戏 II

我觉得这很像动态规划。依旧超时。

class Solution {
public:
    int jump(vector<int>& nums) {
        int n = nums.size();
        int dp[n+1];
        memset(dp,  10001, sizeof(dp));
        dp[0] = 0;
        for(int i=0; i<n; i++){
            for(int j=i+1; j<n&&j<=i+nums[i]; j++){
                dp[j] = min(dp[i]+1, dp[j]);
                for(int i=0; i<n; i++){
                    cout<<dp[i]<<" ";
                }
                cout<<endl;
            }
        }
        
        return dp[n-1];
    }
};

贪心算法,依旧是覆盖范围,不过是第几步可覆盖范围。
来自代码随想录

//代码来自代码随想录
class Solution {
public:
    int jump(vector<int>& nums) {
        if (nums.size() == 1) return 0;
        int curDistance = 0;    // 当前覆盖最远距离下标
        int ans = 0;            // 记录走的最大步数
        int nextDistance = 0;   // 下一步覆盖最远距离下标
        for (int i = 0; i < nums.size(); i++) {
            nextDistance = max(nums[i] + i, nextDistance);  // 更新下一步覆盖最远距离下标
            if (i == curDistance) {                         // 遇到当前覆盖最远距离下标
                ans++;                                  // 需要走下一步
                curDistance = nextDistance;             // 更新当前覆盖最远距离下标(相当于加油了)
                if (nextDistance >= nums.size() - 1) break;  // 当前覆盖最远距到达集合终点,不用做ans++操作了,直接结束
            }
        }
        return ans;
    }
};

----------------------------------------------------------------------------2023年10月30日----------------------------------------------------------

7|1005. K 次取反后最大化的数组和

每次都取数组里最小的进行取反。O(knlogn)

class Solution {
public:
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        for(int i=1; i<=k; i++){
            sort(nums.begin(), nums.end());
            nums[0] = -1*nums[0];
        }
        int sum=0;
        for(int i=0; i<nums.size(); i++){
            sum += nums[i];
        }
        return sum;

    }
};

真正的贪心思路,负数变正,正数最小变负(来回变换)。

class Solution {
static bool cmp(int a, int b) {
    return abs(a) > abs(b);
}
public:
    int largestSumAfterKNegations(vector<int>& A, int K) {
        sort(A.begin(), A.end(), cmp);       // 第一步,从大到小
        for (int i = 0; i < A.size(); i++) { // 第二步,负数变正
            if (A[i] < 0 && K > 0) {
                A[i] *= -1;
                K--;
            }
        }
        if (K % 2 == 1) A[A.size() - 1] *= -1; // 第三步
        int result = 0;
        for (int a : A) result += a;        // 第四步
        return result;
    }
};

8|134. 加油站

思路超时了。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int n=gas.size(), curgas=0, maxgasIdx=0, sumgas, sumcost;
        int flag[n], ans=-1;
        for(int i=0; i<n; i++){
            if(gas[i]<cost[i]){
                continue;
            }
            curgas = 0;
            ans = i;
            for(int j=0; j<n; j++){
                if(curgas+gas[(i+j)%n]==cost[(i+j)%n]){
                    curgas = 0;
                    continue;
                }
                if(curgas+gas[(i+j)%n]<cost[(i+j)%n]){
                    ans = -1;
                    break;
                }
                curgas += (gas[(i+j)%n]-cost[(i+j)%n]);
            }
            if(ans!=-1){
                break;
            }
        }
        return ans;

    }
};

来自代码随想录

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int curSum = 0;
        int totalSum = 0;
        int start = 0;
        for (int i = 0; i < gas.size(); i++) {
            curSum += gas[i] - cost[i];
            totalSum += gas[i] - cost[i];
            if (curSum < 0) {   // 当前累加rest[i]和 curSum一旦小于0
                start = i + 1;  // 起始位置更新为i+1
                curSum = 0;     // curSum从0开始
            }
        }
        if (totalSum < 0) return -1; // 说明怎么走都不可能跑一圈了
        return start;
    }
};

9|135. 分发糖果

思路:进行分发,直至分发数满足要求,但是超时。

class Solution {
public:
    int candy(vector<int>& ratings) {
        int ans=2, sum=ratings.size(), n=ratings.size();
        int candyN[n+1];
        memset(candyN, 0, sizeof(candyN));
        while(1){
            for(int i=0; i<n-1; i++){
                if(ratings[i]<ratings[i+1]){
                    if(candyN[i+1]<=candyN[i]){
                        sum += (candyN[i] + 1 - candyN[i+1]);
                        candyN[i+1]=candyN[i]+1;                  
                    }              
                }
                if(ratings[i]>ratings[i+1]){
                    if(candyN[i+1]>=candyN[i]){
                        sum += (candyN[i+1] + 1 - candyN[i]);
                        candyN[i]=candyN[i+1]+1;                  
                    }
                } 
            } 
            if(ans==sum){
                break;
            }
            ans = sum;
        }
    return ans;
    }
};

贪心思路,从前往后一遍,从后往前一遍就好了。满足比左边大的多拿,满足比右边大的多拿,那么就满足比两边大的多拿。

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;
    }
};

----------------------------------------------------------------------------2023年10月31日----------------------------------------------------------

10|435. 无重叠区间

想着学习上一篇左不重叠,右不重叠。没想到,超时了。

class Solution {
public:
    static bool cmp(vector<int> a, vector<int> b){
        return a[0]<b[0];
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end(), cmp);
        int n = intervals.size();
        int sum = 0;
        for(int i=1; i<intervals.size(); i++){
            if(intervals[i][0]<intervals[i-1][0]){
                intervals.erase(intervals.begin()+i);
                i=i-1;
            }else if(intervals[i][0]==intervals[i-1][0]){
                if(intervals[i][1]>intervals[i-1][1]){
                    intervals.erase(intervals.begin()+i);
                    i=i-1;
                }else{
                   intervals.erase(intervals.begin()+i-1);
                    i=i-1; 
                }

            }   
        }        
        for(int j=1; j<intervals.size(); j++){
            if(intervals[j][0]<intervals[j-1][1]){
               if(intervals[j][1]>intervals[j-1][1]){
                    intervals.erase(intervals.begin()+j);
                    j=j-1;
                }else{
                   intervals.erase(intervals.begin()+j-1);
                    j=j-1; 
                }
            }       
        }
        return n-intervals.size();

    }
};

其实完全没有必要左右。尽管还是超时。

class Solution {
public:
    static bool cmp(vector<int> a, vector<int> b){
        return a[0]<b[0];
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end(), cmp);
        int n = intervals.size();
        int sum = 0;
        for(int i=1; i<intervals.size(); i++){
            if(intervals[i][0]<intervals[i-1][1]){
               if(intervals[i][1]>intervals[i-1][1]){
                    intervals.erase(intervals.begin()+i);
                    i=i-1;
                }else{
                   intervals.erase(intervals.begin()+i-1);
                    i=i-1; 
                }
            }      
        }        
        return n-intervals.size();

    }
};

代码随想录的方法,记录分割点。

class Solution {
public:
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[0] < b[0]; // 改为左边界排序
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.size() == 0) return 0;
        sort(intervals.begin(), intervals.end(), cmp);
        int count = 0; // 注意这里从0开始,因为是记录重叠区间
        int end = intervals[0][1]; // 记录区间分割点
        for (int i = 1; i < intervals.size(); i++) {   
            if (intervals[i][0] >= end)  end = intervals[i][1]; // 无重叠的情况
            else { // 重叠情况 
                end = min(end, intervals[i][1]);
                count++;
            }
        }
        return count;
    }
};

11|763. 划分字母区间

完全想不到怎么做。
仔细想想,还是能找到区间重叠的影子的。统计每一个字母出现的起始位置和结束位置,作为一个区间。那么求这样多个区间里,重叠覆盖之后的区间长度。

class Solution {
public:
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[0] < b[0]; // 改为左边界排序
    }
    vector<int> partitionLabels(string s) {
        vector<int> ans;
        int n = s.length();
        int labels[26][2];
        memset(labels, -1, sizeof(labels));
        for(int i=0; i<n; i++){
            if(labels[s[i]-'a'][0] == -1){
                labels[s[i]-'a'][0]=i; 
            }
            labels[s[i]-'a'][1] = i;
        }
        vector<vector<int>> nums;
        vector<int> temp;
        for(int i=0, j=0; i<26; i++){
            if(labels[i][0]!=-1){
                temp.push_back(labels[i][0]);
                temp.push_back(labels[i][1]);
                nums.push_back(temp);
                temp.clear();
                j++;
            }
        }
        sort(nums.begin(), nums.end(), cmp);
        int mi=nums[0][0], ma=nums[0][1];
        for(int i=0; i<nums.size(); i++){
            if(nums[i][0]<mi&&nums[i][1]<ma&&nums[i][1]>mi){
                mi=nums[i][0];
            }
            if(nums[i][1]>ma&&nums[i][0]>mi&&nums[i][0]<ma){
                ma=nums[i][1];
            }
            if(nums[i][0]>ma){
                ans.push_back(ma-mi+1);
                mi=nums[i][0];
                ma=nums[i][1];
            }
        }
        ans.push_back(ma-mi+1);
        return  ans;

    }
};

代码随想录里的方法:
来自代码随想录

class Solution {
public:
    vector<int> partitionLabels(string S) {
        int hash[27] = {0}; // i为字符,hash[i]为字符出现的最后位置
        for (int i = 0; i < S.size(); i++) { // 统计每一个字符最后出现的位置
            hash[S[i] - 'a'] = i;
        }
        vector<int> result;
        int left = 0;
        int right = 0;
        for (int i = 0; i < S.size(); i++) {
            right = max(right, hash[S[i] - 'a']); // 找到字符出现的最远边界
            if (i == right) {
                result.push_back(right - left + 1);
                left = i + 1;
            }
        }
        return result;
    }
};

12|56. 合并区间

这一题就是上一题解法的一部分。。

class Solution {
public:
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[0] < b[0]; // 改为左边界排序
    }
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<int> temp;
        vector<vector<int>> ans;
        sort(intervals.begin(), intervals.end(), cmp);
        int mi=intervals[0][0], ma=intervals[0][1];
        for(int i=0; i<intervals.size(); i++){
            if(intervals[i][0]<mi&&intervals[i][1]<=ma&&intervals[i][1]>=mi){
                mi=intervals[i][0];
            }
            if(intervals[i][1]>ma&&intervals[i][0]>=mi&&intervals[i][0]<=ma){
                ma=intervals[i][1];
            }
            if(intervals[i][0]>ma){
                temp.push_back(mi);
                temp.push_back(ma);
                ans.push_back(temp);
                temp.clear();
                mi=intervals[i][0];
                ma=intervals[i][1];
            }
        }
        temp.push_back(mi);
        temp.push_back(ma);
        ans.push_back(temp);
        return  ans;
    }
};

13|452. 用最少数量的箭引爆气球

看起来与前两题是同样的方法。但还是有所不同,这个算是求交集,上述两题求并集。

class Solution {
public:
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[0] < b[0]; // 改为左边界排序
    }
    int findMinArrowShots(vector<vector<int>>& points) {
        vector<int> temp;
        vector<vector<int>> ans;
        sort(points.begin(), points.end(), cmp);
        int mi=points[0][0], ma=points[0][1], res=0;
        for(int i=0; i<points.size(); i++){
            if(points[i][0]<=ma){
                mi=points[i][0];
                ma=min(points[i][1], ma);
            }else{
                res++;
                mi=points[i][0];
                ma=points[i][1];
            }
        }
        res++;
        return  res;
    }
};

----------------------------------------------------------------------------2023年11月01日----------------------------------------------------------

13|738.单调递增的数字

根据错误不断调试的结果。

class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        vector<int> nums;
        int temp=n;
        int res=0;
        if(n==0){
            return 0;
        }
        while(temp){
            nums.push_back(temp%10);
            temp /= 10;
        }
        for(int i=nums.size()-1; i>=0; i--){
            cout<<nums[i]<<" ";
        }
        cout<<endl;
        if(nums.size()==1){
            return n;
        }
        for(int i=0, j=0; i<nums.size()-1; i++){
            if(nums[i]<nums[i+1]){
                nums[i]=9;
                nums[i+1] = (nums[i+1]-1)%10;
                j=i-1;
                while(j>=0){
                    if(nums[j]!=9){
                        nums[j] = 9;
                    }else{
                        break;
                    }
                    j--;
                }
            }
        }
        for(int i=nums.size()-1; i>=0; i--){
            cout<<nums[i]<<" ";
            res += (nums[i]*pow(10, i));
        }
        cout<<endl;
        return res;

    }
};

代码随想录的方法是一旦一个数变成9,之后的所有数字都要变成9。那么就记录最后一个变成9的位置就好。

class Solution {
public:
    int monotoneIncreasingDigits(int N) {
        string strNum = to_string(N);
        // flag用来标记赋值9从哪里开始
        // 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行
        int flag = strNum.size();
        for (int i = strNum.size() - 1; i > 0; i--) {
            if (strNum[i - 1] > strNum[i] ) {
                flag = i;
                strNum[i - 1]--;
            }
        }
        for (int i = flag; i < strNum.size(); i++) {
            strNum[i] = '9';
        }
        return stoi(strNum);
    }
};

14|968.监控二叉树

----------------------------------------------------------------------------2023年11月02日----------------------------------------------------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值