1. 程式人生 > >9. 貪心演算法

9. 貪心演算法

一. 貪心基礎 Assign Cookies

假設你是一位很棒的家長,想要給你的孩子們一些小餅乾。但是,每個孩子最多隻能給一塊餅乾。對每個孩子 i ,都有一個胃口值 g(i) ,這是能讓孩子們滿足胃口的餅乾的最小尺寸;並且每塊餅乾 j ,都有一個尺寸 s(j) 。如果 s(j) >= g(i) ,我們可以將這個餅乾 j 分配給孩子 i ,這個孩子會得到滿足。你的目標是儘可能滿足越多數量的孩子,並輸出這個最大數值。

注意:

你可以假設胃口值為正。
一個小朋友最多隻能擁有一塊餅乾。

示例 1:

輸入: [1,2,3], [1,1]

輸出: 1

解釋: 
你有三個孩子和兩塊小餅乾,3個孩子的胃口值分別是:1,2,3。
雖然你有兩塊小餅乾,由於他們的尺寸都是1,你只能讓胃口值是1的孩子滿足。
所以你應該輸出1。
示例 2:

輸入: [1,2], [1,2,3]

輸出: 2

解釋: 
你有兩個孩子和三塊小餅乾,2個孩子的胃口值分別是1,2。
你擁有的餅乾數量和尺寸都足以讓所有孩子滿足。
所以你應該輸出2.

解決思路

先把餅乾按大小排序, 小朋友按胃口大小排序
如果最大的餅乾能滿足胃口最大的小朋友, 就把最大的餅乾給他,
否則就把最大的餅乾給胃口第二大的小朋友....以此類推

程式碼

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

/// 455. Assign Cookies
/// https://leetcode.com/problems/assign-cookies/description/
/// 先嚐試滿足最貪心的小朋友
/// 時間複雜度: O(nlogn)    主要體現在排序中
/// 空間複雜度: O(1)
class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        // s表示餅乾   g表示小朋友
        sort(g.begin(), g.end(), greater<int>()); // 從大到小排序, greater表示從大到小, 否則預設從小到大
        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;
    }
};

二. 貪心演算法和動態規劃的關係 Non-overiapping Intervals

給定一個區間的集合,找到需要移除區間的最小數量,使剩餘區間互不重疊。  

注意:

可以認為區間的終點總是大於它的起點。  
區間 [1,2] 和 [2,3] 的邊界相互“接觸”,但沒有相互重疊。 
示例 1:

輸入: [ [1,2], [2,3], [3,4], [1,3] ]

輸出: 1

解釋: 移除 [1,3] 後,剩下的區間沒有重疊。 
示例 2:

輸入: [ [1,2], [1,2], [1,2] ]

輸出: 2

解釋: 你需要移除兩個 [1,2] 來使剩下的區間沒有重疊。 
示例 3:

輸入: [ [1,2], [2,3] ]

輸出: 0

解釋: 你不需要移除任何區間,因為它們已經是無重疊的了。 

解題思路

暴力解法: 先要排序, 方便判斷不重疊,找出所有子區間的組合, 之後判斷它不重疊。 O((2^n)*n)  

動態規劃: 先要排序, 方便判斷不重疊,使用最長上升子序列 的解法

動態規劃程式碼

#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]為結尾的區間能構成的最長不重疊區間序列 個數, 開始時 都為1
        vector<int> memo(intervals.size(), 1);
        for(int i = 1 ; i < intervals.size() ; i ++)
            // memo[i]
            for(int j = 0 ; j < i ; j ++) // 掃描i前面的 所有區間
                if(intervals[i].start >= intervals[j].end) // 滿足條件, 則計算 到第i個區間時, 可以連起來的區間數
                    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; // 得到要去除的 最少區間數
    }
};

貪心演算法

按照區間的結尾排序,
每次選擇結尾最早的, 且和前一個區間不重疊的區間

程式碼

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

三. 貪心選擇性質的證明

一般過程如下:
假設貪心演算法為A 不是最優解,
則存在一個最優演算法O

只要證明A完全可以代替O,且不影響求出最優解 即可

上一節中貪心選擇性質的證明

題目: 給定一組區間, 問最多保留多少個區間, 可以讓這些區間之間相互不重疊。

貪心演算法: 按照區間的結尾排序, 每次選擇結尾最早的, 且和前一個區間不重疊的區間


反證法證明:

貪心演算法的 某次選擇的是[s(i), f(i)] 這個區間; 其中f(i)是當前所有選擇中結尾最早的

假設這個選擇不是最優解。 也就是說, 如果這個問題的最優解為k, 則這個問題得到的解, 最多為k-1


假設最優解在這一步選擇的是[s(j), f(j)], f(j)>f(i)

顯然 可以將[s(i),f(i)]替換[s(j),f(j)],  而不影響後序的區間選擇:

    f(j)>f(i)  顯然不會影響後面選擇
    而s(i)和s(i), 則隱藏在條件中: 
    前面的m次選擇是一樣的, 到這一次 使用'貪心選擇'還是 '最優選擇', 都不會對全面已經選擇過的區間  產生影響了.