代码随想录算法训练营第25天 | 第八章 贪心算法 part03

第八章 贪心算法 Part 03

134. 加油站

本题有点难度,不太好想,推荐大家熟悉一下方法二
加油站题解

贪心算法1

思路:情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的

情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。

情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。
确实很巧妙的解法。
如果 sum < 0,说明绕一圈时,油量不够,直接返回 -1。
如果 min >= 0,说明从 0 号加油站出发,油量始终足够,可以绕一圈,直接返回 0。
如果 min < 0,需要从倒数第一个加油站往前找,找到一个新的出发点,使得从该点开始,油量始终为正,返回该加油站的索引。
一直在思考为什么从后往前遍历,后来结合chatGPT才知道,如果我们已经知道从0号加油站出发不行,那么最合理的选择就是从一个更靠后的加油站出发。在从最后一个加油站向前遍历时,如果发现到某一站时油量不再为负,意味着从这一站出发可以成功绕一圈。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int sum = 0;
        int min = 0; 
        for (int i = 0; i < gas.size(); i++) {
            int rest = gas[i] - cost[i];
            sum += rest;
            if (sum < min) {
                min = sum;
            }
        }
        if (sum < 0) return -1;  
        if (min >= 0) return 0;     
        for (int i = gas.size() - 1; i >= 0; i--) {
            int rest = gas[i] - cost[i];
            min += rest;
            if (min >= 0) {
                return i;
            }
        }
        return -1;  
    }
};

贪心算法2

思路:i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。如果之后出现更大的负数,就是更新i,那么起始位置又变成新的i+1了。

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int sum = 0;
        int totalSum = 0; 
        int start=0;
        for (int i = 0; i < gas.size(); i++) {
            int rest = gas[i] - cost[i];
            sum += rest;
            totalSum+=rest;
            if (sum < 0) {
               start = i + 1;  // 起始位置更新为i+1
                sum = 0;     
            }
        }
        if (totalSum < 0) return -1;
        return start;

    }
};

135. 分发糖果

本题涉及到一个思想,就是先处理好一边再处理另一边,不要两边想着一起兼顾,后面还会有题目用到这个思路
分发糖果题解
最开始写的代码是找到最小值,然后分别向左右遍历,大的话+1,小的话或者相等,减一或者为1,但是,运行的时候只有几个通过了,才想到最小值可能不止为一个,如果多个最小值的话,那还得一一比较那种情况值最小.所以这种思路不可以.

class Solution {
public:
    int candy(vector<int>& ratings) {
        vector<int> candy(ratings.size(), 1);
        for (int i = 1; i < ratings.size(); i++) {
            if (ratings[i] > ratings[i - 1]) candy[i] = candy[i - 1] + 1;
        }
        for (int i = ratings.size() - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1] ) {
                candy[i] = max(candy[i], candy[i + 1] + 1);
            }
        }
        int result = 0;
        for (int i = 0; i < candy.size(); i++) result += candy[i];
        return result;
    }
};

整体思路和代码都很好理解,看完以后,恍然大悟,很轻松就能写出来,最大的问题,是理解为什么第二次循环是从后向前遍历确定左孩子大于右孩子的情况(从后向前遍历)遍历顺序这里有同学可能会有疑问,为什么不能从前向后遍历呢?因为 rating[5]与rating[4]的比较 要利用上 rating[5]与rating[6]的比较结果,所以 要从后向前遍历。如果从前向后遍历,rating[5]与rating[4]的比较 就不能用上 rating[5]与rating[6]的比较结果了.看下面的图就明白了.
在这里插入图片描述

860. 柠檬水找零

本题看上去好像挺难,其实很简单,大家先尝试自己做一做 .确实难度不大,就是唬人的.
柠檬水找零题解

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        vector<int> nums(2,0);
        for(int i=0;i<bills.size();i++)
        {
            if(bills[i]==5)
            nums[0]++;
            if(bills[i]==10)
            {
                nums[1]++;
                nums[0]--;
            }
            if(bills[i]==20)
            {
                if(nums[1]>0)
                {
                    nums[1]--;
                    nums[0]--;
                }
                else
                nums[0]-=3;       
            }
            if(nums[0]<0||nums[1]<0)
            return false;
        }
        return true;
    }
};

nums[0] 和 nums[1]:

nums[0] 用来存储当前有多少张5美元。
nums[1] 用来存储当前有多少张10美元。
没必要统计20美元的,因为20美元 不能用来找零.
找零逻辑:

