代码随想录——贪心算法

(一)分发饼干

这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩

可以尝试使用贪心策略,先将饼干数组和小孩数组排序。

然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。

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

(二)摆动序列

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int result = 1;
        int flag = 0; //记录摆动情况,0是第一个,1代表前一个升序,2代表前一个降序
        int qian = nums[0];
        if(nums.size() == 1){
            return result;
        }
        for(int i = 1; i < nums.size(); i ++){
            if((flag == 0 || flag == 2) && nums[i] > qian){
                flag = 1;
                qian = nums[i];
                result ++;
            }else if((flag == 0 || flag == 1) && nums[i] < qian){
                flag = 2;
                qian = nums[i];
                result ++;
            }else{
                // 注意这里还是要更新qian,因为如果一直上升,或一直下降,要取峰顶和谷底,这样可以保留最多的元素
                qian = nums[i];
            }
        }
        return result;
    }
};

(三)最大子数组和

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int result = nums[0];
        int count = 0;
        for(int i = 0; i < nums.size(); i++){
            count += nums[i];          
            if(count > result){
                // 注意这里要先给result赋值,针对数组大小为1,且其元素小于0的情况
                result = count; //记录当前最好结果
            }
            if(count <= 0){
                count = 0; //当count小于0,对后面为负影响
            }
        }
        return result;
    }
};

(四)买卖股票的最佳时机

在这里插入图片描述

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

(五)跳跃游戏

转换为跳跃的覆盖范围是否可以覆盖到终点。

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int cover = 0;
        //注意这里可以遍历到的元素为当前最远可以覆盖到的元素
        for(int i = 0; i <= cover; i++){
           //更新cover值为当前可以覆盖到的最大元素
            cover = max(cover, i + nums[i]);
            if(cover >= (nums.size() - 1)){
                return true;
            }
        }
        return false;
    }
};

(六)跳跃游戏II

用最少的步数,增加覆盖范围,直到覆盖到终点为止,将步数输出。

if(num.size == 1) return 0;
int cur = 0; // 当前覆盖范围
int next = 0; // 下一步覆盖范围
int result = 0;
for(i = 0; i < nums.size(); i ++){
  //记录在当前覆盖范围内下一步能跳的最远的距离
  next = max(next, i + nums[i]);
  if(i == cur){//走到了当前覆盖范围的末位
    if(cur != nums.size() - 1){ //不是数组的终点
      result ++;
      cur = next;
      if(cur >= nums.size() - 1) break; //更新后当前覆盖范围已经覆盖全部范围
    }else break; //当前覆盖范围已经到终点了
  }
}
return result;
class Solution {
public:
    int jump(vector<int>& nums) {
        if(nums.size() == 1) return 0; 
        int cur = 0;//当前最大cover
        int next = 0; //下一个最大cover
        int result = 0;
        for(int i = 0; i <= nums.size(); i++){
            next = max(next, i + nums[i]);
            if(i == cur){               
                if(cur >= nums.size() - 1){
                    break;
                }else{
                    result ++;
                    cur = next;
                    if(cur >= nums.size() - 1) break;
                }
            }
        }
        return result;
    }
};

(七)K次取反后最大化的数组和

  • 第一步:将数组按照绝对值大小从大到小排序

  • 第二步:从前向后遍历,遇到负数将其变为正数,k–

  • 第三步:如果K还大于0,且K为奇数(为偶数同一个数翻两次值不变),则反复反转数值最小的元素,将K用完。

  • 第四步:求和

class Solution {
static bool cmp(int a, int b){
    return abs(a) > abs(b); 
}
public:
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        sort(nums.begin(), nums.end(), cmp);
        int result = 0;
        for(int i = 0; i < nums.size(); i++){
            if(nums[i] < 0 && k > 0){
                nums[i] = -nums[i];
                k --;
            }
        }
        if(k % 2 == 1) nums[nums.size() - 1] *= -1;
        for(int i = 0; i < nums.size(); i++){
            result += nums[i];
        }
        return result;
    }
};

