算法学习记录~2023.5.18~贪心Day3~134. 加油站 & 135. 分发糖果 & 860.柠檬水找零 & 406.根据身高重建队列


134. 加油站

题目链接

力扣题目链接

思路1:暴力解法

模拟从每一个加油站出发,然后再分别模拟一圈。

如果中途出现油不足以去下一站则退出,将下一个加油站设为出发地。如果都没问题则说明符合结果,返回这个出发地。

很明显就是O(n^2)的。

for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历

自己写的这个代码在leetcode上超时了

代码

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int result = -1;            //初始化结果
        for (int i = 0; i < gas.size(); i++){       //以每一个加油站为起点
            int cur = i;            //cur记录当前加油站开始的路径走到了哪里,初始化为起点
            int count = 0;          //记录每一圈走过了多少站
            int sum = 0;                //当前汽油总量
            while (count != gas.size()){  //判断环形终点
                sum += gas[cur];            //走到某一个点的汽油总量为以前的加上当前加油站的
                if (sum < cost[cur]){       //只要其中一站无法走到下一步,则本加油站为起点不满足要求,继续考虑下一个加油站为起点的环形
                    result = -2;            //此条环路走不通,设为-2来和初始化的-1做区分
                    break;
                }
                else{
                    sum -= cost[cur];   //减去到下一站烧的油
                    count ++;       //本站能去下一站就加上
                    cur ++;         //向前走了一步
                    if (cur == gas.size())  //到头了就重置为最开头
                        cur = 0;
                }
            }
            result = (result == -2) ? -1 : i;       //如果此条路走不通就返回-1,否则返回出发加油站
            if (result == i)                        //如果找到了结果直接退出
                break;
        }
        return result;
    }
};

思路2:从全局进行考虑

这个思路想了非常久,而且其实现在也无法确定是否是正确的。先把carl哥的说法贴下来


直接从全局进行贪心选择,情况如下:

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

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

情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。

前两种情况很好理解,对于情况3,目前的想法是这样的。
为什么要从后向前呢?如果想到达这个点,那么从出发点到这个点的累积油量一定要大于等于0。那么从后往前遍历其实可以理解为倒着开,从i点开到0的和是min,是负的,那么如果想要变正就还得往前开,也就是从最后开始再往前,直到这个min也就是累积油量大于等于0,这样才能跑满一圈。由于它是累积油量的min,因此如果有唯一解那一定是从这个缺口开始找到的。
但感觉也不是很对劲,不知道这么理解对不对,非常蒙

时间复杂度:O(n)
空间复杂度:O(1)

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int curSum = 0;
        int min = INT_MAX; // 从起点出发,油箱里的油量最小值
        for (int i = 0; i < gas.size(); i++) {
            int rest = gas[i] - cost[i];
            curSum += rest;
            if (curSum < min) {
                min = curSum;
            }
        }
        if (curSum < 0) return -1;  // 情况1
        if (min >= 0) return 0;     // 情况2
                                    // 情况3
        for (int i = gas.size() - 1; i >= 0; i--) {
            int rest = gas[i] - cost[i];
            min += rest;
            if (min >= 0) {
                return i;
            }
        }
        return -1;
    }
};

思路3:贪心算法(感觉也不是很算?)

如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。

每个加油站的剩余量rest[i]为gas[i] - cost[i]。

i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。
(这里是因为i处能够用掉[0,i]累积下来的所有油,且由于之前累积的油必然大于等于0,所以i之前的都不可以)

那么为什么一旦[0,i] 区间和为负数,起始位置就可以是i+1呢,i+1后面就不会出现更大的负数?

如果出现更大的负数,就是更新i,那么起始位置又变成新的i+1了。

那有没有可能 [0,i] 区间选某一个作为起点,累加到 i 这里 curSum是大于零呢?如图所示
在这里插入图片描述
如果 curSum<0 说明 区间和1 + 区间和2 < 0, 那么 假设从上图中的位置开始计数curSum不会小于0的话,就是 区间和2>0。

区间和1 + 区间和2 < 0 同时 区间和2>0,只能说明区间和1 < 0, 那么就会从假设的箭头初就开始从新选择起始位置了。

(但总觉得这个解释和上面会出现更大的负数就更新i的解释冲突?)

那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置。

(其实还是不能理解这个思路,总觉得非常奇怪)

时间复杂度:O(n)
空间复杂度:O(1)

代码

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

总结

首先是按照思路1的时候,找代码错误找了一个多小时,最后发现是“?:”的语法记出问题用错了…因为一个非常低级的错误浪费了这么久真的很崩溃,尤其是发现这样还超时…