当收到 5 美元时,直接计数,无需找零。
当收到 10 美元时,需要找 5 美元。如果没有足够的5美元,返回 false。
当收到 20 美元时,优先使用一张 10 美元和一张 5 美元来找零,如果没有10美元,则使用三张5美元。如果两种方式都无法满足,返回 false。
不算难

406. 根据身高重建队列

本题有点难度,和分发糖果类似,不要两头兼顾,处理好一边再处理另一边
根据身高重建队列题解

看了他们讲了一大堆,表示完全不懂,
直接看过程,卧槽,一目了然,插入的过程:第一个数字相同的, 就按照第二个数字排序. 然后一步步的插入
最核心的思想: 高个子插入时,它的 k 值可以直接确定它的插入位置,因为此时队列中所有比它高的或者一样高的人都已经插好。
矮个子插入时,它的 k 值仍然准确地反映了它应该插入的具体位置,因为队列中比它高的人已经确定了他们的最终位置。
主要看两遍下面的流程即可:
插入[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]]

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b) {
        if (a[0] == b[0]) return a[1] < b[1];
        return a[0] > b[0];
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort (people.begin(), people.end(), cmp);//排好序
        vector<vector<int>> que;//按照要求定义一个队列(数组)
        for (int i = 0; i < people.size(); i++) 
        {
           int position = people[i][1];//
            que.insert(que.begin() + position, people[i]);//插入位置必须要加上que.begin()
        }
        return que;
    }
};

另外学习了静态函数的用法和功能:

静态函数

静态函数与普通函数的区别主要体现在作用域访问权限调用方式、以及与对象的关联性等方面。下面是两者的详细对比:

1. 与对象的关联性
  • 静态函数
    • 静态函数属于类本身,不与类的对象实例绑定。
    • 静态函数没有隐式的 this 指针,因为它不依赖于具体的对象实例。
    • 静态函数可以在不创建对象的情况下通过类名直接调用。如MyClass::staticFunction(); // 通过类名调用
  • 普通函数
    • 普通成员函数属于类的对象,必须通过类的对象实例来调用。
    • 普通成员函数具有一个隐式的 this 指针,指向当前对象,因而可以访问该对象的成员变量和成员函数。
    • 不能通过类名直接调用普通成员函数,必须创建对象并通过对象来调用.obj.memberFunction()
2. 访问权限
  • 静态函数

    • 静态函数只能访问类的静态成员变量静态成员函数,因为它不依赖对象实例,无法访问对象的非静态成员。
    • 静态函数不能访问非静态成员函数或成员变量。
  • 普通函数

    • 普通成员函数可以访问类的所有成员变量和成员函数,包括静态成员非静态成员,因为普通函数通过对象调用,隐含了对象实例的 this 指针。
3. 函数内部的行为
  • 静态函数

    • 静态函数没有 this 指针,不能使用 this 关键字。
    • 静态函数通常用于实现与对象状态无关的功能,比如工具类函数或帮助函数。
  • 普通函数

    • 普通成员函数有隐式的 this 指针,可以使用 this 关键字访问当前对象的成员。
    • 普通函数用于操作或处理当前对象的成员变量和成员函数。
总结
特性静态函数普通函数
与对象的关联属于类本身,与对象无关属于对象实例,必须通过对象调用
访问权限只能访问静态成员可以访问静态和非静态成员
调用方式通过类名或对象调用(推荐类名)只能通过对象调用
是否有 this没有 this 指针隐式传递 this 指针
典型应用场景工具类函数、计数器、比较函数操作对象的成员和状态

又学习了一点知识,真棒,总结下,静态函数与类没关系,普通函数与类有关系. 因为静态函数没有 this 指针,所以用起来更节省资源.

链表法

C++中vector(可以理解是一个动态数组,底层是普通数组实现的)如果插入元素大于预先普通数组大小,vector底部会有一个扩容的操作,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。

所以使用vector(动态数组)来insert,是费时的,插入再拷贝的话,单纯一个插入的操作就是O(n^2)了,甚至可能拷贝好几次,就不止O(n^2)了。

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b) {
        if (a[0] == b[0]) return a[1] < b[1];
        return a[0] > b[0];
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort (people.begin(), people.end(), cmp);//排好序
        list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
        for (int i = 0; i < people.size(); i++) 
        {
           int position = people[i][1];//
           list<vector<int>>::iterator it = que.begin();
           while (position--) { // 寻找在插入位置
                it++;
            }
            que.insert(it, people[i]);
            
        }
        return  vector<vector<int>>(que.begin(), que.end());
    }
};

挺好,又复习了下链表.

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第14训练营中,讲解了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论中还分享了相关的博客文章和配图,帮助学员更好地理解和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15的讨论中,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16的讨论中,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地解决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值