算法设计与分析第3章 贪心算法
第4章 貪心算法
貪心算法總是作出在當前看來最好的選擇。也就是說貪心算法并不從整體最優考慮,它所作出的選擇只是在某種意義上的局部最優選擇。
貪心算法的基本要素
1、貪心選擇性質
所謂貪心選擇性質是指所求問題的整體最優解可以通過一系列局部最優的選擇,即貪心選擇來達到。這是貪心算法可行的第一個基本要素,也是貪心算法與動態規劃算法的主要區別。
動態規劃算法通常以自底向上的方式解各子問題,而貪心算法則通常以自頂向下的方式進行,以迭代的方式作出相繼的貪心選擇,每作一次貪心選擇就將所求問題簡化為規模更小的子問題。
對于一個具體問題,要確定它是否具有貪心選擇性質,必須證明每一步所作的貪心選擇最終導致問題的整體最優解。
2、最優子結構性質
當一個問題的最優解包含其子問題的最優解時,稱此問題具有最優子結構性質。
問題的最優子結構性質是該問題可用動態規劃算法或貪心算法求解的關鍵特征。
例1.活動安排問題:在所給的活動集合中選出最大的相容活動子集合
測試樣例:
input:
4
1 2
1 3
2 4
4 5
output:
3
input:
5
1 3
2 4
3 6
3 5
5 6
output:
3
分析:
由于輸入的活動以其完成時間的非減序排列,所以算法每次總是選擇具有最早完成時間的相容活動加入集合A中。直觀上,按這種方法選擇相容活動為未安排活動留下盡可能多的時間。也就是說,該算法的貪心選擇的意義是使剩余的可安排時間段極大化,以便安排盡可能多的相容活動。
當輸入的活動已按結束時間的非減序排列,算法只需O(n)的時間安排n個活動,使最多的活動能相容地使用公共資源。如果所給出的活動未按非減序排列,可以用O(nlogn)的時間重排。
代碼:
#include<iostream>
#include<stdlib.h>
#include<math.h>
#include<cstdio>
#include<string>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
struct seg
{int s,e;
} s[10010];
int cmp(seg x,seg y)
{return x.e<y.e;
}
int main()
{int i,n,num;while(~scanf("%d",&n)){num=1;for(i=0; i<n; i++){scanf("%d%d",&s[i].s,&s[i].e);}sort(s,s+n,cmp);int time=s[0].e;for(i=1; i<n; i++){if(time<=s[i].s){time=s[i].e;num++;}}printf("%d\n",num);}return 0;
}
例2.活動安排問題的拓展:有若干個活動,第i個開始時間和結束時間是[Si,fi),活動之間不能交疊,要把活動都安排完,至少需要幾個教室?
思路:求活動疊加的最大值
分析:將時間標記為1個時間結點(flag標記其為開始還是結束時間),然后將所有時間結點按時間大小排序,初始化交疊次數為0,接下來開始遍歷時間結點,若為開始時間,則交疊次數+1,若為結束時間,則交疊次數-1,記錄交疊次數的最大值,即為所求。
測試樣例:
input:
5
1 3
2 4
3 5
3 6
5 6
output:
3
對測試樣例的處理過程:
(1)n個活動,給所有時間標志結束還是開始,形成2*n個時間結點
1 0
3 1
2 0
4 1
3 0
5 1
3 0
6 1
5 0
6 1
(2)給時間結點排序
1 0
2 0
3 1 //時間值為3,標記為結束時間的排在前
3 0 //時間值為3,標記為結束時間的排在后
3 0
4 1
5 1
5 0
6 1
6 1
代碼:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<algorithm>
using namespace std;
struct seg //活動
{int s;int e;
}a[10010];
int cmp1(seg x,seg y) //先將活動按結束時間排序
{return x.e<y.e;
}
struct node //時間結點,flag標記:0代表開始時間,1代表結束時間
{int vlue;int flag;
}b[20010];
int cmp2(node x,node y) //時間結點排序
{return x.vlue<y.vlue;
}
int main()
{int n,sum;while(cin>>n){sum=0;for(int i=0; i<n; i++){cin>>a[i].s>>a[i].e;}sort(a,a+n,cmp1); //先對活動按先結束時間排序,這樣就可以使得下面的時間結點排序中,int k=0; //兩個活動:一個的開始時間與另一個的結束時間相同時,先將結束時間放前面for(int i=0; i<n; i++){b[k].vlue=a[i].s; b[k++].flag=0;//0代表開始時間b[k].vlue=a[i].e; b[k++].flag=1;//1代表結束時間}sort(b,b+k,cmp2); //給時間結點排序,由于上面活動的排序,時間結點排序時,會將值相同的兩個時間結點,結束標記的先排在前面int Max=0; //以便于下面記錄次數交疊時,先對該時間結束的活動記錄交疊次數-1,再對該時間開始的活動記錄交疊次數+1for(int i=0; i<k; i++){if(b[i].flag==0) //活動開始{sum++; //交疊次數+1if(Max<sum){Max=sum;}}else{sum--; //活動結束,交疊次數-1}}cout<<Max<<endl;}
}
例3.最優裝載(背包)問題
給定n種物品和一個背包。物品i的重量是Wi,其價值為Vi,背包的容量為C。在選擇物品i裝入背包時,可以選擇物品i的一部分,而不一定要全部裝入背包,應如何選擇裝入背包的物品,使得裝入背包中物品的總價值最大?
用貪心算法解背包問題的基本步驟:
1.首先計算每種物品單位重量的價值Vi/Wi;
2.然后依貪心選擇策略,將盡可能多的單位重量價值最高的物品裝入背包;
3.若將這些物品全部裝入背包后,背包內的物品總重量未超過C,則選擇單位重量價值次高的物品并盡可能多地裝入背包
4.依此策略一直地進行下去,直到背包裝滿為止。
代碼:
//C++#include<iostream>
#include<stdlib.h>
#include<math.h>
#include<cstdio>
#include<string>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
struct item
{double weight;double value;
} a[1010];
int cmp(item x,item y)
{return (x.value/x.weight)>(y.value/y.weight);
}
int main()
{int i,n;double v;while(~scanf("%d%lf",&n,&v)){double sum=0;for(i=0; i<n; i++){scanf("%lf%lf",&a[i].weight,&a[i].value);}sort(a,a+n,cmp);for(i=0; i<n; i++){if(a[i].weight<v){v-=a[i].weight;sum+=a[i].value;}else break;}if(v>0&&i!=n){sum+=(a[i].value/a[i].weight)*v;}printf("%lf\n",sum);}return 0;
}//C
/*
#include<stdio.h>
#include<math.h>
int wi[1010],val[1010];
void Sort(int wi[],int val[],int n)
{for(int i=0; i<n-1; i++){for(int j=0; j<n-i-1; j++){double t1=val[j]*1.0/wi[j];double t2=val[j+1]*1.0/wi[j+1];if(t1<t2){int temp1,temp2;temp1=wi[j];wi[j]=wi[j+1];wi[j+1]=temp1;temp2=val[j];val[j]=val[j+1];val[j+1]=temp2;}}}
}
int main()
{int i,n;double v;while(~scanf("%d%lf",&n,&v)){double sum=0;for(i=0; i<n; i++){scanf("%d%d",&wi[i],&val[i]);}Sort(wi,val,n);for(i=0; i<n; i++){if(wi[i]<v){v-=wi[i];sum+=val[i];}else break;}if(v>0&&i!=n){sum+=(val[i]*1.0/wi[i])*v;}printf("%lf\n",sum);}return 0;
}
*/
例4.哈夫曼編碼
哈夫曼編碼算法用字符在文件中出現的頻率表來建立一個用0,1串表示各字符的最優表示方式。
給出現頻率高的字符較短的編碼,出現頻率較低的字符以較長的編碼,可以大大縮短總碼長。
前綴碼:對每一個字符規定一個0,1串作為其代碼,并要求任一字符的代碼都不是其它字符代碼的前綴。這種編碼稱為前綴碼。
代碼:
//哈夫曼編碼簡單實現(對應切割木棒問題)
#include<iostream>
#include<stdlib.h>
#include<math.h>
#include<cstdio>
#include<string>
#include<cstring>
#include<cmath>
#include<queue>
#include<vector>
#include<algorithm>
using namespace std;
struct cmp
{bool operator() (int &a,int &b){return a>b;}
};
int main()
{priority_queue<int, vector<int>, cmp>que;int i,n,x,sum;while(cin>>n){sum=0;for(i=0; i<n; i++){cin>>x;que.push(x);}while(que.size()!=1){int a=que.top();que.pop();int b=que.top();que.pop();int c=a+b;que.push(c);sum+=c;cout<<a<<" + "<<b<<" = "<<c<<endl;}while(!que.empty()){que.pop();}cout<<sum<<endl;}return 0;
}
例5.最小生成樹問題
設G =(V,E)是無向連通帶權圖,即一個網絡。E中每條邊(v,w)的權為c[v][w]。如果G的子圖G’是一棵包含G的所有頂點的樹,則稱G’為G的生成樹。生成樹上各邊權的總和稱為該生成樹的耗費。在G的所有生成樹中,耗費最小的生成樹稱為G的最小生成樹。
最小生成樹性質
設G=(V,E)是連通帶權圖,U是V的真子集。如果(u,v)屬于E,且u屬于U,v屬于V-U,且在所有這樣的邊中,(u,v)的權c[u][v]最小,那么一定存在G的一棵最小生成樹,它以(u,v)為其中一條邊。這個性質有時也稱為MST性質。
1、Prim算法
設G=(V,E)是連通帶權圖,V={1,2,…,n}。
構造G的最小生成樹的Prim算法的基本思想是:
首先置S={1},然后,只要S是V的真子集,就作如下的貪心選擇:
選取滿足條件i屬于S,j屬于V-S,且c[i][j]最小的邊,將頂點j添加到S中。這個過程一直進行到S=V時為止。
在這個過程中選取到的所有邊恰好構成G的一棵最小生成樹。
按Prim算法順序得到的最小生成樹上的邊如下圖所示:
時間復雜度:O(n2)
代碼:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
#define Max 0x3f3f3f3f
int ch[1005][1005],low[1005],vis[1005]; //分別記錄邊權、當前最小邊權、標記訪問
int n;
int prim()
{int pos=1,sum=0;memset(vis,0,sizeof(vis));vis[pos]=1; //先將1加入集合 for(int i=2; i<=n; i++){low[i]=ch[pos][i];}for(int i=1; i<n; i++) //將n-1個頂點加入集合 {int Min=Max;for(int j=1; j<=n; j++) //找出不在集合中且邊最小的頂點 {if(!vis[j]&&Min>low[j]){Min=low[j];pos=j;}}sum+=Min;vis[pos]=1;for(int j=1; j<=n; j++) //隨著新頂點的加入而更新其他頂點的最小邊權 {if(!vis[j]&&low[j]>ch[pos][j]){low[j]=ch[pos][j];}}}return sum;
}
int main()
{int m,s,e,w;while(cin>>n>>m){memset(ch,Max,sizeof(ch));for(int i=0; i<m; i++){cin>>s>>e>>w;ch[s][e]=ch[e][s]=w;}int ans=prim();cout<<ans<<endl;}return 0;
}
2、Kruskal算法(避環法)
Kruskal算法構造G的最小生成樹的基本思想是:
(1)首先將G的n個頂點看成n個孤立的連通分支。
(2)將所有的邊按權從小到大排序。
(3)然后從第一條邊開始,依邊權遞增的順序查看每一條邊
(4)按下述方法連接2個不同的連通分支:
當查看到第k條邊(v,w)時,如果端點v和w分別是當前2個不同的連通分支T1和T2中的頂點時,就用邊(v,w)將T1和T2連接成一個連通分支,然后繼續查看第k+1條邊;
如果端點v和w在當前的同一個連通分支中,就直接再查看第k+1條邊。這個過程一直進行到只剩下一個連通分支時為止。
按Kruskal算法順序得到的最小生成樹上的邊如下圖所示:
算法復雜度:
當圖的邊數為e時,Kruskal算法所需的計算時間是O(eloge)。當時,Kruskal算法比Prim算法差,但當時,Kruskal算法卻比Prim算法好得多。(即邊少時Kruskal算法優于Prim)
代碼:
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int f[1010];
int find(int x)
{return f[x]==x?x:f[x]=find(f[x]);
}
struct city
{int a;int b;int c;
}ch[6000];
int cmp(city x,city y)
{return x.c<y.c;
}
int main()
{int i,n,m;while(cin>>n>>m){int sum=0;for(i=1; i<=n; i++){f[i]=i;}for(i=1; i<=m; i++){cin>>ch[i].a>>ch[i].b>>ch[i].c;}sort(ch+1,ch+1+m,cmp);for(i=1; i<=m; i++){int xx=find(ch[i].a);int yy=find(ch[i].b);if(xx!=yy){if(xx>yy){int temp;temp=xx;xx=yy;yy=temp;}f[yy]=xx;sum+=ch[i].c;}}cout<<sum<<endl;}return 0;
}
例6.最短路徑問題
1.Dijkstra(迪杰斯特拉)算法
典型的最短路徑算法,用于計算一個節點到其他所有節點的最短路徑。主要特點是以起始點為中心向外層層擴展,直到擴展到終點為止。Dijkstra算法能得出最短路徑的最優解,但由于它遍歷計算的節點很多,所以效率低。
基本思想:設置頂點集合S并不斷地作貪心選擇來擴充這個集合。初始時,S中僅含有源。設u是G的某一個頂點,把從源到u且中間只經過S中頂點的路稱為從源到u的特殊路徑,并用數組dis記錄當前每個頂點所對應的最短路徑長度。Dijkstra算法每次從V-S中取出具有最短特殊路長度的頂點u,將u添加到S中,同時對數組dis作必要的修改。一旦S包含了所有V中頂點,dis就記錄了從源到所有其它頂點之間的最短路徑長度。
算法復雜度:
時效性較好,時間復雜度為O(V2+E)
源點可達的話,O(V*lgV+E*lgV)=> O(E*lgV)
當是稀疏圖的情況時,此時E=V2/lgV,所以算法的時間復雜度可為O(V2)
若是斐波那契堆作優先隊列的話,算法時間復雜度,則為O(V*lgV + E)
代碼:
/*
無法判斷是否存在負權邊,非負邊權圖中效率穩定
使用范圍:求單源(單源點的最短路徑問題是指:給定一個加權有向圖G和源點s,對于圖G中的任意一點v,求從s到v的最短路徑。)、無負權的最短路。權值非負時使用
*/
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#define INF 0x3ffffff
using namespace std;
int ch[505][505];
int dis[505];
int vis[505];
//int path[505];
int main()
{int n,m,i,j,x,y,l,source,des;while(cin >> n >>m){//cin>>source>>des;for(i=0; i<n; i++){for(j=0; j<n; j++){ch[i][j] = INF;}}for(i=0; i<n; i++){dis[i] = INF;vis[i] = 0;//path[i]=source;}//dis[source]=0;dis[0] = 0;for(i=0; i<m; i++){scanf("%d%d%d",&x,&y,&l);ch[x][y] = min(ch[x][y],l);ch[y][x] = min(ch[y][x],l);}for(i=0; i<n; i++) //遍歷n個頂點{int Min = INF;int s=-1;for(j=0; j<n; j++) //每次找一個最小權頂點 {if(Min > dis[j] && !vis[j]){s = j;Min = dis[j];}}if(s!=-1){vis[s] = 1;for(j=0; j<n; j++){dis[j] = min(dis[s] + ch[s][j],dis[j]);/* if(dis[j]>dis[s]+ch[s][j]){dis[j]=dis[s]+ch[s][j];path[j]=s;}*/}}}for(i=0; i<n; i++){for(j=0; j<n; j++){printf("%d ",ch[i][j]);}printf("\n");}for(j=0; j<n; j++){printf("%d ",dis[j]);}cout << endl;/*for(i=0; i<n; i++)//求具體路徑{if(i==s) continue;printf("從%d到%d最短路為 :%d\t",s,i,dis[i]);int next;next=path[i];printf("%d--",i);while(next!=path[next]){printf("%d--",next);next=path[next];}printf("%d\n",s);}*/return 0;
}
2.Bellman-Ford算法
求解單源最短路徑問題的一種算法。求單源最短路,可以判斷有無負權回路(若有,則不存在最短路),與Dijkstra算法不同的是,在Bellman-Ford算法中,邊的權值可以為負數。
Bellman-Ford算法的流程如下:
給定圖G(V, E)(其中V、E分別為圖G的頂點集與邊集),源點s,
數組dis[i]記錄從源點s到頂點i的路徑長度,初始化數組dis[n]為MAX,dis[s]為0;
以下操作循環執行至多n-1次,n為頂點數:
對于每一條邊e(u, v),如果dis[u] + w(u, v) < dis[v],則令dis[v] = dis[u]+w(u, v)。w(u, v)為邊e(u,v)的權值;
若上述操作沒有對dis進行更新,說明最短路徑已經查找完畢,或者部分點不可達,跳出循環。否則執行下次循環;
為了檢測圖中是否存在負環路,即權值之和小于0的環路。對于每一條邊e(u, v),如果存在dis[u] + w(u, v) < dis[v]的邊,則圖中存在負環路,即是該圖無法求出單源最短路徑。否則數組dis[n]中記錄的就是源點s到各頂點的最短路徑長度。
時間復雜度:O(V*E)。
代碼:
/*
求含負權圖的單源最短路徑算法,可檢測并輸出負圈效率較低
使用范圍:當權值有負值,且可能存在負圈時使用
*/
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <string>
#define MAX 200000000 //表示無窮遠
using namespace std;
struct node //結構體存邊
{int x,y;int l;
} a[10005];
int n,m; //n個點,m條邊
int dis[105]; //到每個點的最短距離
int main()
{int i,j,x,y,l;while(~scanf("%d%d",&n,&m)&&(n||m)){for(i=1; i<=n; i++){dis[i]=MAX;}j=0;for(i=0; i<m; i++){scanf("%d%d%d",&x,&y,&l);a[j].x = x;a[j].y = y;a[j++].l = l;a[j].x = y;a[j].y = x;a[j++].l = l;}dis[1]=0; //起點標記為0bool flag = 1;for(i=0; i<n-1; i++){if(flag){flag=0;for(j=0; j<2*m; j++){if(dis[a[j].y] > dis[a[j].x]+a[j].l){dis[a[j].y] = dis[a[j].x]+a[j].l;flag = 1;}}}elsebreak;}/* flag = 0; //檢驗是否存在負環for(i=0; i<2*m; i++){if(dis[a[i].y] > dis[a[i].x]+a[i].l) //如果if句成立 表明上循環完成后再循環中出現負環{dis[a[i].y] = dis[[i].x]+a[i].l;flag = 1;}}*/printf("%d\n",dis[n]);}return 0;
}
3.SPFA算法
Bellman-Ford的隊列優化,與Bellman-ford算法類似,SPFA算法采用一系列的松弛操作以得到從某一個節點出發到達圖中其它所有節點的最短路徑。不同的是,SPFA算法通過維護一個隊列,使得一個節點的當前最短路徑被更新之后沒有必要立刻去更新其他的節點,從而大大減少了重復的操作次數。
與Dijkstra算法與Bellman-ford算法都不同,SPFA的算法時間效率是不穩定的,即它對于不同的圖所需要的時間有很大的差別。
SPFA算法在負邊權圖上可以完全取代Bellman-ford算法,另外在稀疏圖中也表現良好。但是在非負邊權圖中,為了避免最壞情況的出現,通常使用效率更加穩定的Dijkstra算法,以及它的使用堆優化的版本。
時間復雜度:O(kE)(k<<V)。
在最好情形下,每一個節點都只入隊一次,則算法實際上變為廣度優先遍歷,其時間復雜度僅為O(E)。如果每一個節點都被入隊(V-1)次,算法退化為Bellman-ford算法,其時間復雜度為O(VE)。
代碼:
/*
Bellman的隊列優化 可檢測但不能輸出負圈
使用范圍:當權值有負值且沒有負圈時使用SPFA
*/
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <string>
#include <queue>
#include <vector>
#define MAX 20000000
using namespace std;
struct node
{int x;int l;
};
vector<node> a[105]; //鄰接表存圖
queue<int> q;
int n,m;
int vis[105]; //標記是否在隊列中
int dis[105];
void init()
{int i,j;for(i=1; i<=n; i++){a[i].clear();vis[i]=0;dis[i] = MAX;}while(!q.empty()){q.pop();}
}
int main()
{int i,j,x,y,l;while(~scanf("%d%d",&n,&m)&&(n||m)){init();for(i=0; i<m; i++){scanf("%d%d%d",&x,&y,&l);node t1,t2;t1.x = y;t1.l = l;t2.x = x;t2.l = l;a[x].push_back(t1);a[y].push_back(t2);}dis[1] = 0;q.push(1); //入隊的是下標vis[1]=1; //標記在隊中while(!q.empty()){int s = q.front();q.pop();vis[s] = 0;x = s;for(i=0; i<a[s].size(); i++){y = a[s][i].x;if(dis[y]>dis[x]+a[s][i].l){dis[y]=dis[x]+a[s][i].l;if(!vis[y])q.push(y);}}}printf("%d\n",dis[n]);}return 0;
}
總結
以上是生活随笔為你收集整理的算法设计与分析第3章 贪心算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 白菜多少钱啊?
- 下一篇: 《introduction to inf