算法基础 典型题(三)贪心算法

一、基本概念

贪心算法是一种 “活在当下,只看眼前” 的算法策略。它在每一步都做出当前看起来最好的选择,并期望通过这一系列的局部最优选择,最终导致一个全局最优解。

一个经典比喻:找零钱问题。假设硬币体系为[100, 50, 20, 10, 5, 1],店员需要找给你 136元。贪心策略:每一步都选择面值不超过剩余金额的最大硬币。选100元,剩36元。选20元,剩16元。选10元,剩6元。选5元,剩1元。选1元,完成。最终方案是:100+20+10+5+1,共5枚硬币。这确实是最优解。

核心思想1)局部最优:每一步都只考虑当前情况下的最佳选择,不考虑整体和未来。
2)不可回溯:一旦做出选择,就不会再回头改变。这和回溯算法(会“撤销”选择)形成鲜明对比。3)高效性:因为不做回溯,通常时间复杂度很低。

二、适用场景

贪心算法不能保证对所有问题都得到全局最优解(比如,如果硬币体系是[4, 3, 1],要找6元,贪心会给出4+1+1,而不是更优的3+3)。因此,判断一个问题能否用贪心算法至关重要。必须同时满足以下两个条件:

1、首先是贪心选择性质,也就是说,每一步的局部最优解能最终导向全局最优解。这通常需要数学证明或逻辑推理。

2、其次是最优子结构,问题的最优解包含了子问题的最优解。解决了大问题,就意味着小问题也已经以最佳方式解决了。

简单总结:如果能证明“每一步都贪心”最终能得到全局最优,并且子问题的解是最优的,那么就可以用贪心算法。

三、典型题目

1)分糖果:先对饼干和小孩都加以排序,遍历小孩最小的小孩获取满足要求的饼干

2)摇摆序列: 设定一个反转状态(start/increase/decrease),每个状态尽可能多包含元素,切换一次摆动增加

3)移掉K位数字 保持结果最小:分析删除规律,每次变小的时删除前比其大的,前面没有数零丢弃,到最后还有未丢弃的从末尾开始丢弃, 可以利用栈来做(单调栈:单调递增栈,每次放进栈的数单向递增,如果发现放入的元素再变小,就把原来的栈顶进行出栈操作)。注意最后栈内容生成结果时要反向;

3)跳跃游戏:每个下标可以达到的最大距离为 i + nums[i], 只需要遍历index过程,刷新最大可到距离 判定可以到nums.size()即可。

4)跳跃游戏2(最少动作):相比跳跃游戏,记录过程最大max_reach和当前reach,当发现reach不够的时候,更新max_reach,并记录一次跳跃

5)用最少数量弓箭引爆气球:先对坐标排序, 每一只箭都尽可能多的射到更多气球,遍历过程超过新气球左边说明也能射中,超过右边界,要回退保证都能射中

6)最优加油法:按节点用记录路过位置的油量,每次经可能往前多走,发现油不够就取记录最大油节点油量 并 统计次数,如果全取完还不够就说明到不了;简化技巧 用大顶堆记录油节点简化统计复杂度,大顶堆:堆顶(根节点)的元素是整个堆中最大的元素。

分糖果:455. 分发饼干 - 力扣(LeetCode)

假设你是一位家长,想要给你的孩子们一些小饼干。每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        /* 贪心,先对饼干和小孩都加以排序,遍历小孩最小的小孩获取满足要求的饼干 */
        int s_index = 0;
        int g_index = 0;
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        while (s_index < s.size() && g_index < g.size()) {
            if (g[g_index] <= s[s_index]) { // 小孩被满足
                g_index++;
            }
            s_index++;
        }
        return g_index;
    }
};

摇摆序列:376. 摆动序列 - 力扣(LeetCode)

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

class Solution {
    typedef enum {
        STATE_START = 0, //  给起始状态的,可能存在连续相同的数值,状态既不属于递增也不属于递减
        STATE_INCREASE,
        STATE_DECREASE, 
    } state_mode;
public:
    int wiggleMaxLength(vector<int>& nums) {
        /* 贪心, 设定一个反转状态(start/increase/decrease),每个状态尽可能多包含元素,切换一次摆动增加,
            start是给起始状态的,因为可能存在连续相同的数值,状态既不属于递增也不属于递减 */
        if (nums.size() == 0) {
            return 0;
        }
        if (nums.size() == 1) {
            return 1;
        }
        state_mode state = STATE_START; 
        int result = 1;
        for (int i = 1; i < nums.size(); i++) {
            // 起始状态
            if (state == STATE_START) {
                if (nums[i] > nums[i-1]) {
                    state = STATE_INCREASE;
                    result++;
                } else if (nums[i] < nums[i-1]) {
                    state = STATE_DECREASE;
                    result++;
                }
                // cout << "start " << i << " " << nums[i] << endl;
            // 递增
            } else if (state == STATE_INCREASE) {
                if (nums[i] < nums[i-1]) {
                    state = STATE_DECREASE;
                    result++;
                }
                // cout << "up " << i << " " << nums[i] << endl;
            // 递减
            } else {
                if (nums[i] > nums[i-1]) {
                    state = STATE_INCREASE;
                    result++;
                }
                // cout << "down " << i << " " << nums[i] << endl;
            }
        }
        return result;
    }
};

移除K个数字:402. 移掉 K 位数字 - 力扣(LeetCode)

给定一个以字符串表示的非负整数 num,移除这个数中的 位数字,使得剩下的数字最小。

