贪心算法学习笔记

写在之前的话,很多人认为贪心算法实现起来比较容易,思路很清晰,但是我觉得贪心算法的难点在于确定当前的问题可以使用贪心算法来求解?


简单贪心算法问题

例题:【455】Assign Cookies
这里写图片描述
通常的解决策略:我们尝试将最大的饼干给最贪心的小朋友,这样做有什么好处呢,试想一下:
  1)若当前最大饼干可以满足最贪心的小朋友,那么留给次贪心的小朋友的饼干在当前看来也是最大的一块,接下来的操作就是:将剩余饼干中的最大饼干分给剩余小朋友中最贪心的那位(也就是次贪心的那位);
  2)如果最大的饼干都没有满足最贪心的小朋友,那么其余的饼干也一定无法满足这位最贪心的小朋友。对于最贪心小朋友的操作可以看做是,我们在尽最大的力让他保持开心。
  
排序:从上面的策略分析不难发现,贪心算法往往依赖排序算法(每次都取最大值或者最小值)所以需要我们对数据进行排序。

using namespace std;
//先尝试满足最贪心的小朋友
class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        //对饼干数组和小朋友数组进行逆序排序。
        sort(g.begin(), g.end(), greater<int>());
        sort(s.begin(), s.end(), greater<int>());

        int gi = 0, si = 0;
        int res = 0;
        while( gi < g.size() && si < s.size() ){
            //当前饼干大小可以满足当前的小朋友
            if( s[si] >= g[gi] ){
                res ++;
                si ++;
                gi ++;
            }
            //当前饼干无法满足当前小朋友
            else
                gi ++;
        }

        return res;
    }
};

int main() {

    int g1[] = {1, 2, 3};
    vector<int> gv1(g1, g1 + sizeof(g1)/sizeof(int));
    int s1[] = {1, 1};
    vector<int> sv1(s1, s1 + sizeof(s1)/sizeof(int));
    cout<<Solution().findContentChildren(gv1, sv1)<<endl;

    int g2[] = {1, 2};
    vector<int> gv2(g2, g2 + sizeof(g2)/sizeof(int));
    int s2[] = {1, 2, 3};
    vector<int> sv2(s2, s2 + sizeof(s2)/sizeof(int));
    cout<<Solution().findContentChildren(gv2, sv2)<<endl;

    return 0;
}

代码二: 先尝试满足最不贪心的小朋友

import java.util.Arrays;

/// 455. Assign Cookies
/// https://leetcode.com/problems/assign-cookies/description/
/// 先尝试满足最不贪心的小朋友
/// 时间复杂度: O(nlogn)
/// 空间复杂度: O(1)
public class Solution2 {

    public int findContentChildren(int[] g, int[] s) {

        Arrays.sort(g);
        Arrays.sort(s);

        int gi = 0, si = 0;
        int res = 0;
        while(gi < g.length && si < s.length){
            if(s[si] >= g[gi]){
                res ++;
                gi ++;
            }
            si ++;
        }

        return res;
    }

    public static void main(String[] args) {

        int g1[] = {1, 2, 3};
        int s1[] = {1, 1};
        System.out.println((new Solution2()).findContentChildren(g1, s1));

        int g2[] = {1, 2};
        int s2[] = {1, 2, 3};
        System.out.println((new Solution2()).findContentChildren(g2, s2));
    }
}

例题【392】Is Subsequence
这里写图片描述
子序列:不连续


贪心算法与动态规划的关系

例题【435】Non-overlapping Intervals
这里写图片描述
等价说法:最少删除——>最多保留
这里写图片描述
动态规划:
思考:对于组合问题能不能使用动态规划?
排序:按照区间的起始点进行排序。
分析:对于每一个区间,我们都要和它前面的所有区间进行比较,若可以跟在前面区间之后形成不重叠的区间分为,则对前面最长的不重叠区间的状态数加1;(最长上升子序列)
至此:问题转化成为了经典的动态规划问题。
动态规划代码:

#include <iostream>
#include <vector>

using namespace std;


/// Definition for an interval.
struct Interval {
    int start;
    int end;
    Interval() : start(0), end(0) {}
    Interval(int s, int e) : start(s), end(e) {}
};

bool compare(const Interval &a, const Interval &b){

    if(a.start != b.start)
        return a.start < b.start;
    return a.end < b.end;
}

/// 435. Non-overlapping Intervals
/// https://leetcode.com/problems/non-overlapping-intervals/description/
/// 动态规划
/// 时间复杂度: O(n^2)
/// 空间复杂度: O(n)
class Solution {

public:
    int eraseOverlapIntervals(vector<Interval>& intervals) {

        if(intervals.size() == 0)
            return 0;

        sort(intervals.begin(), intervals.end(), compare);

        // memo[i]表示以intervals[i]为结尾的区间能构成的最长不重叠区间序列
        vector<int> memo(intervals.size(), 1);
        for(int i = 1 ; i < intervals.size() ; i ++)
            // memo[i]
            for(int j = 0 ; j < i ; j ++)
                if(intervals[i].start >= intervals[j].end)
                    memo[i] = max(memo[i], 1 + memo[j]);

        int res = 0;
        for(int i = 0; i < memo.size() ; i ++)
            res = max(res, memo[i]);

        return intervals.size() - res;
    }
};

