算法設計與分析---第5章 貪心算法

在求最優解問題的過程中,依據某種貪心標準,從問題的初始狀態出發,直接去求每一步的最優解,通過若干次的貪心選擇,最終得出整個問題的最優解,這種求解方法就是貪心算法
從貪心算法的定義可以看出,貪心算法不是從整體上考慮問題,它所做出的選擇只是在某種意義上的局部最優解,而由問題自身的特性決定了該題運用貪心算法可以得到最優解。
如果一個問題可以同時用幾種方法解決,貪心算法應該是最好的選擇之一。
5.1 活動安排問題
設有n個活動的集合E={1,2,…,n},其中每個活動都要求使用同一資源,如演講會場等,而在同一時間內只有一個活動能使用這一資源。 每個活動i都有一個要求使用該資源的起始時間si和一個結束時間fi,且sifi。如果選擇了活動i,則它在半開時間區間[si ,fi )內佔用資源。若區間[si ,fi )與區間[sj,fj )不相交,則稱活動i與活動j是相容的。當 si ≥ fj 或 sj ≥ fi 時,活動i與活動j相容。 活動安排問題就是在所給的活動集合中選出最大的相容活動子集合。
數據結構

struct action{  
	int s;   //起始時間  
	int f;   //結束時間  
	int index;  //活動的編號 
};

活動的集合E記爲數組:

action a[1000];

按活動的結束時間升序排序
排序比較因子:

bool cmp(const action &a, const action &b) 
{  
	if (a.f<=b.f) return true;  
	return false; 
}

使用標準模板庫函數排序(下標0未用):

sort(a, a+n+1, cmp);

算法5.1 計算活動安排問題的貪心算法

//形參數組b用來記錄被選中的活動 
void GreedySelector(int n, action a[], bool b[]) 
{   
	b[1] = true;     //第1個活動是必選的   
	//記錄最近一次加入到集合b中的活動
	int preEnd = 1;   
	for(int i=2; i<=n; i++)     
		if (a[i].s>=a[preEnd].f)     
		{
			b[i] = true;       
			preEnd = i;     
		} 
}

5.2 貪心算法的理論基礎
貪心算法是一種在每一步選擇中都採取在當前狀態下最好或最優的選擇,希望得到結果是最好或最優的算法。
這種策略是一種很簡潔的方法,對許多問題它能產生整體最優解,但不能保證總是有效,因爲它不是對所有問題都能得到整體最優解。
利用貪心策略解題,需要解決兩個問題:
(1)該題是否適合於用貪心策略求解;
(2)如何選擇貪心標準,以得到問題的最優/較優解。
5.2.1 貪心選擇性質
貪心選擇性質是指所求問題的整體最優解可以通過一系列局部最優的選擇,即貪心選擇來達到。
(1)在動態規劃算法中,每步所做的選擇往往依賴於相關子問題的解,因而只有在解出相關子問題後,才能做出選擇。
(2)在貪心算法中,僅在當前狀態下做出最好選擇,即局部最優選擇,然後再去解出這個選擇後產生的相應的子問題。
5.2.2 最優子結構性質
當一個問題的最優解包含其子問題的最優解時,稱此問題具有最優子結構性質。
貪心算法的每一次操作都對結果產生直接影響,而動態規劃則不是。
貪心算法對每個子問題的解決方案都做出選擇,不能回退;動態規劃則會根據以前的選擇結果對當前進行選擇,有回退功能。
動態規劃主要運用於二維或三維問題,而貪心一般是一維問題。
5.2.3 貪心算法的求解過程
使用貪心算法求解問題應該考慮如下幾個方面:
(1)候選集合A:爲了構造問題的解決方案,有一個候選集合A作爲問題的可能解,即問題的最終解均取自於候選集合A。
(2)解集合S:隨着貪心選擇的進行,解集合S不斷擴展,直到構成滿足問題的完整解。
(3)解決函數solution:檢查解集合S是否構成問題的完整解。
(4)選擇函數select:即貪心策略,這是貪心法的關鍵,它指出哪個候選對象最有希望構成問題的解,選擇函數通常和目標函數有關。
(5)可行函數feasible:檢查解集合中加入一個候選對象是否可行,即解集合擴展後是否滿足約束條件。
算法5.2貪心算法的一般流程

