貪心演算法

尐鱼儿發表於2024-11-14

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

貪心演算法的基本思路是從問題的某一個初始解出發一步一步地進行,根據某個優化測度,每一步都要確保能獲得區域性最優解。每一步只考慮一個資料,他的選取應該滿足區域性優化的條件。若下一個資料和部分最優解連在一起不再是可行解時,就不把該資料新增到部分解中,直到把所有資料列舉完,或者不能再新增演算法停。

貪心演算法不是對所有問題都能得到整體最優解,關鍵是貪心策略的選擇,選擇的貪心策略必須具備無後效性,即某個狀態以前的過程不會影響以後的狀態,只與當前狀態有關。

一句話:不求最優,僅僅求可行解。

選擇使用貪心演算法

 我們能夠依據貪心法的2個重要的性質去證明:貪心選擇性質和最優子結構性質

1、貪心選擇

  什麼叫貪心選擇?從字義上就是貪心也就是目光短線。貪圖眼前利益。在演算法中就是僅僅依據當前已有的資訊就做出選擇,並且以後都不會改變這次選擇。(這是和動態規劃法的主要差別)  

  所以對於一個詳細問題。要確定它是否具有貪心選擇性質,必須證明每做一步貪心選擇是否終於導致問題的總體最優解。

2、最優子結構

  當一個問題的最優解包括其子問題的最優解時,稱此問題具有最優子結構性質。

       運用貪心策略在每一次轉化時都取得了最優解。問題的最優子結構性質是該問題可用貪心演算法或動態規劃演算法求解的關鍵特徵。貪心演算法的每一次操作都對結果產生直接影響,而動態規劃則不是。貪心演算法對每個子問題的解決方案都做出選擇,不能回退;動態規劃則會根據以前的選擇結果對當前進行選擇,有回退功能。動態規劃主要運用於二維或三維問題,而貪心一般是一維問題

基本思路

貪心演算法的基本思路是從問題的某一個初始解觸發一步一步地進行,根據抹個優化測度,每一步都要確保能獲得區域性最優解,每一步值考慮一個資料,他的選取應該滿足區域性優化的條件。若下一個資料和部分最優解連載一起不再是可行解時,就不把改資料新增到部分解中,知道把所有資料列舉玩,或者不能在新增演算法停止。

過程

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

演算法特性

  1. 隨著演算法的進行,將積累起其它兩個集合:一個包含已經被考慮過並被選出的候選物件,另一個包含已經被考慮過但被丟棄的候選物件。
  2. 有一個函式來檢查一個候選物件的集合是否提供了問題的解答。該函式不考慮此時的解決方法是否最優。
  3. 還有一個函式檢查是否一個候選物件的集合是可行的,也即是否可能往該集合上新增更多的候選物件以獲得一個解。和上一個函式一樣,此時不考慮解決方法的最優性。
  4. 選擇函式可以指出哪一個剩餘的候選物件最有希望構成問題的解。
  5. 最後,目標函式給出解的值。
  6. 為了解決問題,需要尋找一個構成解的候選物件集合,它可以優化目標函式,貪婪演算法一步一步的進行。起初,演算法選出的候選物件的集合為空。接下來的每一步中,根據選擇函式,演算法從剩餘候選物件中選出最有希望構成解的物件。如果集合中加上該物件後不可行,那麼該物件就被丟棄並不再考慮;否則就加到集合裡。每一次都擴充集合,並檢查該集合是否構成解。如果貪婪演算法正確工作,那麼找到的第一個解通常是最優的。

一般求解過程

  使用貪心法求解能夠依據下面幾個方面進行(終於也相應著每步程式碼的實現),以找零錢為例:

  1、候選集合(C)

    通過一個候選集合C作為問題的可能解。(終於解均取自於候選集合C)

    比如。在找零錢問題中,各種面值的貨幣構成候選集合。

  2、解集合(S)

    每完畢一次貪心選擇,將一個解放入S。終於獲得一個完整解S

  3、解決函式(solution)

    檢查解集合S是否構成問題的完整解。

    比如,在找零錢問題中。解決函式是已付出的貨幣金額恰好等於應付款。

  4、選擇函式(select)

    即貪心策略。這是貪心法的關鍵,選擇出最有希望構成問題的解的物件。

(這個選擇函式通常和目標函式有關)

   比如,在找零錢問題中,貪心策略就是在候選集合中選擇面值最大的貨幣。

  5、可行函式(feasible)

    檢查解集合中增加一個候選物件是否可行。(增加下一個物件後是不是滿足約束條件)

    比如。在找零錢問題中,可行函式是每一步選擇的貨幣和已付出的貨幣相加不超過應付款。

例子

1.活動選擇問題

有n個需要在同一天使用同一個教室的活動a1,a2,…,an,教室同一時刻只能由一個活動使用。每個活動ai都有一個開始時間si和結束時間fi 。一旦被選擇後,活動ai就佔據半開時間區間[si,fi)。如果[si,fi]和[sj,fj]互不重疊,ai和aj兩個活動就可以被安排在這一天。該問題就是要安排這些活動使得儘量多的活動能不衝突的舉行。貪心演算法

