貪婪演算法 貪心演算法(Greedy Algorithm) 簡介貪心演算法,又名貪婪法,是尋找最優解問題的常用方法,這種方法模式一般將求解過程分成若幹個步驟,但每個步驟都應用貪心原則,選取當前狀態下最好/最優的選擇(局部最有利的選擇),並以此希望最後堆疊出的結果也是最好/最優的解。{看著這個名字,貪心,貪 ...
貪婪演算法
貪心演算法(Greedy Algorithm) 簡介貪心演算法,又名貪婪法,是尋找最優解問題的常用方法,這種方法模式一般將求解過程分成若幹個步驟,但每個步驟都應用貪心原則,選取當前狀態下最好/最優的選擇(局部最有利的選擇),並以此希望最後堆疊出的結果也是最好/最優的解。{看著這個名字,貪心,貪婪這兩字的內在含義最為關鍵。這就好像一個貪婪的人,他事事都想要眼前看到最好的那個,看不到長遠的東西,也不為最終的結果和將來著想,貪圖眼前局部的利益最大化,有點走一步看一步的感覺。}
貪婪法的基本步驟:
步驟1:從某個初始解出發;
步驟2:採用迭代的過程,當可以向目標前進一步時,就根據局部最優策略,得到一部分解,縮小問題規模;
步驟3:將所有解綜合起來。
事例一:找零錢問題假設你開了間小店,不能電子支付,錢櫃里的貨幣只有 25 分、10 分、5 分和 1 分四種硬幣,如果你是售貨員且要找給客戶 41 分錢的硬幣,如何安排才能找給客人的錢既正確且硬幣的個數又最少?這裡需要明確的幾個點:1.貨幣只有 25 分、10 分、5 分和 1 分四種硬幣;2.找給客戶 41 分錢的硬幣;3.硬幣最少化思考,能使用我們今天學到的貪婪演算法嗎?怎麼做?(回顧一下上文貪婪法的基本步驟,1,2,3)
- 找給顧客sum_money=41分錢,可選擇的是25 分、10 分、5 分和 1 分四種硬幣。能找25分的,不找10分的原則,初次先找給顧客25分;
- 還差顧客sum_money=41-25=16。然後從25 分、10 分、5 分和 1 分四種硬幣選取局部最優的給顧客,也就是選10分的,此時sum_money=16-10=6。重覆迭代過程,還需要sum_money=6-5=1,sum_money=1-1=0。至此,顧客收到零錢,交易結束;
- 此時41分,分成了1個25,1個10,1個5,1個1,共四枚硬幣。
編程實現
#include<iostream>
using namespace std;
#define ONEFEN 1
#define FIVEFEN 5
#define TENFEN 10
#define TWENTYFINEFEN 25
int main()
{
int sum_money=41;
int num_25=0,num_10=0,num_5=0,num_1=0;
//不斷嘗試每一種硬幣
while(money>=TWENTYFINEFEN) { num_25++; sum_money -=TWENTYFINEFEN; }
while(money>=TENFEN) { num_10++; sum_money -=TENFEN; }
while(money>=FIVEFEN) { num_5++; sum_money -=FIVEFEN; }
while(money>=ONEFEN) { num_1++; sum_money -=ONEFEN; }
//輸出結果
cout<< "25分硬幣數:"<<num_25<<endl;
cout<< "10分硬幣數:"<<num_10<<endl;
cout<< "5分硬幣數:"<<num_5<<endl;
cout<< "1分硬幣數:"<<num_1<<endl;
return 0;
}
事例二:背包最大價值問題有一個背包,最多能承載重量為 C=150的物品,現在有7個物品(物品不能分割成任意大小),編號為 1~7,重量分別是 wi=[35,30,60,50,40,10,25],價值分別是 pi=[10,40,30,50,35,40,30],現在從這 7 個物品中選擇一個或多個裝入背包,要求在物品總重量不超過 C 的前提下,所裝入的物品總價值最高。這裡需要明確的幾個點:
- 每個物品都有重量和價值兩個屬性;
- 每個物品分被選中和不被選中兩個狀態(後面還有個問題,待討論);
- 可選物品列表已知,背包總的承重量一定。
所以,構建描述每個物品的數據體結構 OBJECT和背包問題定義為
//typedef是類型定義的意思
//定義待選物體的結構體類型
typedef struct tagObject
{
int weight;
int price;
int status;
}OBJECT;
//定義背包問題
typedef struct tagKnapsackProblem
{
vector<OBJECT>objs;
int totalC;
}KNAPSACK_PROBLEM;
這裡採用定義結構體的形式,主要是可以減少代碼的書寫量,可以實現代碼的復用性和可擴展性,簡化,提高可讀性。就是貪圖簡單方便,規避繁瑣。
如下,實例化
objectsOBJECT objects[] = { { 35,10,0 },{ 30,40,0 },{ 60,30,0 },{ 50,50,0 },
{ 40,35,0 },{ 10,40,0 },{ 25,30,0 } };
思考:如何選,才使得裝進背包的價值最大呢?
策略1:價值主導選擇,每次都選價值最高的物品放進背包;
策略2:重量主導選擇,每次都選擇重量最輕的物品放進背包;
策略3:價值密度主導選擇,每次選擇都選價值/重量最高的物品放進背包。
(貪心法則:求解過程分成若幹個步驟,但每個步驟都應用貪心原則,選取當前狀態下最好的或最優的選擇(局部最有利的選擇),並以此希望最後堆疊出的結果也是最好或最優的解)
策略1:價值主導選擇,每次都選價值最高的物品放進背包根據這個策略最終選擇裝入背包的物品編號依次是 4、2、6、5,此時包中物品總重量是 130,總價值是 165。
//遍歷沒有被選的objs,並且選擇price最大的物品,返回被選物品的編號
int Choosefunc1(std::vector<OBJECT>& objs, int c)
{
int index = -1; //-1表示背包容量已滿
int max_price = 0;
//在objs[i].status == 0的物品里,遍歷挑選objs[i].price最大的物品
for (int i = 0; i < static_cast<int>(objs.size()); i++)
{
if ((objs[i].status == 0) && (objs[i].price > max_price ))//objs沒有被選,並且price> max_price
{
max_price = objs[i].price;
index = i;
}
}
return index;
}
策略2:重量主導選擇,每次都選擇重量最輕(小)的物品放進背包根據這個策略最終選擇裝入背包的物品編號依次是 6、7、2、1、5,此時包中物品總重量是 140,總價值是 155。
int Choosefunc2(std::vector<OBJECT>& objs, int c)
{
int index = -1;
int min_weight= 10000;
for (int i = 0; i < static_cast<int>(objs.size()); i++)
{
if ((objs[i].status == 0) && (objs[i].weight < min_weight))
{
min_weight= objs[i].weight;
index = i;
}
}
return index;
}
策略3:價值密度主導選擇,每次選擇都選價值/重量最高(大)的物品放進背包物品的價值密度 si 定義為 pi/wi,這 7 件物品的價值密度分別為 si=[0.286,1.333,0.5,1.0,0.875,4.0,1.2]。根據這個策略最終選擇裝入背包的物品編號依次是 6、2、7、4、1,此時包中物品的總重量是 150,總價值是 170。
int Choosefunc3(std::vector<OBJECT>& objs, int c)
{
int index = -1;
double max_s = 0.0;
for (int i = 0; i < static_cast<int>(objs.size()); i++)
{
if (objs[i].status == 0)
{
double si = objs[i].price;
si = si / objs[i].weight;
if (si > max_s)
{
max_s = si;
index = i;
}
}
}
return index;
}
有了物品,有了方法,下麵就是將兩者結合起來的貪心演算法
GreedyAlgovoid GreedyAlgo(KNAPSACK_PROBLEM *problem, SELECT_POLICY spFunc)
{
int idx;
int sum_weight_current = 0;
//先選
while ((idx = spFunc(problem->objs, problem->totalC- sum_weight_current)) != -1)
{ //再檢查,是否能裝進去
if ((sum_weight_current + problem->objs[idx].weight) <= problem->totalC)
{
problem->objs[idx].status = 1;//如果背包沒有裝滿,還可以再裝,標記下裝進去的物品狀態為1
sum_weight_current += problem->objs[idx].weight;//把這個idx的物體的重量裝進去,計算當前的重量
}
else
{
//不能選這個物品了,做個標記2後重新選剩下的
problem->objs[idx].status = 2;
}
}
PrintResult(problem->objs);//輸出函數的定義,查看源代碼
}
註意:這裡對objs[idx].status定義了三種狀態,分別是待選擇為0(初始所有狀態均為0),裝進包里變為1,判斷不符合變為2,這樣最後只需要拿去狀態為1的即可。主函數部分
OBJECT objects[] = { { 35,10,0 },{ 30,40,0 },{ 60,30,0 },{ 50,50,0 },
{ 40,35,0 },{ 10,40,0 },{ 25,30,0 } };
int main()
{
KNAPSACK_PROBLEM problem;
problem.objs.assign(objects, objects + 7);//assign賦值,std::vector::assign
problem.totalC = 150;
cout << "Start to find the best way ,NOW" << endl;
GreedyAlgo(&problem, Choosefunc3);
system("pause");
return 0;
}
查看策略3的輸出結果:
但是,我們再回顧一下第一個事例問題現在問題變了,還是需要找給顧客41分錢,現在的貨幣只有 25 分、20分、10 分、5 分和 1 分四種硬幣;該怎麼辦?按照貪心演算法的三個步驟:1.41分,局部最優化原則,先找給顧客25分;2.此時,41-25=16分,還需要找給顧客10分,然後5分,然後1分;3.最終,找給顧客一個25分,一個10分,一個5分,一個1分,共四枚硬幣。是不是覺得哪裡不太對,如果給他2個20分,加一個1分,三枚硬幣就可以了呢?^_^;總結:貪心演算法的優缺點優點:簡單,高效,省去了為了找最優解可能需要窮舉操作,通常作為其它演算法的輔助演算法來使用;缺點:不從總體上考慮其它可能情況,每次選取局部最優解,不再進行回溯處理,所以很少情況下得到最優解。