贪心算法巩固

贪心的本质是选择每一阶段的局部最优,从而达到全局最优。

贪心算法并没有固定的套路。因而需要进一步巩固,多做做多有感觉才行。

这次巩固题目来源:代码随想录

1.贪心简单题

(1)分发饼干,已做过。

(2)1005.K次取反后最大化的数组和

 其实模拟就能做(每次改变最小的其实也是一种贪心)


class Solution {
public:

    int largestSumAfterKNegations(vector<int>& nums, int k) {
        int max = 0;
        for (int i = 0; i < nums.size(); ++i) {
            max += nums[i];
        }
        while (k--) {
            int min = nums[0];
            int dex = 0;
            for (int i = 0; i < nums.size(); ++i) {
                if (min > nums[i]) {
                    min = nums[i];
                    dex = i;
                }
            }
            max -= nums[dex];
            nums[dex] = -nums[dex];
            max += nums[dex];

        }
        return max;
    }

贪心也行:
/*第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
第二步:从前向后遍历,遇到负数将其变为正数,同时K--
第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
第四步:求和*/

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

(3)860.柠檬水找零

  • 情况一:账单是5,直接收下。
  • 情况二:账单是10,消耗一个5,增加一个10
  • 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
class Solution {
public:
    bool lemonadeChange(vector<int>& bills) {
        vector<int> cur(3,0);
        for (int i = 0; i < bills.size(); ++i) {
            if (bills[i] == 5) {
                cur[0] += 1;
                continue;
            }
            if (bills[i] == 10) {
                cur[1] += 1;
                if (cur[0] > 0) {
                    --cur[0];
                }
                else {
                    return false;
                }
            }
            if (bills[i] == 20) {
                cur[2] += 1;
                if (cur[0] > 0 && cur[1] > 0) {
                    --cur[0];
                    --cur[1];
                }
                else if (cur[0] >= 3) {
                    cur[0] -= 3;
                }
                else {
                    return false;
                }

            }

        }

        return true;
    }
};

2.贪心中等题

贪心中等题,靠常识可能就有点想不出来了。开始初现贪心算法的难度与巧妙之处。

(1)738.单调递增的数字

这个说白了是个数学问题,怎么能最小呢?

首先先把每一位存到数组里面,然后从后往前遍历:

     * 思路:
     *  从右向左扫描数字,若发现当前数字比其左边一位(较高位)小,
     *  则把其左边一位数字减1,并将该位及其右边的所有位改成9 

仔细想想,这很正确

class Solution {
public:

    void turnNine(vector<int>& num,int left) {
        for (int i = left; i < num.size(); ++i) {
            num[i] = 9;
        }
    }

    int monotoneIncreasingDigits(int n) {
        int len = 0;
        int temp = n;
        while (temp) {
            ++len;
            temp = temp / 10;
        }
        temp = n;
        vector<int> num(len);
        int i = len - 1;
        while (temp) {
            int tail = temp % 10;
            temp /= 10;
            num[i--] = tail;
        }
        /**
     * 思路:
     *  从右向左扫描数字,若发现当前数字比其左边一位(较高位)小,
     *  则把其左边一位数字减1,并将该位及其右边的所有位改成9
     */
        for (int i = len - 1; i > 0; --i) {
            if (num[i - 1] > num[i]) {
                num[i - 1] -= 1;
                turnNine(num, i);
            }

        }
        int rs = 0;
        for (int i = 0; i < len; ++i) {
            rs = rs * 10 + num[i];
        }
        return rs;
    }
};

(2)376. 摆动序列

这还真的挺难,不会做

法一:贪心

//贪心,转化为上坡下坡,有几个坡。最后不忘加一就行(这里事先就加一了)
//保持区间波动,只需要把单调区间上的元素移除就可以了。 

 实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)

这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点。

于是这就转化为了求有几个坡( 包括上坡下坡)的问题

class Solution1{
public:
    //注意不能为0
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        int curDiff = 0; // 当前一对差值
        int preDiff = 0; // 前一对差值
        int result = 1;  // 记录峰值个数,序列默认序列最右边有一个峰值
        for (int i = 0; i < nums.size() - 1; i++) {
            curDiff = nums[i + 1] - nums[i];
            // 出现峰值
            //cur不能为0,prediff=0仅仅在第一次判断时用,我们假设0号元素(也就是第一个元素)的prediff=0
            if ((curDiff > 0 && preDiff <= 0) //当前上坡,前一个下坡
                || (preDiff >= 0 && curDiff < 0)) {//前一个上坡,当前下坡
                result++;
                preDiff = curDiff;
            }
        }
        return result;
    }
};