(八)加油站

  1. 暴力法:用for循环模拟假设从第i个加油站出发,用while循环模拟环形遍历过程

(超时)

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        for(int i = 0; i < cost.size(); i ++){ //从第i个节点开始走
            int rest =  gas[i] - cost[i];
            int index =  (i + 1)%(cost.size());
            while(index != i && rest > 0){
                rest += gas[index] - cost[index];
                index = (index + 1)%cost.size();
            }
            if(rest >= 0 && index == i){
                return i;
            }
        }
        return -1;
    }
};

  1. 贪心

计算补充 → 消耗, 对邮箱的净增

用currentSum累加每个站点剩余的油,如果净增累加(currentSum)起来小于0,就从当前站点的下一个站点开始继续向后遍历,则可能跑完全程(缩小范围,因为从该点前面开始肯定不能跑完全程了)

curSum = 0; //当前消耗,如果当前消耗为负,则从下一个开始(i+1)
totalSum = 0; //整体消耗,如果整体消耗为负数,说明从任何一个位置开始都不可能跑完全程
start = 0; //记录从哪个开始遍历(i+1),可以走完全程
for(int i = 0; i < gas.size(); i ++){
  curSum += (gas[i] - cost[i]); //当前剩余总和
  totalSum += (gas[i] - cost[i]); //统计总共的剩余
  if(curSum < 0 ) { // 说明i前的位置都不适合作为起始位置
    start = i + 1;
    curSum = 0; // 重新开始这个要归零
  }
  if(totalSum < 0) return -1;
  return start; //如果totalSum >= 0,那么肯定是有解的
}
class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
       int curSum = 0;
       int totalSum = 0;
       int startIndex = 0;
       for(int i = 0; i < gas.size(); i ++){
        curSum += (gas[i] - cost[i]);
        totalSum += (gas[i] - cost[i]);
        if(curSum < 0){
            startIndex = i + 1;
            curSum = 0;
        }
       }
       if(totalSum < 0) return -1;
       return startIndex;
    };
};

(九)分发糖果

注意:得分相同,如果没有得分比方便的小孩得分高,则得到的糖果不能比他高

两边一起考虑:顾此失彼

先确定一边,再确定另一边

  • 一次是从左到右遍历,只比较右边孩子评分比左边大的情况。

  • 一次是从右到左遍历,只比较左边孩子评分比右边大的情况。

默认所有小孩初始都为1

情况1:右半的小孩比左边的小孩高

if(rating[i] > rating[i - 1])
  candy[i] = candy[i - 1] + 1;

情况2:左边孩子比右边小孩高

注意:这里必须要从右到左遍历,这样才能利用前一步的结果

因为 rating[5]与rating[4]的比较 要利用上 rating[5]与rating[6]的比较结果,所以 要从后向前遍历。

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

(十)柠檬水找零

只需要维护三种金额的数量,5,10和20。

有如下三种情况:

  • 情况一:账单是5,直接收下。

  • 情况二:账单是10,消耗一个5,增加一个10

  • 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5

因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!

所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。

class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        // 分别记录5元、10元、20元
        int five = 0, ten = 0, twenty = 0;
        for(int i = 0; i < bills.size(); i ++){
            if(bills[i] == 5){
                five ++;
            }
            if(bills[i] == 10){
                if(five > 0){
                    five --;
                    ten ++;
                }else{
                    return false;
                }
            }
            //这里使用了贪心,先用10元的找零,5元用处比较大,能省则省
            if(bills[i] == 20){
                if(ten > 0 && five > 0){
                    ten --;
                    five --;
                    twenty ++;
                }else if(five > 2){
                    five -= 3;
                    twenty ++;
                }else{
                    return false;
                }
            }
        }
        return true;
    }
};

(十一)根据身高重建队列

涉及两个维度,不要同时考虑,确定一个维度后,再确定另一个维度

尝试一: 按照k从小到大排序,对于k相同的,将h较小的排在前面(因为k的含义是,前面有k个比h大的,所以h较大的排在后面,对k没有影响)。→ 两个维度都没有定下来