問題分析:
活動安排問題要求安排一系列爭用某一公共資源的活動。用貪心演算法可提供一個簡單、漂亮的方法,使盡可能多的活動能相容的使用公共資源。設有n個活動的集合{0,1,2,…,n-1},其中每個活動都要求使用同一資源,如會場等,而在同一時間內只有一個活動能使用這一資源。每個活動i都有一個要求使用該資源的起始時間starti和一個結束時間endi,且starti<endi。如選擇了活動i,則它在半開時間區間[starti,endi)內佔用資源。若區間[starti,endi)與區間[startj,endj)不相交,稱活動i與活動j是相容的。也就是說,當startj≥endi或starti≥endj時,活動i與活動j相容。活動安排問題就是在所給的活動集合中選出最多的不相容活動。
活動安排問題就是要在所給的活動集合中選出最大的相容活動子集合,是可以用貪心演算法有效求解的很好例子。該問題要求高效地安排一系列爭用某一公共資源的活動。貪心演算法提供了一個簡單、漂亮的方法使得儘可能多的活動能相容地使用公共資源。


演算法設計:
若被檢查的活動i的開始時間starti小於最近選擇的活動j的結束時間endj,則不選擇活動i,否則選擇活動i加入集合中。運用該演算法解決活動安排問題的效率極高。當輸入的活動已按結束時間的非減序排列,演算法只需O(n)的時間安排n個活動,使最多的活動能相容地使用公共資源。如果所給出的活動未按非減序排列,可以用O(nlogn)的時間重排。

#include<cstdio>  
#include<iostream>   
#include<algorithm>   
using namespace std;      
int N;  
struct Act  
{  
    int start;  
    int end;  
}act[100010];  
  
bool cmp(Act a,Act b)    
{    
    return a.end<b.end;    
}   
  
int greedy_activity_selector()    
{    
    int num=1,i=1;     
    for(int j=2;j<=N;j++)    
    {    
        if(act[j].start>=act[i].end)    
        {    
            i=j;    
            num++;    
        }    
    }    
    return num;  
}  
  
int main()    
{    
    int t;  
    scanf("%d",&t);  
    while(t--)  
    {  
        scanf("%d",&N);  
        for(int i=1;i<=N;i++)  
        {  
            scanf("%lld %lld",&act[i].start,&act[i].end);  
        }  
        act[0].start=-1;  
        act[0].end=-1;  
        sort(act+1,act+N+1,cmp);   
        int res=greedy_activity_selector();  
        cout<<res<<endl;    
    }  
}    
複製程式碼

2.錢幣找零問題

這個問題在我們的日常生活中就更加普遍了。假設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;
}
複製程式碼

3.揹包問題

有一個揹包,揹包容量是M=150kg。有7個物品,物品不可以分割成任意大小。要求儘可能讓裝入揹包中的物品總價值最大,但不能超過總容量。貪心演算法

問題分析
目標函式: ∑pi最大,使得裝入揹包中的所有物品pi的價值加起來最大。

約束條件:裝入的物品總重量不超過揹包容量:∑wi<=M( M=150)

貪心策略:

⑴根據貪心的策略,每次挑選價值最大的物品裝入揹包,得到的結果是否最優?

⑵每次挑選所佔重量最小的物品裝入是否能得到最優解?

⑶每次選取單位重量價值最大的物品,成為解本題的策略。
⑴貪心策略:選取價值最大者。
W=30
物品:A B C
重量:28 12 12
價值:30 20 20
根據策略,首先選取物品A,接下來就無法再選取了,可是,選取B、C則更好。

⑵貪心策略:選取重量最小。它的反例與第一種策略的反例差不多。

⑶貪心策略:選取單位重量價值最大的物品。

反例:
W=30
物品:A B C
重量:28 20 10
價值:28 20 10
根據策略,三種物品單位重量價值一樣,程式無法依據現有策略作出判斷,如果選擇A,則答案錯誤。
【注意:如果物品可以分割為任意大小,那麼策略3可得最優解】
對於選取單位重量價值最大的物品這個策略,可以再加一條優化的規則:對於單位重量價值一樣的,則優先選擇重量小的!這樣,上面的反例就解決了。
但是,如果題目是如下所示,這個策略就也不行了。
W=40
物品:A B C
重量:25 20 15
價值:25 20 15
  • 演算法設計:
  1. 計算出每個物品單位重量的價值
  2. 按單位價值從大到小將物品排序
  3. 根據揹包當前所剩容量選取物品
  4. 如果揹包的容量大於當前物品的重量,那麼就將當前物品裝進去。否則,那麼就將當前物品捨去,然後跳出迴圈結束。

#include<iostream>
#include<algorithm>
using namespace std;
typedef struct{
    int w;
    int v;
    double avg;
}P;
bool cmp(P a,P b){
    return a.avg>b.avg;
}
int main(){
    P *p;
    int n,i,m;//n 物品個數 m揹包容量
    while(cin>>n>>m){
        p=new P[n];
        for(i=0;i<n;i++){
            cin>>p[i].w>>p[i].v;
            p[i].avg=p[i].v/p[i].w*1.0;
        }
        sort(p,p+n,cmp);
        int maxvalue=0;
        for(i=0;i<n;i++){
            if(p[i].w<=m){
                m-=p[i].w;
                maxvalue+=p[i].v;
            }else{
                break;
            }
        }
        cout<<maxvalue<<endl;
    }
    return 0;
}
複製程式碼


相關文章