代码随想录算法训练营Day25| LeetCode 134 加油站、135 分发糖果、860 柠檬水找零、406 根据身高重建队列

力扣 134 加油站

笔试遇到过,不出意外的还是没写出来。代码很简单,但是逻辑不好想:

  • 从当前候选起点 start 开始累积 gas[i] - cost[i],如果某一步累计和 curr 变成负值,就意味着从 start 到当前站点之间,任何中间点都不可能是有效的起点。
  • 如果所有站点的gas[i] - cost[i]总和(即 total)大于等于 0,那么从一个合适的候选起点出发一定走完整个环。证明的核心是:
    • 假设从候选起点 start 到终点(绕一圈回来)会出现亏损,那么在更新候选起点时已经保证了这个部分被排除。
    • 因为总和非负,所以候选起点之后的所有部分加上之前被丢弃部分的正余量能够补足亏损,从而保证整个环的连续性。

通过不断更新 start,当累积和 curr 一次次地重置时,算法实际上已经“跳过”了那些不可能作为起点的站点。所以最后的start是唯一能保证一路累计不为负的起点。最终,代码如下:

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int total = 0, curr = 0, start = 0;
        for (int i = 0; i < gas.size(); i++) {
            int diff = gas[i] - cost[i];
            curr += diff;
            total += diff;
            if (curr < 0) {
                start = i + 1;
                curr = 0;
            }
        }
        return total < 0 ? -1 : start;
    }
};

力扣 135 分发糖果

从左向右进行一遍贪心,从右向左再进行一遍。每个小孩最少获得一个糖果,所以数组内的值初始化为1

class Solution {
public:
    int candy(vector<int>& ratings) {
        int n = ratings.size();
        vector<int> arr(n, 1);
        arr[0] = 1;
        //从左向右
        for(int i = 1; i < n; i++){
            if(ratings[i] > ratings[i-1]) arr[i] = arr[i - 1] + 1;
        }
        //从右向左
        for(int i = n - 2; i >= 0; i--){
            if(ratings[i] > ratings[i + 1]){
                arr[i] = max(arr[i], arr[i + 1] + 1);
            }
        } 
        int ans = 0;
        for(auto & i : arr) ans += i;
        return ans;
    }
};

力扣 860 柠檬水找零 AC

这道题挺好想的,代码如下:

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

鉴于其实只有三种数额,用map有点大材小用了。并且记录”20”这个数额没有意义,可以优化代码如下:

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

力扣 406 根据身高重建队列

这道题也存在两个维度,因此需要进行两次贪心,第一次用比较函数,从大到小排列,第二次去考虑第二个维度将当前数值插入正确的位置。

class Solution {
public:
    static bool cmp(vector<int> a, 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) {
        vector<vector<int>> que;
        sort(people.begin(), people.end(), cmp);
        for(int i = 0; i < people.size(); i++){
            int pos = people[i][1];
            que.insert(que.begin() + pos, people[i]);
        }
        return que;
    }
};

鉴于vector 插入操作时间复杂度较高,这一步可以优化成链表,代码如下(cr. 代码随想录):

class Solution {
public:
    // 身高从大到小排(身高相同k小的站前面)
    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);c
        list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
        for (int i = 0; i < people.size(); i++) {
            int position = people[i][1]; // 插入到下标为position的位置
            std::list<vector<int>>::iterator it = que.begin();
            while (position--) { // 寻找在插入位置
                it++;
            }
            que.insert(it, people[i]);
        }
        return vector<vector<int>>(que.begin(), que.end());
    }
};

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

最后再复习一下c++中vectorlist的特性:

特性 vector list
内存布局 连续内存,具有良好的缓存局部性 非连续内存,使用双向链表实现
随机访问 O ( 1 ) O(1) O(1) 时间复杂度 O ( n ) O(n) O(n) 时间复杂度,需要顺序遍历
插入/删除操作 尾部插入高效;中间或头部插入需要移动大量元素 任意位置插入/删除操作高效(只需调整指针)
空间开销 内存连续,额外指针开销小,但可能预留额外空间 每个节点需要额外存储前后指针,内存开销较大
适用场景 需要频繁随机访问或尾部操作的场景 需要频繁在中间进行插入或删除操作的场景

你可能感兴趣的:(数据结构与算法修炼,算法,leetcode,贪心算法,c++,数据结构)