法二 dp

设dp状态dp[i][0],表示考虑前i个数,第i个数作为山峰的摆动子序列的最长长度
设dp状态dp[i][1],表示考虑前i个数,第i个数作为山谷的摆动子序列的最长长度
dp[i][0] = max(dp[i][0], dp[j][1] + 1),其中0 < j < i且nums[j] < nums[i],
表示将nums[i]接到前面某个山谷后面,作为山峰。
dp[i][1] = max(dp[i][1], dp[j][0] + 1),其中0 < j < i且nums[j] > nums[i],
表示将nums[i]接到前面某个山峰后面,作为山谷

class Solution {
public:
    int dp[1005][2];
    int wiggleMaxLength(vector<int>& nums) {
        memset(dp, 0, sizeof dp);
        dp[0][0] = dp[0][1] = 1;

        for (int i = 1; i < nums.size(); ++i)
        {
            dp[i][0] = dp[i][1] = 1;

            for (int j = 0; j < i; ++j)
            {
                if (nums[j] > nums[i]) dp[i][1] = max(dp[i][1], dp[j][0] + 1);
            }

            for (int j = 0; j < i; ++j)
            {
                if (nums[j] < nums[i]) dp[i][0] = max(dp[i][0], dp[j][1] + 1);
            }
        }
        return max(dp[nums.size() - 1][0], dp[nums.size() - 1][1]);
    }
};

3.贪心解决股票问题

(1)122.买卖股票的最佳时机II

做过了,思路就是每天都可以卖出然后买进,只要比昨天高就卖然后再买

[7, 1, 5, 6]   ——》  (5-1)+ (6-5)= 5

(2)714. 买卖股票的最佳时机含手续费

这个就不会了。好好审题。

法一贪心

本题有了手续费,就要关系什么时候买卖了,因为计算所获得利润,需要考虑买卖利润可能不足以手续费的情况。

如果使用贪心策略,就是最低值买,最高值(如果算上手续费还盈利)就卖

无非就是要找到两个点,买入日期,和卖出日期

所以我们在做收获利润操作的时候其实有三种情况:

 这三个情况要深刻体会,尤其是第一种,如何用代码实现,并且和第二种连接。

//妙,很难,多思考
class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int result = 0;
        int minPrice = prices[0]; // 记录最低价格
        for (int i = 1; i < prices.size(); i++) {
            // 情况二:相当于买入
            if (prices[i] < minPrice) minPrice = prices[i];

            // 情况三:保持原有状态(因为此时买则不便宜,卖则亏本)
            if (prices[i] >= minPrice && prices[i] <= minPrice + fee) {
                continue;
            }

            // 计算利润,可能有多次计算利润,最后一次计算利润才是真正意义的卖出
            if (prices[i] > minPrice + fee) {
                result += prices[i] - minPrice - fee;
                minPrice = prices[i] - fee; // 情况一,这一步很关键
            }
        }
        return result;
    }
};

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

从代码中可以看出对情况一的操作,因为如果还在收获利润的区间里,表示并不是真正的卖出,而计算利润每次都要减去手续费,所以要让minPrice = prices[i] - fee;,这样在明天收获利润的时候,才不会多减一次手续费!

这里有个更好理解的:

class Solution {
    public int maxProfit(int[] prices, int fee) {
        if (prices.length == 1) return 0; // 长度为1,没有交易空间;
 
        int base = prices[0] + fee; // 本身带交易费的买入,后面高于这个部分的,都是利润;
        int profit = 0;

        for (int i = 1; i < prices.length; ++i) {
            if (prices[i] > base) { // 高于的,都是利润;
                profit += prices[i] - base;
                base = prices[i]; // 一直往上走;
            }
            else if (prices[i] + fee < base) { // 一旦遇到下降,说明利润达到顶点了,转为下滑;
                // 不断试探,最低点(买入点)在哪里;但是只要遇到高点,if语句就会加入利润
                base = prices[i] + fee;
            }
        }

        return profit;
    }
}

法二dp