思路2和思路3理解起来非常的吃力,这道题可能花了三小时才稍微理解了,而且感觉理解还是存在问题。这个情况来看很没信心之后遇到时候自己还能想起来。具体对于这两种思路的考虑和困难都写在上面对应思路的部分了。

不知道是不是要考虑放弃下这道题,贪心算法真的就是一个萝卜一个坑没有任何题目之间能举一反三,自信心打击器了属于是。

这道题目前可以标记为不会。


135. 分发糖果

题目链接

力扣题目链接

思路1:(自己想的)暴力枚举所有情况

将评分排序,随后从最低评分开始依次给糖果。

用一个copy数组保存原来的数组,原数组sort排序,candy数组记录分配情况,used数组判断元素是否被处理。

暴力枚举所有情况,主要有以下几种:

  1. 在中间 且 左右都有数了
    - 左右评分和中间相同 --> 设为1
    - 左相同右边低 --> 比右边多1
    - 左边低右相同 --> 比左边多1
    - 左右都低 --> 比左右两边最大的多1
  2. 在中间 且 左无右有
    - 和右边相同 --> 设为1
    - 大于右边 --> 比右边多1
  3. 在中间 且 左有右无
    - 和左边相同 --> 设为1
    - 大于左边 --> 比左边多1
  4. 在中间 且 左右都没数
    - 设为1
  5. 左边界
    - 右边没数 --> 设为1
    - 右边有数且评分小 --> 比右边多1
    - 右边有数且评分相同 --> 设为1
  6. 右边界
    - 左边没数 --> 设为1
    - 左边有数且评分小 --> 比左边多1
    - 左边有数且评分相同 --> 设为1

最后结果应该是没啥问题,但是太暴力了,n*n,超时

代码

class Solution {
public:
    int candy(vector<int>& ratings) {
        vector<int> copy(ratings);          //复制原数组
        int result = 0;
        sort(ratings.begin(), ratings.end());   //原数组进行排序
        vector<int> candy(copy.size());               //具体糖果数量
        vector<int> used(ratings.size(), 0);   //元素处理情况,0为为处理,1为已经处理
        if (ratings.size() == 1)
            return 1;
        for (int i = 0; i < ratings.size(); i++){       //从评分最低的开始
            for (int j = 0; j < copy.size(); j++){
                if (copy[j] == ratings[i] && used[j] == 0){  //找到对应元素并且判断该元素还没有处理过

                    if (j != 0 && j != copy.size() - 1 && used [j - 1] == 1 && used[j + 1] == 1){   //在中间 且 左右都有数了
                        if (copy[j] == copy[j - 1] && copy[j] == copy[j + 1])      //左中右评分相同,则中间的为1
                            candy[j] = 1;
                        else if (copy[j] == copy[j - 1] && copy[j] > copy[j + 1])
                            candy[j] = candy[j + 1] + 1;
                        else if (copy[j] > copy[j - 1] && copy[j] == copy[j + 1])
                            candy[j] = candy[j - 1] + 1;
                        else
                            candy[j] = max(candy[j - 1], candy[j + 1]) + 1;

                    }
                    else if (j != 0 && j != copy.size() - 1 && used [j - 1] == 0 && used[j + 1] == 1){    //在中间 且 左无右有
                        if (copy[j] == copy[j + 1])     //和右边相同,设为1
                            candy[j] = 1;
                        else{                           //比右边大,则比右边多1
                            candy[j] = candy[j + 1] + 1;
                        }
                    }
                    else if (j != 0 && j != copy.size() - 1 && used [j - 1] == 1 && used[j + 1] == 0){    //在中间 且 左有右无
                        if (copy[j] == copy[j - 1])     //和左边相同,设为1
                            candy[j] = 1;
                        else{                           //比左边大,则比左边多1
                            candy[j] = candy[j - 1] + 1;
                        }
                    }
                    else if (j != 0 && j != copy.size() - 1 && used [j - 1] == 0 && used[j + 1] == 0) {    //在中间 且 左右都没数
                        candy[j] = 1;
                    }
                    else if (j == 0){                   //左边界
                        if (used[j + 1] == 0){          //右边没数,设为1
                            candy[j] = 1;
                        }
                        else if(used[j + 1] == 1 && copy[j] > copy[j + 1]){     //右边有数且评分小
                            candy[j] = candy[j + 1] + 1;
                        }
                        else{           //右边有数且评分相等
                            candy[j] = 1;
                        }
                    }
                    else if (j == copy.size() - 1){     //右边界
                        if (used[j - 1] == 0){          //左边没数,设为1
                            candy[j] = 1;
                        }
                        else if(used[j - 1] == 1 && copy[j] > copy[j - 1]){     //左边有数且评分小
                            candy[j] = candy[j - 1] + 1;
                        }
                        else{           //左边有数且评分相等
                            candy[j] = 1;
                        }
                    }
                    used[j] = 1;            //该元素已经处理过,标为1
                    break;
                }
            }

        }
        for (int i = 0; i < candy.size(); i++){     //统计总数
            result += candy[i];
        }
        return result;
    }
};

