1.定义
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
2.使用环境
分析问题是否满足:
(1).贪心选择性质
一个问题的整体最优解可通过一系列局部的最优解的选择达到,并且每次的选择可以依赖以前作出的选择,但不依赖于后面要作出的选择。这就是贪心选择性质。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
(2).最优子结构
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心法求解的关键所在。在实际应用中,至于什么问题具有什么样的贪心选择性质是不确定的,需要具体问题具体分析。
3.贪心算法的几个要素
(1)贪心法适用于组合优化问题。
(2)求解过程是多步判断过程,最终的判断序列对应于问题的最优解。
(3)依据某种 “ 短视的 ” 贪心选择性质判断,性质好坏决定算法的成败。
(4)贪心法必须进行正确性证明。
(5)证明贪心法不正确的技巧:举反例。
4.例题
(1)会场安排问题
在足够多的会场里安排一批活动,并希望使用尽可能少的会场。设计一个有效的贪心算法进行安排。(类似于图着色问题)
关键思路是比较i活动开始时间和j活动结束时间;
排好序后如果i的时间大于等于j,则不需要增加会场,j加一;
反之,则增加一个会场;
关键部分代码如下(a和b分别为排好序的开始时间和结束时间):
int arrange(int a[],int b[],int n){
int j=0,sum=0;
for(int i=0;i<n;i++){
if(a[i]<b[j])sum++;
else j++;
}
return sum;
}
(2)Dijstra算法
首先把起点到所有点的距离存下来找个最短的,然后松弛一次再找出最短的,所谓的松弛操作就是,遍历一遍看通过刚刚找到的距离最短的点作为中转站会不会更近,如果更近了就更新距离,这样把所有的点找遍之后就存下了起点到其他所有点的最短距离。
关键代码如下:
void Dijkstra(int n,int v,int pre[],int dist[]){
bool s[MAX];
int i,j;
for(i=1;i<=n;i++){
dist[i]=w[v][i];
s[i]=false;
if(dist[i]==MAX)
pre[i]=0;
else
pre[i]=v;
}
dist[v]=0;
s[v]=true;
for(i=1;i<n;i++){
int temp=MAX;
int u=v;
for(j=1;j<=n;j++){
if((!s[j])&&(dist[j]<temp)){
u=j;
temp=dist[j];
}
}
s[u]=true;
for(j=1;j<=n;j++){
if((!s[j])&&(w[u][j]<BIG)){
int newdist=dist[u]+w[u][j];
if(newdist<dist[j]){
dist[j]=newdist;
pre[j]=u;
}
}
}
}
}
(3)哈夫曼编码
哈夫曼编码需要按照一定的规则求出某个字符对应的编码;
译码过程需要方便的取出编码的前缀,因此需要表示前缀码的合适的数据结构。为此,可以用二叉树作为前缀码的数据结构:树叶表示给定字符;从树根到树叶的路径当作该字符的前缀码;代码中每一位的0或1分别作为指示某节点到左儿子或右儿子的“路标”。
这里使用了优先队列来建立树,将两个节点取出后求和得到一个新的结点并压回队列;
在取出时设置好左右孩子的地址;
在构建编码时只需根据地址找到路径就可以求出对应的哈夫曼编码了;
核心代码如下:
#include<iostream>
#include<queue>
using namespace std;
typedef struct node{
int f;
char op;
node *lchild,*rchild;
node():lchild(NULL),rchild(NULL),f(0),op('\0'){
}
}node,*P;
struct cmp{
bool operator()(P a,P b){
return a->f>b->f;
}
};
priority_queue<P,vector<P>,cmp> pq;
void HFC(int n){
P l,r;
while(pq.size()>1){
node *l,*r;
l=pq.top();
pq.pop();
r=pq.top();
pq.pop();
node *p=new node;
p->f=l->f+r->f;
p->lchild=l;
p->rchild=r;
pq.push(p);
}
}
void Print(P p,string str){
if(p==NULL)return;
if(p->lchild){
str+='0';
Print(p->lchild,str);
}
if(p->lchild==NULL&&p->rchild==NULL){
cout<<p->op<<" "<<str<<endl;
}
str.erase(str.end()-1);
if(p->rchild){
str+='1';
Print(p->rchild, str);
}
}
int main(){
int i,n;
string str;
char tmp;
cin>>n;
char c;
int fre;
for(i=0;i<n;i++){
cin>>c>>fre;
node *p=new node;
p->f=fre;
p->op=c;
pq.push(p);
}
HFC(n);
Print(pq.top(),str);
return 0;
}
5.小结
贪心法正确性证明方法:
(1)直接计算优化函数,贪心法的解恰好取得最优值。
(2)数学归纳法(对算法步数或者问题规模归纳)
(3)交换论证。
其他方法:举反例。
贪心法的优势:算法简单,时间和空间复杂性低。
劣势:
1、不能保证最优解。
2、贪心算法一般用来解决求最大或最小解。