【基礎演算法】(07)五大常用演算法之三:貪心演算法

西海Tech發表於2017-10-21

【基礎演算法】(07)五大常用演算法之三:貪心演算法

Auther: Thomas Shen
E-mail: Thomas.shen3904@qq.com
Date: 2017/10/23
All Copyrights reserved !


1. 簡述:

本系列介紹了五大常用演算法,其中本文是第三篇,介紹了 ‘貪心演算法’ 的細節內容。

所謂貪心演算法是指,在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的僅是在某種意義上的區域性最優解。

貪心演算法沒有固定的演算法框架,演算法設計的關鍵是貪心策略的選擇。必須注意的是,貪心演算法不是對所有問題都能得到整體最優解,選擇的貪心策略必須具備無後效性,即某個狀態以後的過程不會影響以前的狀態,只與當前狀態有關。所以對所採用的貪心策略一定要仔細分析其是否滿足無後效性。

2. 演算法原理:

2.1 基本思路:

1.建立數學模型來描述問題。
2.把求解的問題分成若干個子問題。
3.對每一子問題求解,得到子問題的區域性最優解。
4.把子問題的解區域性最優解合成原來解問題的一個解。

2.2 適用問題:

貪心策略適用的前提是:區域性最優策略能導致產生全域性最優解。

實際上,貪心演算法適用的情況很少。一般,對一個問題分析是否適用於貪心演算法,可以先選擇該問題下的幾個實際資料進行分析,就可做出判斷。

因為用貪心演算法只能通過解區域性最優解的策略來達到全域性最優解,因此,一定要注意判斷問題是否適合採用貪心演算法策略,找到的解是否一定是問題的最優解。

3. 實現框架:

    從問題的某一初始解出發;
    while (能朝給定總目標前進一步)
    { 
        利用可行的決策,求出可行解的一個解元素;
    }
    由所有解元素組合成問題的一個可行解;

4. 應用案例:

4.1 案例一:

錢幣找零問題:

這個問題在我們的日常生活中就更加普遍了。假設1元、2元、5元、10元、20元、50元、100元的紙幣分別有c0, c1, c2, c3, c4, c5, c6張。現在要用這些錢來支付K元,至少要用多少張紙幣?用貪心演算法的思想,很顯然,每一步儘可能用面值大的紙幣即可。在日常生活中我們自然而然也是這麼做的。在程式中已經事先將Value按照從小到大的順序排好。

#include<iostream>  
#include<algorithm>  
using namespace std;  
const int N=7;   
int Count[N]={3,0,2,1,0,3,5};  
int Value[N]={1,2,5,10,20,50,100};  

int solve(int money)   
{  
    int num=0;  
    for(int i=N-1;i>=0;i--)   
    {  
        int c=min(money/Value[i],Count[i]);  
        money=money-c*Value[i];  
        num+=c;  
    }  
    if(money>0) num=-1;  
    return num;  
}  

int main()   
{  
    int money;  
    cin>>money;  
    int res=solve(money);  
    if(res!=-1) cout<<res<<endl;  
    else cout<<"NO"<<endl;  
}  
4.2 案例二:

多機排程問題:

n個作業組成的作業集,可由m臺相同機器加工處理。要求給出一種作業排程方案,使所給的n個作業在儘可能短的時間內由m臺機器加工處理完成。作業不能拆分成更小的子作業;每個作業均可在任何一臺機器上加工處理。這個問題是NP完全問題,還沒有有效的解法(求最優解),但是可以用貪心選擇策略設計出較好的近似演算法(求次優解)。當n<=m時,只要將作業時間區間分配給作業即可;當n>m時,首先將n個作業從大到小排序,然後依此順序將作業分配給空閒的處理機。也就是說從剩下的作業中,選擇需要處理時間最長的,然後依次選擇處理時間次長的,直到所有的作業全部處理完畢,或者機器不能再處理其他作業為止。如果我們每次是將需要處理時間最短的作業分配給空閒的機器,那麼可能就會出現其它所有作業都處理完了只剩所需時間最長的作業在處理的情況,這樣勢必效率較低。在下面的程式碼中沒有討論n和m的大小關係,把這兩種情況合二為一了。

#include<iostream>    
#include<algorithm>      
using namespace std;    
int speed[10010];    
int mintime[110];    

bool cmp( const int &x,const int &y)    
{    
    return x>y;    
}    

int main()    
{    
    int n,m;           
    memset(speed,0,sizeof(speed));    
    memset(mintime,0,sizeof(mintime));    
    cin>>n>>m;    
    for(int i=0;i<n;++i) cin>>speed[i];    
    sort(speed,speed+n,cmp);    
    for(int i=0;i<n;++i)     
    {   
        *min_element(mintime,mintime+m)+=speed[i];     
    }     
    cout<<*max_element(mintime,mintime+m)<<endl;   
}  
4.3 案例三:

小船過河問題:

POJ1700是一道經典的貪心演算法例題。題目大意是隻有一艘船,能乘2人,船的執行速度為2人中較慢一人的速度,過去後還需一個人把船劃回來,問把n個人運到對岸,最少需要多久。先將所有人過河所需的時間按照升序排序,我們考慮把單獨過河所需要時間最多的兩個旅行者送到對岸去,有兩種方式:

  1. 最快的和次快的過河,然後最快的將船劃回來;次慢的和最慢的過河,然後次快的將船劃回來,所需時間為:t[0]+2*t[1]+t[n-1];
  2. 最快的和最慢的過河,然後最快的將船劃回來,最快的和次慢的過河,然後最快的將船劃回來,所需時間為:2*t[0]+t[n-2]+t[n-1]。
    算一下就知道,除此之外的其它情況用的時間一定更多。每次都運送耗時最長的兩人而不影響其它人,問題具有貪心子結構的性質。