dp1[i]表示第i天手上有股票,dp2[i]表示第i天手上没有股票,递归方程:

dp1[i] = max(dp1[i-1], dp2[i-1] - prices[i]) (第二项表示在第i天买入股票)
dp2[i] = max(dp2[i-1], dp1[i-1] + prices[i] - fee) (第二项表示在第i天将股票卖出,需扣除手续费)

class Solution2 {
public:
    int maxProfit(vector<int>& prices, int fee) {
        vector<int>dp1(prices.size(), 0);
        vector<int>dp2(prices.size(), 0);
        if (prices.size() < 2) {
            return 0;
        }
        dp1[0] -= prices[0];
        for (int i = 1; i < prices.size(); ++i) {
            dp1[i] = max( dp1[i - 1],dp2[i - 1] - prices[i] );
            dp2[i] = max(dp2[i - 1], dp1[i - 1] - fee+ prices[i]);
        }
        
        //最后一天后肯定都卖了
        return dp2[prices.size() - 1];


    }
};

4.两个维度权衡问题   

分发糖果和根据身高重建队列

遇到两个维度权衡的时候,一定要先确定一个维度,再确定另一个维度。

如果两个维度一起考虑一定会顾此失彼。

比如重建队列:

如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。

那么按照身高h来排序呢,身高一定是从大到小排(身高相同的话则k小的站前面),让高个子在前面。

此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!

5.区间问题

(1)

 (2)55. 跳跃游戏


//跳跃覆盖范围究竟可不可以覆盖到终点
//贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),
//整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
class Solution {
public:
    bool canJump(vector<int>& nums) {
        int cur = 0;
        if (nums.size() <= 1) return true;
        int range = nums[cur];
        int i = 0;
        while (i <= range) {
            if (i >= nums.size() - 1) {
                return true;
            }
            if (i + nums[i] > range) {
                range = nums[i] + i;
            }//范围扩大
            ++i;

        }
        return false;
    }
};

(3)45.跳跃游戏II

这个难一点

 

先第一个元素获取第一个范围,然后在这第一个范围里面找到值最大的数,作为新的范围,如此反复,然后这里说了一定可以跳到最后,所以我们不用担心无解


class Solution {
public:
    int jump(vector<int>& nums) {
        int cur = 0;
        if (nums.size() <= 1) return 0;
        int range = nums[cur];
        int rs = 1;//第一步肯定要跳
        int max = range;
        int p = 0;//加快内部的for
        while (range<nums.size()-1) {
            for (int i = p; i <=range; ++i) {//这里是等于,很重要
                if (i + nums[i] > max) {
                    max = i + nums[i];
                }
            }
            p += range;
            range = max;
            ++rs;
        }
        return rs;
    }
};

(4)56. 合并区间

模拟的过程就是贪心啦。

排序然后分情况处理,排序用引用加快速度

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        if (intervals.size()<=1 ){
            return  intervals;
        }
   
        sort(intervals.begin(), intervals.end(), [](const vector<int> &a, const vector<int> &b)->bool {
            return a[0] < b[0];//这里按左边界排序
            });
        vector<vector<int>> rs;
        vector<int> temp;

        int end = intervals[0][1];
        int start = intervals[0][0];
        temp.push_back(start);
        temp.push_back(end);
        for (int i = 1; i < intervals.size(); ++i) {
            if (intervals[i][0] <= end) {
                if (intervals[i][1] > end) {
                    end = intervals[i][1];
                }
                temp.pop_back();
                temp.push_back(end);
            }
            else {
                rs.push_back(temp);
                temp.clear();
                start = intervals[i][0];
                end = intervals[i][1];
                temp.push_back(start);
                temp.push_back(end);

            }
            if (i == intervals.size() - 1) {
                rs.push_back(temp);
            }
        }
        return rs;

    }
};

 6.其他

(1)53. 最大子序和 

第一反应用动态规划而不是贪心

dp
/*定义一个函数f(n),以第n个数为结束点的子数列的最大和,
存在一个递推关系f(n) = max(f(n-1) + A[n], A[n]);

将这些最大和保存下来后,取最大的那个就是,最大子数组和。
因为最大连续子数组 等价于 最大的以n个数为结束点的子数列和*/