//A是問題的輸入集合即候選集合 
Greedy(A) 
{   
	S={ };           //初始解集合爲空集   
	while (not solution(S))  //集合S沒有構成問題的一個解   
	{
		x = select(A);     //在候選集合A中做貪心選擇     
		if feasible(S, x)    //判斷集合S中加入x後的解是否可行       
			S = S+{x};       
			A = A-{x};   
	}
	return S;

5.3 揹包問題
給定一個載重量爲M的揹包,考慮n個物品,其中第i個物品的重量 ,價值wi (1≤i≤n),要求把物品裝滿揹包,且使揹包內的物品價值最大。
有兩類揹包問題(根據物品是否可以分割),如果物品不可以分割,稱爲0—1揹包問題(動態規劃);如果物品可以分割,則稱爲揹包問題(貪心算法)
假設xi是物品i裝入揹包的部分(0<=xi<=1),當xi=0時表示物品i沒有被裝入書包;當xi=1時表示物品i被全部裝入揹包。其中,0<=xi<=1。
數據結構

struct bag{ 	
	int w;			//物品的重量 	
	int v;			//物品的價值 	
	double c;		//性價比 
}a[1001];			//存放物品的數組

排序因子(按性價比降序):

bool cmp(bag a, bag b){ 	
	return a.c >= b.c; 
}

使用標準模板庫函數排序(最好使用stable_sort()函數,在性價比相同時保持輸入的順序):

sort(a, a+n, cmp);

算法5.3 計算揹包問題的貪心算法

//形參n是物品的數量,c是揹包的容量M,數組a是按物品的性價比降序排序 
double knapsack(int n, bag a[], double c) 
{
	double cleft = c;        //揹包的剩餘容量   
	int i = 0;   
	double b = 0;          //獲得的價值   
	//當揹包還能完全裝入物品i
	while(i<n && a[i].w<cleft)   
	{     
		cleft -= a[i].w;     
		b += a[i].v;     
		i++;   
	}
	//裝滿揹包的剩餘空間   
	if (i<n) b += 1.0*a[i].v*cleft/a[i].w;   
	return b; 
	}

如果要獲得解向量X={x1,x2,…,xn},則需要在數據結構中加入物品編號:

struct bag{ 	
	int w; 	
	int v; 	
	double x;		//裝入揹包的量,0≤x≤1 	
	int index;		//物品編號 	
	double c; 
}a[1001];

算法5.4 計算揹包問題的貪心算法,同時得到解向量

double knapsack(int n, bag a[], double c) 
{   
	double cleft = c;   
	int i = 0;   
	double b = 0;
	while(i<n && a[i].w<=cleft)   
	{     
		cleft -= a[i].w;     
		b += a[i].v;     
		//物品原先的序號是a[i].index,全部裝入揹包
		a[a[i].index].x = 1.0;     
		i++;   
	}   
	if (i<n) {
		a[a[i].index].x = 1.0*cleft/a[i].w;     
		b += a[a[i].index].x*a[i].v;   
	}   
	return b; 
}

5.4 最優裝載問題
有一批集裝箱要裝上一艘載重量爲c的輪船,其中集裝箱i的重量爲wi。最優裝載問題要求確定在裝載體積不受限制的情況下,將儘可能多的集裝箱裝上輪船。
最優裝載問題可用貪心算法求解。採用重量最輕者先裝的貪心選擇策略,可得到裝載問題的最優解。表示集裝箱的數據結構如下:

struct load { 	
	int index;		//集裝箱編號 	
	int w;			//集裝箱重量 
}box[1001];

排序因子(按集裝箱的重量升序):

bool cmp (load a, load b) { 	
	if (a.w<b.w) return true; 	
	else return false; 
}

使用標準模板庫函數排序(box[0]未使用):

stable_sort(box, box+n+1, cmp);

這是穩定排序函數,當重量相同時,保持輸入數據原來的順序。
算法5.4 最優裝載問題的貪心算法

while (scanf("%d%d", &c, &n)!=EOF) 
{   
	memset(box, 0, sizeof(box));   
	memset(x, 0, sizeof(x));
	for (int i=1; i<=n; i++)    
	{     
		scanf("%d", &box[i].w);     
		box[i].index = i;   
	}
	//按集裝箱的重量升序排序   
	stable_sort(box, box+n+1, cmp);   
	if (box[1].w>c) {     
		printf("No answer!\n");     
		continue;   
	}
	//貪心算法的實現,重量最輕者先裝載   
	int i;   
	for (i=1; i<=n && box[i].w<=c; i++)   
	{     
		x[box[i].index] = 1;     
		c -= box[i].w;   
	}
	//輸出裝載的集裝箱數量   
	printf("%d\n", i-1);   
	//輸出裝載的集裝箱編號   
	for (i=1; i<=n; i++)     
		if (x[i]) printf("%d ", i);   
	printf("\n");

5.7 刪數問題
給定n位正整數a,去掉其中任意k≤n個數字後,剩下的數字按原次序排列組成一個新的正整數。對於給定的n位正整數a和正整數k,設計一個算法找出剩下數字組成的新數最小的刪數方案。
輸入
第1行是1個正整數a,第2行是正整數k
輸出
對於給定的正整數a,編程計算刪去k個數字後得到的最小數
n位數a可表示爲x1x2…xixjxk…xn,要刪去k位數,使得剩下的數字組成的整數最小。
將該問題記爲T,最優解A=xi1xi2…xim(i1<i2<…<im,m=n-k),在刪去k個數後剩下的數字按原次序排成的新數,其最優值記爲N。
本問題採用貪心算法求解,採用最近下降點優先的貪心策略:即x1< <x2<… <xi-1<xi,如果xi+1<xi (下降點),則刪去xi,即得到一個新的數且這個數爲n- 1位中最小的數N1,可表示爲x1x2…xi-1 xi+1… xn。
算法5.13刪數問題的貪心算法實現

string a;        //n位數a 
int k; 
cin>>a>>k; 
//如果k≥n,數字被刪完了
If (k >= a.size())  a.erase(); 
else while(k > 0) 
{   
	//尋找最近下降點   
	int i;   
	for (i=0; (i<a.size()-1) && (a[i] <= a[i+1]);  ++i);
	a.erase(i, 1);    //刪除xi   
	k- -; 
} 
//刪除前導數字0
while(a.size() > 1 && a[0] == '0')    
	a.erase(0, 1); 
cout<<a<<endl;

5.8 多處最優服務次序問題
設有n個顧客同時等待一項服務,顧客i需要的服務時間爲ti,1≤i≤n,共有s處可以提供此項服務。應如何安排n個顧客的服務次序才能使平均等待時間達到最小?平均等待時間是n個顧客等待服務時間的總和除以n。
給定的n個顧客需要的服務時間和s的值,編程計算最優服務次序。
輸入
第一行有2個正整數n和s,表示有n個顧客且有s處可以提供顧客需要的服務。接下來的1行中,有n個正整數,表示n個顧客需要的服務時間。
輸出
最小平均等待時間,輸出保留3位小數。
假設原問題爲T,並已經知道某個最優服務序列,即最優解爲A={t1, t2,…,tn},其中ti爲第i個 用戶需要的服務時間,則每個用戶等待時間Ti爲:
T1=t1;
T2=t1 + t2;

Tn=t1 + t2+…+tn
那麼總的等待時間,即最優值N爲:
N=nt1 + (n - 1)t2+…+2tn-1+tn
由於平均等待時間是n個顧客等待時間的總和除以n,故本題實際上就是求使顧客等待時間的總和最小的服務次序
設計貪心策略如下:
對服務時間最短的顧客先服務的貪心選擇策略。
首先對需要服務時間最短的顧客進行服務,即做完第一次選擇後,原問題T變成了需對n—1個顧客服務的新問題T’。
新問題和原問題相同,只是問題規模由n減小爲n—1。
基於此種選擇策略,對新問題T’,在n—1個顧客中選擇服務時間最短的先進行服務,如此進行下去,直至所有服務都完成爲止。
算法5.14 多處最優服務次序問題的貪心算法實現

//顧客等待的隊列爲client,提供服務的窗口s個 
double greedy(vector<int> client, int s) 
{   
	//服務窗口的顧客等待時間   
	vector<int> service(s+1, 0);   
	//服務窗口顧客等待時間的總和   
	vector<int> sum(s+1, 0);   
	//顧客的數量   
	int n = client.size();   
	//按顧客的服務時間升序排序   
	sort(client.begin(), client.end());
	//貪心算法的實現   
	int i=0;          //顧客的指針   
	int j=0;          //窗口的指針   
	while(i < n)   
	{     
		service[j] += client[i];     
		sum[j] += service[j];     
		++i, ++j;     
		if(j == s) j = 0;   
	}   
	//計算所有窗口服務時間的總和   
	double t=0;   
	for(i=0; i<s; ++i) t += sum[i];   
	t /= n;
	return t; 
}

5.10 ZOJ1025-Wooden Sticks
現有n根木棒,已知它們的長度和重量。要用一部木工機一根一根地加工這些木棒。該機器在加工過程中需要一定的準備時間,是用於清洗機器,調整工具和模版的。
木工機需要的準備時間如下:
(1)第一根木棒需要1min的準備時間;
(2)在加工了一根長爲l ,重爲w的木棒之後,接着加工一根長爲l ’ (l ≤ l’ ),重爲 w’ ( w≤w’)的木棒是不需要任何準備時間的,否則需要一分鐘的準備時間。
給定n根木棒,找到最少的準備時間。
例如現在有長和重分別爲(4,9),(5,2),(2,1),(3,5)和(1,4)的五根木棒,那麼所需準備時間最少爲2min,順序爲(1,4),(3,5),(4,9),(2,1),(5,2)。
輸入
輸入有多組測試例。輸入數據的第一行是測試例的個數T。
每個測試例兩行:
第一行是一個整數n(1≤n≤5000),表示有多少根木棒;
第二行包括n×2個整數,表示l1,w1,l2,w2,l3,w3,…,ln,wn,其中li和wi表示第i根木棒的長度和重量。
數據由一個或多個空格分隔。
輸出
輸出是以分鐘爲單位的最少準備時間,一行一個。
本題僅僅使用貪心算法是不夠的,排序之後還要使用動態規劃的算法。
(1)數據結構
採用結構體表示木棒的信息:

#define maxN 5001 
struct stick  
{  	
	int l;			//木棒的長度 	
	int w;			//木棒的重量 
}; 
stick data[maxN];		//存放所有木棒

(2)按木棒的長度使用貪心算法
利用C++的標準模板庫函數sort()實現排序:

sort(data, data+n, cmp);

排序函數cmp()的實現:

int cmp(stick a, stick b)  
{  	
	//長度相等時,按重量排序 	
	if (a.l == b.l) return a.w < b.w;  	
	//優先按長度排序 	
	else if (a.l < b.l) return true; 	
	return false; 
}

(3)使用動態規劃的方法,計算重量w的最長單調遞增子序列的個數
用數組b記錄重量w的分組序號。
則a[i].w(0≤i<n)最長遞增子序列的分組個數爲:max {b[i]}。
b[i]滿足最優子結構性質,可以遞歸地定義爲:
b[0]=1;
b[i] = max {b[j]}+1,0≤j<i
0<i<n
a[i].w<a[j].w
算法5.16 計算重量w的最長單調遞增子序列個數的動態規劃實現

//形參n是木棒的數量,stick是木棒參數的數組 
int LIS(int n, stick a[])  
{   
	//數組b表示木棒分組的序號   
	int b[maxN];   
	memset(b, 0, sizeof(b));   
	int i, j, k;   
	b[0]=1;
	for (i=1; i<n; i++)    
	{     
		//計算第i個木棒的的分組序號     
		k=0;     
		for (j=0; j<i; j++)        
			if (a[i].w<a[j].w && k<b[j]) k=b[j];  
		b[i]=k+1;   
	}
	//查找最大的分組序號(數組b中的最大值)   
	int max=0;   
	for (i=0; i<n; i++)     
		if (b[i]>max) max=b[i];   
	return max; 
}

5.13 ZOJ1161-Gone Fishing
約翰有h(1≤h≤16)個小時的時間,在該地區有n(2≤n≤25)個湖,這些湖剛好分佈在一條路線上,該路線是單向的。約翰從湖1出發,他可以在任一個湖結束釣魚。但他只能從一個湖到達另一個與之相鄰的湖,而且不必每個湖都停留。
假設湖i(i=1~n—1),以5分鐘爲單位,從湖i到湖i+1需要的時間用ti(0<ti≤192)表示。例如t3=4,是指從湖3到湖4需要花20分鐘時間。
已知在最初5分鐘,湖i預計釣到魚的數量爲fi(fi≥0)。以後每隔5分鐘,預計釣到魚的數量將以常數di(di≥0)遞減。如果某個時段預計釣到魚的數量小於或等於di,那麼在下一時段將釣不到魚。爲簡單起見,假設沒有其它的釣魚者影響約翰的釣魚數量。
編寫程序,幫助約翰制定釣魚旅行的計劃,以便儘可能多的釣到魚。
輸入
對每組測試例,第一行是n,接下來一行是h。
下面一行是n個整數fi(1≤i≤n),然後是一行n個整數di(1≤i≤n),最後一行是n—1個整數ti(1≤i≤n—1)。
輸出
對每個測試例,輸出在每個湖上花費的時間,這是約翰要實現釣到最多的魚的計劃(必須使整個計劃在同一行輸出)。 接下來一行是釣到的魚的數量:
如果存在很多方案,儘可能選擇在湖1釣魚所耗費的時間,即使有些時段沒有釣到魚;如果還是無法區分,那就儘可能選擇在湖2釣魚所耗費的時間,以此類推。
(1)數據結構
每個湖預計釣到魚的數量,定義爲數組:
#define NUM 30
int f[NUM];
每個湖預計釣到魚的數量的遞減值,定義爲數組:
int d[NUM];
相鄰湖之間的旅行時間,定義爲數組:
int t[NUM];
釣魚計劃,定義爲數組:
int plan[NUM];
湖的個數n,用於釣魚的時間h,儘可能多的釣魚數量best。
(2)搜索,在任意一個湖結束釣魚時的最優釣魚計劃
設花費在路程.上的時間爲:
int time = 0;
假設約翰在第m個湖結束釣魚,路程是單向的
展開搜索:

for (i= 1;i<= n && h-time; ++i)
{
	greedy(i, h - time); 
	time += t[i];
}

(3)採用貪心策略,每次選擇魚最多的湖釣一次魚
可以認爲約翰能從一個湖“瞬間轉移”到另一個湖,即在任意一個時刻都可以從湖1到湖pos中任選一個釣一次魚。
算法5.19選擇魚最多的湖釣魚的貪心算法實現

//從湖1起到湖pos止,花費時間time(不含路程)的釣魚計劃 
void greedy(int pos, int time) 
{    
	if (time <= 0) return;      //時間已經用完   
	int i, j;   
	int fish[MAXN];   
	int p[MAXN];   
	int t = 0; 
	for (i = 0; i < pos; ++i)      
		fish[i] = f[i];    
	memset(p, 0, sizeof(p));    
	//在時間time內,選擇魚最多的湖釣魚;如果魚都沒有了,就把時間放在湖1上 
	for (i = 0; i < time; ++i) 
	{    
		int max = 0;		//魚最多的湖中,魚的數量   
		int id = -1;     //魚最多的湖的編號   
		//查找魚最多的湖中,魚的數量和湖的編號
		for (j = 0; j < pos; ++j)     
			if (fish[j] > max){        
				max = fish[j];        
				id = j;      
			} 
		if (id != -1)      //找到了,進行釣魚處理   
		{     
			++p[id];      
			fish[id] -= d[id];      
			t += max;    
		}   
		//沒有找到(從湖1起到湖pos全部釣完了),就把時間放在湖1上   
		else ++p[0];  
	} 
}
//處理最優方案 
if (t > best) 
{    
	best = t;         //最優值   
	memset(plan, 0, sizeof(plan));   
	for (i = 0; i < pos; ++i)  //最優解     
		plan[i] = p[i];  
}

輸出釣魚計劃時,再把5乘回去,就變成實際的釣魚時間(分鐘):

for (i=0; i<n-1; ++i)  	
	printf("%d, ", plan[i] * 5); 
printf("%d\n", plan[n-1] * 5);  
printf("Number of fish expected: %d\n", best);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章