思路2:贪心

分两次贪心。

这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼。

先确定右边评分大于左边的情况(也就是从前向后遍历)

此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果

局部最优可以推出全局最优。

再进行反向操作,需要注意的是反方向时也要从后向前遍历,因为由于时左边大于右边,所以前面的元素会受后边的影响。

同时当第一次贪心和第二次贪心结果冲突时,应取更大的值,这样才能同时满足两个要求

代码

class Solution {
public:
    int candy(vector<int>& ratings) {
        vector<int> candy(ratings.size(), 1);
        //从前往后,左 < 右
        for (int i = 0; i < ratings.size() - 1; i++){
            if (ratings[i] < ratings[i + 1]){
                candy[i + 1] = candy[i] + 1;
            }
        }
        //从后往前,左 > 右
        for (int i = ratings.size() - 1; i >= 1; i--){
            if (ratings[i - 1] > ratings[i]){
                candy[i - 1] = max(candy[i] + 1, candy[i - 1]);     //注意需要取两次遍历最大值
            }
        }
        int result = 0;
        for (int i = 0; i < ratings.size(); i++)
            result += candy[i];
        return result;
    }
};

总结


860.柠檬水找零

题目链接

力扣题目链接

思路

从头到尾遍历挨个处理和记录每种货币数量。

总共只有三种情况:

  1. 账单是5 --> count5加1
  2. 账单是10 --> count5减1count10加1
  3. 账单是20 --> 一张10加一张5,或者三张5

对每种情况进行代码编写,处理当前账单,在每层最后判断是否有负值,如果有则说明此次找不开了

代码

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        int count5 = 0;
        int count10 = 0;
        int count20 = 0;
        for (int i = 0; i < bills.size(); i++){
            if (bills[i] == 5){
                count5++;
            }
            else if (bills[i] == 10){
                count5--;
                count10++;
            }
            else{                   //20的
                if(count10 >= 1){       //有10块的就组合找钱
                    count10--;
                    count5--;
                    count20++;
                }
                else{               //否则找3张5块
                    count5 -= 3;
                }
            }
            if (count5 < 0 || count10 < 0)      //操作完发现数值为负则当前无法找零
                return false;
        }
        return true;
    }
};

总结


406.根据身高重建队列

题目链接

力扣题目链接

思路

和 135. 分发糖果 类似,先确定一个维度,再处理另一个维度。

首先一定要先考虑 h 维度,因为先考虑 k ,会发现并不太能找打到一个合适的规则去排序,而且也无法继续在 h 维度上进一步处理。

因此要先处理身高 h 。

处理身高 h ,一定要高的在前面,因为这样可以防止后排入人员影响先排入人员的位置。对于相同身高,则 k 越大越在后面。

接着处理 k ,由于从前往后身高递减,因此从前往后处理时,对每一个要被排入的新人员,已经处于排好队列的人身高一定 >= 当前身高,因此新排入的人员的位置索引就是这个人的 k ,因为新排入一定是最矮的,因此一定不会影响之前已经排好人员的位置。

代码1:使用vector数组

class Solution {
public:
    static bool cmp (const vector<int>& a, const vector<int>& b){
        if (a[0] == b[0])           //身高相同则 k 小的在前,k 大的在后
            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];            //取k值,这就是针对前面已排好que的下一元素的对应位置
            que.insert(que.begin() + position, people[i]);  //把身高h值插入到对应位置
        }
        return que;
    }
};

代码2:使用list链表优化性能

class Solution {
public:
    static bool cmp (const vector<int>& a, const vector<int>& b){
        if (a[0] == b[0])           //身高相同则 k 小的在前,k 大的在后
            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];            //取k值,这就是针对前面已排好que的下一元素的对应位置
            std::list<vector<int>>::iterator it = que.begin();
            while (position--){     //寻找插入位置
                it++;
            }
            que.insert(it, people[i]);
        }
        return vector<vector<int>>(que.begin(), que.end());     //转回对应格式
    }
};

总结

一开始并没想到和 135. 分发糖果 类似,没意识到其实可以同样抽象为两个维度分别处理的问题。

对于这种类型,原则是,遇到两个维度权衡的时候,一定要先确定一个维度,再确定另一个维度。

同时学习下 list 的索引之类的各种基本语法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山药泥拌饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值