class Solution {
public:
    string removeKdigits(string num, int k) {
    /* 贪心,分析数值删除规律,每次变小的时候删除前比其面大的,如果前面没有数零丢弃,
        到最后如果还有未丢弃的,从低位开始丢弃, 可以利用栈来做 */
        stack <char> result;
        for (int i = 0; i < num.size(); i++) {
            // 只要变小,就丢弃比此数大的数,
            while (!result.empty() && num[i] < result.top() &&  k > 0) {
                // cout << "pop" << result.top() << endl;
                result.pop();
                k--;
            }
            if (!result.empty() || num[i] != '0') {  // 为空的时候零丢弃
                // cout << "push" << num[i] << endl;
                result.push(num[i]);  
            }
        }
        // 遍历完成后还有没丢完的,继续出栈
        while (k > 0 && !result.empty()) {
            // cout << "end_pop" << result.top() << endl;
            result.pop();
            k--;
        }
        // 把栈转化为字符串
        string ret;
        while (!result.empty()) {
            ret += result.top();
            result.pop();
        }
        reverse(ret.begin(), ret.end()); // 记得反序,由于出栈后顺序颠倒
        return ret.size() == 0 ? "0" : ret; // 结尾需要判定是否都丢完了,要返回0
    }
};

跳远游戏:55. 跳跃游戏 - 力扣(LeetCode)

给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。

class Solution {
public:
    bool canJump(vector<int>& nums) {
        /* 贪心,每个下标可以达到的最大距离为 i + nums[i],
            只需要遍历index过程,判定每个位置是否可达,刷新最大可到距离>=nums.size()算到终点 */
        if (nums.size() == 0) {
            return false;
        }
        int max_can_reach = nums[0]; // 初始能力为nums[0]大小, 在位置0
        for (int i = 1; i < nums.size(); i++) {
            // 判断i是否可以到, 不可到直接放弃
            if (max_can_reach < i) {
                return false;
            }
            // 可到的情况判定是否要刷新最大距离
            max_can_reach = max(max_can_reach, i + nums[i]);
            // 如果已经发现可以到终点了,直接退出,不用再检查了
            if (max_can_reach >= nums.size()) {
                break;
            }
        }
        return true;
    }
};

跳远游戏2:45. 跳跃游戏 II - 力扣(LeetCode)

给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。

class Solution {
public:
    int jump(vector<int>& nums) {
        /* 贪心,每个下标可以达到的最大距离为 i + nums[i],
        只需要遍历index过程,记录过程最大max_reach和当前reach,当发现reach不够
        的时候,更新max_reach,并记录一次跳跃 */
        int can_reach = 0; // 初始在位置0,一次都没跳
        int max_can_reach = 0;
        int step = 0;
        for (int i = 0; i < nums.size(); i++) {
            // 如果当前的能力到不到,选择记录的最大max,多一次跳跃
            if (can_reach < i) {
                // cout << "update" << max_can_reach << endl;
                can_reach = max_can_reach;
                step++;
            }
            // 刷新最大距离
            max_can_reach = max(max_can_reach, i + nums[i]);
        }
        return step;
    }
};

射击气球:452. 用最少数量的箭引爆气球 - 力扣(LeetCode)

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。
由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。
开始坐标总是小于结束坐标。平面内最多存在104个气球。
一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,
若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,
则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        /* 贪心,先对坐标排序, 每一只箭都尽可能多的射到更多气球,
        遍历过程超过新气球左边说明也能射中,超过右边界,要回退保证都能射中 */
        if (points.size() == 0) {
            return 0;
        }
        sort(points.begin(), points.end()); // 先对坐标排序,默认比较第一元素,左坐标
        int num = 1;
        int shoot_pos = points[0][1]; // 第一支箭先取最右边的位置
        for (int i = 1; i < points.size(); i++) {
            if (shoot_pos >= points[i][0]) {  // 这只箭超过新气球左边,说明也能射中
                shoot_pos = min(points[i][1], shoot_pos); // 如果超过右边界,回退一部分保证都能射中
            } else { // 无法射中,就添加射箭数量,更新射箭位置
                num++;
                shoot_pos = points[i][1];
            }
        }
        return num;
    }
};

最优加油法:774. 最小化去加油站的最大距离 - 力扣(LeetCode)

已知一条公路上,有一个起点和一个终点,这之间有n个加油站。
已知:从这n个加油站到终点的距离d与各个加油站可以加油的量L,
起点位置至终点的距离L与起始时刻油箱的油量P。假设1个单位的汽油走1个单位的距离,
油箱没有上限,最少加几次油,可以从起点开至终点?(无法到达返回-1)

//L起点到终点的距离,P起点初始汽油量,pair<加油站到终点距离,加油站汽油量>
int get_mini(int L, int P, vector<pair<int, int> > & stop)
{
	priority_queue<int> gas_heap;//存储油量的最大堆
	int result = 0;
	stop.push_back(make_pair(0, 0)); /* 将终点作为一个停靠点,添加至stop */
	sort(stop.begin(), stop.end(), cmp); /* 以停靠点至终点距离从大到小排序 */
	for(int i = 0; i < stop.size(); ++i) {
		/* 当前要走的距离就是 L - 下一个停靠点至终点的距离 */
		int dis = L - stop[i].first; 
		/* 当前油量不够行驶下一段距离时,就加油 */
		while(P < dis && gas_heap.empty() != true) {
			P += gas_heap.top();
			gas_heap.pop();
			result++;
		}
		/* 距离没走完 油没了 返回失败 */
		if(P < dis && gas_heap.empty() == true) {
			return -1;
		}	
		P = P - dis; /* 更新剩余汽油 */	
		L = stop[i].first; /* 更新剩余距离 */	
		gas_heap.push(stop[i].second); /* 把当前站的汽油放到堆中 备用 */	
	}
	return result;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值