尝试二: 按照h从大到小排序,对于h相同,k从小到大排序

这样身高已经确定了,根据k调整插入顺序即可(变成插入排序了),而后面的个子矮的人插入到前面不会对前面高的人的k有影响(妙啊)

// 自定义的排序算法(h从大到小排序,对于h相同,k从小到大排序)
sort(people.begin(), people.end(),cmp);
vector<vector<int>> queue;
// 这里必须新开一个vector,因为如果在原来的vector上面插入,则for循环可能重复访问同一个元素(元素位置在插入时改变了)
for(int i = 0; i < people.size(); i ++){
  int position = people[i][1]; // 获取k, 即插入位置
  queue.insert(queue.begin() + position, people[i]);// 根据k重新插入新队列queue中
}
return queue;
class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b){
        if(a[0] == b[0]) return b[1] > a[1];
        return a[0] > b[0];
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort(people.begin(), people.end(), cmp);
        vector<vector<int>> queue;
        for(int i = 0; i < people.size(); i ++){
            int position = people[i][1];
            queue.insert(queue.begin() + position, people[i]);
        }
        return queue;
    }
};

(十二)用最少数量的箭引爆气球

从是否需要添加弓箭的角度看这道题目

先按照左边界排序,找到气球重叠

情况一:当两个气球不重叠时,一定需要多添加一个弓箭

情况二:如果两个气球重叠,不需要添加弓箭,只要更新当前气球的右边界为这两个气球的最小右边界。如果下一个气球还小于这个更新的边界,说明这三个气球重叠着,可以一起射穿,不需要添加弓箭

情况三:否则还需要添加弓箭(类似情况一)

if(points.size() == 0) return 0;
sort(points.begin(), points.end(), cmp);
int result = 1; // 气球数目>0,弓箭数至少为1
for(int i = 1; i < points.size(); i ++){// 这里需要i和i - 1比较,所以从1开始
  if(points[i][0] > points[i - 1][1]){//两个气球不重叠
    result ++;
  }else{// 两个气球重叠,更新当前气球的右边界为两者的最小右边界,方便继续判断和下一个气球(第三个)是否重叠
    points[i][1] = min(points[i - 1][1], points[i][1]);
  }
}
return result;
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) {
        if(points.size() == 0) return 0;
        sort(points.begin(), points.end(), cmp);
        int result = 1;
        for(int i = 1; i < points.size(); i ++){
            if(points[i][0] > points[i - 1][1]){
                result ++;
            }else{
                points[i][1] = min(points[i - 1][1], points[i][1]);
            }
        }
        return result;
    }
};

(十三)无重叠区间

和上一题一样的,本质都是找重叠区域,只是上一题是重叠区域则result不用加1,这题是有重叠的就result加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];
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.size() == 1) return 0;
        sort(intervals.begin(), intervals.end(), cmp);
        int result = 0;
        for(int i = 1; i < intervals.size(); i ++){
            if(intervals[i][0] >= intervals[i - 1][1]){
                continue;
            }else{
                result ++;
                //删掉那个右边界比较大的区间,这样对后面的影响小
                intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]);
            }
        }
        return result;
    }
};

(十四)划分字母区间

在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。(这个区间必须包含之前遍历过的字母的最远边界,因为同一个字母只能出现在一个区间内,而要这个区间划分数最多,所以就去这个最远边界,这里要注意最远边界是随着字母的遍历动态变化的)。

class Solution {
public:
    vector<int> partitionLabels(string s) {
        int hash[26] = {0};
        vector<int> result;
        for(int i = 0; i < s.size(); i ++){
            hash[s[i] - 'a'] = i;
        }
        int left = 0;
        int right = 0;
        for(int i = 0; i < s.size(); i ++){
            right = max(right, hash[s[i] - 'a']);
            if(i == right){
                int temp = right - left + 1;
                result.push_back(temp);
                left = i + 1;
            }
        }
        return result;
    }
};

(十五)合并区间