class Solution {
public:
    //f_n表示n为终点的最大子数组和
    int maxSubArray(vector<int>& nums) {
        if (nums.size() == 0)return NULL;
        int res = INT_MIN;
        int f_n = -1;
        for (int i = 0; i < nums.size(); ++i) {
            f_n = max(nums[i], f_n + nums[i]);
            res = max(f_n, res);
        }

        return res;


    }
};

牛逼的!

法二:暴力

class Solution2 {
public:
    int maxSubArray(vector<int>& nums) {
        int result = INT32_MIN;
        int count = 0;
        for (int i = 0; i < nums.size(); i++) { // 设置起始位置
            count = 0;
            for (int j = i; j < nums.size(); j++) { // 每次从起始位置i开始遍历寻找最大值
                count += nums[j];
                result = count > result ? count : result;
            }
        }
        return result;
    }
};

法三:贪心

关键在于:不能让“连续和”为负数的时候加上下一个元素,而不是 不让“连续和”加上一个负数。

如果 -2 1 在一起,计算起点的时候,一定是从1开始计算,因为负数只会拉低总和,这就是贪心贪的地方!

局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。

全局最优:选取最大“连续和”

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

(2)134. 加油站

 很妙的解法,看代码来体会,这题我是不会的

class Solution {
public:

    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int rest = 0, run = 0, start = 0;
        //rest是计算是否有解的,也就是说如果没有解,其实就是所有的gas加起来小于cost
        //run代表实时的油量
        //start就是起点
        for (int i = 0; i < gas.size(); ++i) {
            run += (gas[i] - cost[i]);
            rest += (gas[i] - cost[i]);
            if (run < 0) {
                start = i + 1;//为什么不是i?因为如果run此时小于0了就说明i-i+1这一段的cost大于之前剩余的油量加上第i站加油站的油量
                //所以必须是i+1
                run = 0;
            }
        }
        return rest < 0 ?
            -1 : start;
    }

};

当然还有个暴力的:

//暴力
class Solution2 {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        for (int i = 0; i < cost.size(); i++) {
            int rest = gas[i] - cost[i]; // 记录剩余油量
            int index = (i + 1) % cost.size();
            while (rest > 0 && index != i) { // 模拟以i为起点行驶一圈
                rest += gas[index] - cost[index];
                index = (index + 1) % cost.size();
            }
            // 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置
            if (rest >= 0 && index == i) return i;
        }
        return -1;
    }
};

(3)968.监控二叉树

 每个节点有三种状态:节点上有摄像机,节点没摄像机但是被摄像机覆盖,节点没摄像机也没被覆盖。

// 版本一
class Solution {
private:
    int result;
    int traversal(TreeNode* cur) {

        // 空节点,该节点有覆盖
        if (cur == NULL) return 2;

        int left = traversal(cur->left);    // 左
        int right = traversal(cur->right);  // 右

        // 情况1
        // 左右节点都有覆盖
        if (left == 2 && right == 2) return 0;

        // 情况2
        // left == 0 && right == 0 左右节点无覆盖
        // left == 1 && right == 0 左节点有摄像头,右节点无覆盖
        // left == 0 && right == 1 左节点有无覆盖,右节点摄像头
        // left == 0 && right == 2 左节点无覆盖,右节点覆盖
        // left == 2 && right == 0 左节点覆盖,右节点无覆盖
        if (left == 0 || right == 0) {
            result++;
            return 1;
        }

        // 情况3
        // left == 1 && right == 2 左节点有摄像头,右节点有覆盖
        // left == 2 && right == 1 左节点有覆盖,右节点有摄像头
        // left == 1 && right == 1 左右节点都有摄像头
        // 其他情况前段代码均已覆盖
        if (left == 1 || right == 1) return 2;

        // 以上代码我没有使用else,主要是为了把各个分支条件展现出来,这样代码有助于读者理解
        // 这个 return -1 逻辑不会走到这里。
        return -1;
    }

public:
    int minCameraCover(TreeNode* root) {
        result = 0;
        // 情况4
        if (traversal(root) == 0) { // root 无覆盖,判断根节点
            result++;
        }
        return result;
    }
};

简化后:


// 版本二
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;
        else if (left == 0 || right == 0) {
            result++;
            return 1;
        }
        else return 2;
    }