#include<iostream>  
#include<algorithm>  
using namespace std;  

int main()  
{  
    int a[1000],t,n,sum;  
    scanf("%d",&t);  
    while(t--)  
    {  
        scanf("%d",&n);  
        sum=0;  
        for(int i=0;i<n;i++) scanf("%d",&a[i]);  
        while(n>3)  
        {  
            sum=min(sum+a[1]+a[0]+a[n-1]+a[1],sum+a[n-1]+a[0]+a[n-2]+a[0]);  
            n-=2;  
        }  
        if(n==3) sum+=a[0]+a[1]+a[2];  
        else if(n==2) sum+=a[1];  
        else sum+=a[0];  
        printf("%d\n",sum);  
    }  
}  
4.4 案例四:

區間覆蓋問題:

POJ1328是一道經典的貪心演算法例題。題目大意是假設海岸線是一條無限延伸的直線。陸地在海岸線的一側,而海洋在另一側。每一個小的島嶼是海洋上的一個點。雷達坐落於海岸線上,只能覆蓋d距離,所以如果小島能夠被覆蓋到的話,它們之間的距離最多為d。題目要求計算出能夠覆蓋給出的所有島嶼的最少雷達數目。對於每個小島,我們可以計算出一個雷達所在位置的區間。

這裡寫圖片描述

問題轉化為如何用盡可能少的點覆蓋這些區間。先將所有區間按照左端點大小排序,初始時需要一個點。如果兩個區間相交而不重合,我們什麼都不需要做;如果一個區間完全包含於另外一個區間,我們需要更新區間的右端點;如果兩個區間不相交,我們需要增加點並更新右端點。

#include<cmath>  
#include<iostream>  
#include<algorithm>  
using namespace std;  
struct Point  
{  
    double x;  
    double y;  
}point[1000];  

int cmp(const void *a, const void *b)  
{  
    return (*(Point *)a).x>(*(Point *)b).x?1:-1;  
}  

int main()  
{  
    int n,d;  
    int num=1;  
    while(cin>>n>>d)  
    {  
        int counting=1;  
        if(n==0&&d==0) break;  
        for(int i=0;i<n;i++)  
        {  
            int x,y;  
            cin>>x>>y;  
            if(y>d)  
            {  
                counting=-1;  
            }  
            double t=sqrt(d*d-y*y);  
            //轉化為最少區間的問題   
            point[i].x=x-t;  
            //區間左端點   
            point[i].y=x+t;  
            //區間右端點   
        }  
        if(counting!=-1)  
        {  
            qsort(point,n,sizeof(point[0]),cmp);  
            //按區間左端點排序   
            double s=point[0].y;  
            //區間右端點   
            for(int i=1;i<n;i++)  
            {  
                if(point[i].x>s)  
                //如果兩個區間沒有重合,增加雷達數目並更新右端點   
                {  
                    counting++;  
                    s=point[i].y;   
                }  
                else if(point[i].y<s)  
                //如果第二個區間被完全包含於第一個區間,更新右端點   
                {  
                    s=point[i].y;  
                }  
            }  
        }  
        cout<<"Case "<<num<<':'<<' '<<counting<<endl;  
        num++;   
    }  
}     
4.5 案例五:

銷售比賽:

假設有偶數天,要求每天必須買一件物品或者賣一件物品,只能選擇一種操作並且不能不選,開始手上沒有這種物品。現在給你每天的物品價格表,要求計算最大收益。首先要明白,第一天必須買,最後一天必須賣,並且最後手上沒有物品。那麼除了第一天和最後一天之外我們每次取兩天,小的買大的賣,並且把賣的價格放進一個最小堆。如果買的價格比堆頂還大,就交換。這樣我們保證了賣的價格總是大於買的價格,一定能取得最大收益。

#include<queue>  
#include<vector>  
#include<cstdio>  
#include<cstdlib>  
#include<cstring>  
#include<iostream>  
#include<algorithm>  
using namespace std;  
long long int price[100010],t,n,res;  

int main()  
{  
    ios::sync_with_stdio(false);  
    cin>>t;  
    while(t--)  
    {  
        cin>>n;  
        priority_queue<long long int, vector<long long int>, greater<long long int> > q;  
        res=0;  
        for(int i=1;i<=n;i++)  
        {  
            cin>>price[i];  
        }  
        res-=price[1];  
        res+=price[n];  
        for(int i=2;i<=n-1;i=i+2)  
        {  
            long long int buy=min(price[i],price[i+1]);  
            long long int sell=max(price[i],price[i+1]);  
            if(!q.empty())  
            {  
                if(buy>q.top())  
                {  
                    res=res-2*q.top()+buy+sell;  
                    q.pop();  
                    q.push(buy);  
                    q.push(sell);  
                }  
                else  
                {  
                    res=res-buy+sell;  
                    q.push(sell);  
                }  
            }  
            else  
            {  
                res=res-buy+sell;  
                q.push(sell);  
            }  
        }       
        cout<<res<<endl;  
    }  
}  

References. :

相關文章