int main() {

    Interval interval1[] = {Interval(1,2), Interval(2,3), Interval(3,4), Interval(1,3)};
    vector<Interval> v1(interval1, interval1 + sizeof(interval1)/sizeof(Interval));
    cout << Solution().eraseOverlapIntervals(v1) << endl;

    Interval interval2[] = {Interval(1,2), Interval(1,2), Interval(1,2)};
    vector<Interval> v2(interval2, interval2 + sizeof(interval2)/sizeof(Interval));
    cout << Solution().eraseOverlapIntervals(v2) << endl;

    Interval interval3[] = {Interval(1,2), Interval(2,3)};
    vector<Interval> v3(interval3, interval3 + sizeof(interval3)/sizeof(Interval));
    cout << Solution().eraseOverlapIntervals(v3) << endl;

    return 0;
}

贪心算法:

if(intervals[i].start >= intervals[j].end)
这里写图片描述
这里写图片描述

贪心算法代码:

#include <iostream>
#include <vector>

using namespace std;


/// Definition for an interval.
struct Interval {
    int start;
    int end;
    Interval() : start(0), end(0) {}
    Interval(int s, int e) : start(s), end(e) {}
};

bool compare(const Interval &a, const Interval &b){
    if(a.end != b.end)
        return a.end < b.end;
    return a.start < b.start;
}

/// 435. Non-overlapping Intervals
/// https://leetcode.com/problems/non-overlapping-intervals/description/
/// 贪心算法
/// 时间复杂度: O(n)
/// 空间复杂度: O(n)
class Solution {
public:
    int eraseOverlapIntervals(vector<Interval>& intervals) {

        if(intervals.size() == 0)
            return 0;

        sort(intervals.begin(), intervals.end(), compare);

        int res = 1;
        int pre = 0;
        for(int i = 1 ; i < intervals.size() ; i ++)
            if(intervals[i].start >= intervals[pre].end){
                res ++;
                pre = i;
            }

        return intervals.size() - res;
    }
};

int main() {

    Interval interval1[] = {Interval(1,2), Interval(2,3), Interval(3,4), Interval(1,3)};
    vector<Interval> v1(interval1, interval1 + sizeof(interval1)/sizeof(Interval));
    cout << Solution().eraseOverlapIntervals(v1) << endl;

    Interval interval2[] = {Interval(1,2), Interval(1,2), Interval(1,2)};
    vector<Interval> v2(interval2, interval2 + sizeof(interval2)/sizeof(Interval));
    cout << Solution().eraseOverlapIntervals(v2) << endl;

    Interval interval3[] = {Interval(1,2), Interval(2,3)};
    vector<Interval> v3(interval3, interval3 + sizeof(interval3)/sizeof(Interval));
    cout << Solution().eraseOverlapIntervals(v3) << endl;

    return 0;
}

疑问1:针对动态规划或者贪心排序参照点的选择可不可以是另外的情形,即:动态规划可不可以按照区间终止点来排序;在贪心算法中,可不可以按照区间起始点来排序?
这里写图片描述
疑问2:是不是所有能使用动态规划解决的问题,也可以使用贪心算法来解决呢?
当然不是这样的,下面的小结进行了解释。


贪心选择性质的证明

有些动态规划的问题可以用贪心算法来解决,这类问题都满足一个性质:贪心选择性质
贪心选择性质: 在求解一个最优化的问题中,我们使用贪心的方式选择了一组内容之后,不会影响剩下的子问题的求解。难点在于:验证一个问题是否满足贪心选择性质。


如果无法使用贪心算法,举出反例即可。

反例一:
这里写图片描述

反例二:

【279】Perfect Squares
这里写图片描述


如果无法举出反例,如何证明贪心算法的正确性

在算法设计领域中会用到的数学证明方法往往有方式:

  • 数学归纳法:递推的过程,很像动态规划的过程,先将基本的问题解决之后;假设规模为n的问题已经解决了,我们就能推导出规模为n+1的问题。适用领域:通常有一个显著的变量n在逐渐增加。
  • 反证法:先假设不正确,然后看能不能推导出矛盾。

例题:反证法证明Non-overlapping Intervals具有贪心选择性质

具体分析如下:
这里写图片描述
这里写图片描述
上面给出了我们的贪心选择策略,下面用反正法来证明这个策略是具有贪心选择性质的
这里写图片描述
下面从两点来解释为什么区间[s(i), f(i)] 可以替代区间 [s(j), f(j)]:

  区间结束点的比较: f(i)是当前所有选择中结尾最早的:f(i)<f(j),既然大区间 [s(j), f(j)] 是一个满足条件的解(不和之间的解区间产生重叠),那么小区间 [s(j), f(i)]也一定是一个满足条件的解(不和之间的解区间产生重叠)。
  区间起点的比较:假设中已经包含了不重叠的条件,所以区间[s(i), f(i)] 、[s(j), f(j)] 都是合法的选择,即 s(i)、s(j) 都不会和之前的区间发生重叠,所以区间[s(i), f(i)] 可以替代区间 [s(j), f(j)]。
这里写图片描述


反证法证明贪心选择性质的一般思路

这里写图片描述
利用反证明法,先假设提出的贪心算法A无法保证最优解,而问题的真正最优解算法为O,在其后的证明中,发现,算法A完全可以替代算法O,产生最优解。


贪心算法的实际应用

比如最小生成树,最短路径等,在实际中,贪心算法可能是指某些算法设计过程中的一个组成部分,单独以贪心算法出现的情况比较少见。

猜你喜欢

转载自blog.csdn.net/u010758410/article/details/80105484