public:
    int minCameraCover(TreeNode* root) {
        result = 0;
        if (traversal(root) == 0) { // root 无覆盖
            result++;
        }
        return result;
    }
};

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 哈工大的ch5贪心算法作业是一个涉及贪心算法的作业。贪心算法是一种求解最优化问题的算法,其核心思想是每一步都选择当前最优解,以期望最终能得到全局最优解。在这个作业中,我们将学习如何利用贪心算法解决一些实际问题。 这个作业可能会涉及一些经典的贪心算法问题,比如背包问题、任务调度问题等。对于这些问题,我们需要根据题目的要求,设计相应的贪心策略,并编写程序来实现解决方案。 在完成作业的过程中,我们需要理解贪心算法的基本原理和特点,比如贪心选择性质、最优子结构性质等。同时,我们还需要学会分析问题的特点,选择合适的贪心策略,并证明其正确性。 完成这个作业的过程不仅可以提高我们对贪心算法的理解,还可以培养我们的问题解决能力和编程实现能力。同时,我们还可以通过参考其他同学的解答和进行讨论,加深对贪心算法的理解和应用。 总的来说,哈工大的ch5贪心算法作业是一个很好的练习和巩固贪心算法知识的机会。通过完成这个作业,我们可以更加深入地理解贪心算法,并将其应用到实际问题中。 ### 回答2: 哈工大ch5贪心算法作业主要涉及到贪心算法的理解和应用。贪心算法是一种在每一步选择中都考虑当前状态下最优解的策略。其基本思想是通过每一步的局部最优解来寻求全局最优解。 在哈工大ch5贪心算法作业中,可能会涉及到以下几个重要的题目或问题。 首先,可能会要求编写贪心算法的代码。通过分析问题的特点,我们可以设计出一套贪心策略,然后根据策略编写代码。在编写代码时,需要定义好问题的输入和输出格式,并考虑边界情况和异常情况的处理。 其次,可能会要求分析贪心算法的时间复杂度和空间复杂度。时间复杂度是衡量算法执行时间的指标,空间复杂度是衡量算法运行所需内存空间的指标。通过分析算法的每个步骤和数据结构的使用情况,可以计算出算法的时间复杂度和空间复杂度。 此外,可能会要求证明贪心算法的正确性。为了证明贪心算法的正确性,我们需要证明贪心选择性质和最优子结构性质。贪心选择性质是指每一步都选择局部最优解,最优子结构性质是指整个问题的最优解可以通过局部最优解递归构建。 最后,可能会要求应用贪心算法解决实际问题。贪心算法可以应用于很多实际问题,例如任务调度、区间调度、背包问题等。通过将实际问题抽象为数学模型,并根据问题的特点设计贪心策略,可以用贪心算法来求解这些问题。 总之,哈工大ch5贪心算法作业主要涉及到贪心算法的理解、应用和分析。通过完成这些题目,可以进一步提高对贪心算法的理解和掌握。 ### 回答3: 哈尔滨工业大学计算机科学与技术专业的第五章贪心算法作业,主要涉及贪心算法的基本原理和应用。贪心算法是一种求解优化问题的方法,其核心思想是每一步都选择当前状态下最优的决策,从而希望达到全局最优解。 在作业中,学生需要运用贪心算法解决一系列实际问题。这些问题涉及到任务调度、背包问题、区间覆盖等。例如,有一批任务需要在一定时间内完成,每个任务有不同的截止时间和收益,学生需要制定一个调度方案使得总收益最大化;或者给定一组物品,每个物品有其重量和价值,学生需要选出一些物品放入背包中,使得背包的总价值最大化,但是背包的容量有限制。 在解决这些问题的过程中,学生需要根据问题描述和约束条件,选择合适的贪心策略。贪心策略常见的有按照某种特定顺序进行选择、优先选择单位收益最高的任务或物品等。但是需要注意的是,贪心算法并不是适用于所有问题,有些问题无法通过贪心策略得到最优解。 完成作业后,学生需要编写代码实现贪心算法,并对给定的测试用例进行验证。同时,还要对算法的时间复杂度进行分析,并给出评价。贪心算法通常具有较低的时间复杂度,并且可以在很多实际应用场景中取得较好的效果。 总体而言,哈尔滨工业大学计算机科学与技术专业第五章贪心算法作业是一次综合性的实践性作业,通过解决实际问题来加深对贪心算法的理解和应用。这对于学生提高算法设计和分析能力,培养实际问题解决能力,具有重要的意义。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值