还是同类题目,变化是这里要先push一个进入result中,然后决定是更新result中最后一个vector的结束区间,还是新Push进去一个区间

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) {
        if(intervals.size() == 1) return intervals;
        sort(intervals.begin(), intervals.end(), cmp);
        vector<vector<int>> result;
        result.push_back(intervals[0]);
        for(int i = 1; i < intervals.size(); i ++){
            if(intervals[i][0] > result[result.size() - 1][1]){
                result.push_back(intervals[i]);
            }else{
              // 这里需要取重叠区间最后结束的(并集)
                result[result.size() - 1][1] = max(result[result.size() - 1][1], intervals[i][1]);
            }
        }
        return result;
    }
};

(十六)单调递增的数字

那么从后向前遍历,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,就可以重复利用上次比较得出的结果。从后向前遍历332的数值变化为:332 -> 329 -> 299

class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        // 将数字转为字符串,方便逐位处理
        string strNum = to_string(n);
        // 细节,即使中间的某一位小于后一位,只要前面有一个变成了9,后面的都必须变成9
        // eg: 328 -> 298 -> 299
        int flag = strNum.size();
        for(int i = strNum.size() - 2; i >=0; i --){
            if(strNum[i] > strNum[i + 1]){
                flag = i + 1;
                strNum[i] = strNum[i] - 1;
            }
        }
        for(int i = flag; i < strNum.size(); i ++){
            strNum[i] = '9';
        }
        // stoi函数可以自动去除前导0
        return stoi(strNum);
    }
};

(十七)监控二叉树

优先在叶子节点上的父节点上放置摄像头,因为叶子节点比较多,这样可以节省更多的摄像头。之后每隔两个空节点放一个摄像头

需要从下往上遍历二叉树(后序:左-右-中)

根据左右节点的状态确定父节点的状态,模拟每隔两个节点放摄像头的过程

0:无覆盖

1:有摄像头

2:有覆盖

注:空节点的状态:有覆盖,确保叶子节点的父亲节点时有覆盖状态

状态转移,同时统计摄像头数量

1.左右孩子都有覆盖2

其父节点为无覆盖0(等着这个父节点的父节点装摄像头,给他变成覆盖)

2.左右孩子至少有一个为无覆盖0

其父节点一定装摄像头1,覆盖其孩子

3.左右孩子至少有一个有摄像头1

父节点一定是有覆盖状态2

(注意2、3顺序不能换,因为只要左右孩子中有一个无覆盖,即使其中一个孩子装着摄像头,还是要给其父节点添加摄像头)

4.根节点的特殊处理,因为根节点上面没有父节点了,他的无覆盖状态无法改变

如果最后根节点还是无覆盖状态,因此要另外给他加个摄像头

int result;
// 因为这里一个节点的状态是由他的孩子节点的状态决定的,所以递归需要返回值
int traversal(TreeNode* cur){
  // 终止条件,如果遇到空节点,就向上返回
  if(cur == NULL) return 2; // 空节点为有覆盖状态
  int left = traversal(cur -> left); // 左孩子状态
  int right = traversal(cur -> right); // 右孩子状态
  if(left == 2 && right == 2) return 0; // 情况1
  if(left == 0 || right == 0){ // 情况2
    result ++; // 该节点装一个摄像头
    return 1;
  }
  if(left == 1 || right == 1) return 2; //情况3
  return -1; // 这里不会走到
}
// 主函数中处理情况4
int camera(root){
  result = 0;
  if(traversal(root) == 0){ // 最终root的状态返回为无覆盖
    result ++;
  }
  return result;
}
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    int result;
    int traversal(TreeNode* cur){
        if(cur == NULL) return 2;
        int left = traversal(cur -> left);
        int right = traversal(cur -> right);
        if(left == 2 && right == 2) return 0;
        if(left == 0 || right == 0){
            result ++;
            return 1;
        }
        if(left == 1 || right == 1) return 2;
        return -1;
    }
public:
    int minCameraCover(TreeNode* root) {
        result = 0;
        if(traversal(root) == 0) result ++;
        return result;
    }
};
  • 9
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值