贪心算法
贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。贪心算法通过一系列选择来得到问题的解,所做的每个选择都是当前状态下局部最好选择,即贪心选择。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
解题步骤
- 建立数学模型来描述问题;
- 把求解的问题分成若干个子问题;
- 对每一子问题求解,得到子问题的局部最优解;
- 把子问题的解局部最优解合成原来解问题的一个解。
算法实现
- 从问题的某个初始解出发。
- 采用循环语句,当可以向求解目标前进一步时,就根据局部最优策略,得到一个部分解,缩小问题的范围或规模。
- 将所有部分解综合起来,得到问题的最终解。
实例分析
一、背包问题
问题描述
有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品 | A | B | C | D | E | F | G |
---|---|---|---|---|---|---|---|
重量 | 35 | 30 | 60 | 50 | 40 | 10 | 25 |
价值 | 10 | 40 | 30 | 50 | 35 | 40 | 30 |
问题分析
1.目标函数: ∑pi最大,使得装入背包中的所有物品pi的价值加起来最大。
2.约束条件:装入的物品总重量不超过背包容量:∑wi<=M( M=150)
3.贪心策略:
选择单位重量价值最大的物品
算法设计
1.计算出每个物品单位重量的价值
2.按单位价值从大到小将物品排序
3.根据背包当前所剩容量选取物品
4.如果背包的容量大于当前物品的重量,那么就将当前物品装进去。否则,那么就将当前物品舍去,然后跳出循环结束。
伪代码
void Knapsack(int n,float M,float v[],float w[],float x[]){
Sort(n,v,w);
int i;
for(i=1;i<=n;i++)
x[i]=0;
float c=M;
for(i=1;i<=n;i++){
if(w[i]>c)
break;
w[i]=1;
c-=w[i];
}
if(i<=n)
x[i]=c/w[i];
}
二、活动安排问题
问题描述
设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si <fi 。要求设计程序,使得安排的活动最多。
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|
s[i] | 1 | 3 | 0 | 5 | 3 | 5 | 6 | 8 | 8 | 2 | 12 |
f[i] | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
算法设计
若被检查的活动i的开始时间s[i]小于最近选择的活动j的结束时间f[j],则不选择活动i,否则选择活动i加入集合中。当输入的活动已按结束时间的非减序排列,算法只需O(n)的时间安排n个活动,使最多的活动能相容地使用公共资源。如果所给出的活动未按非减序排列,可以用O(nlogn)的时间重排。
伪代码
template<class Type>
void GreedySelector(int n,Type s[],Type f[],bool A[]){
A[1]=true;
int j=1;
for(int i=2;i<=n;i++)
{
if(s[i]>=f[i]){
A[i]=true;
j=i;
}
else
A[i]=false;
}
}
三、哈夫曼编码
哈夫曼编码 一般采用前缀编码 – -- 对字符集进行编码时,要求字符集中任一字符的编码都不是其它字符的编码的前缀,这种编码称为前缀(编)码。
贪心策略
贪心策略:将所有的节点放到一个队列中,用一个节点替换两个频率最低的节点,新节点的频率就是这两个节点的频率之和,新节点就是两个被替换节点的父节点。循环直到队列中只剩一个节点(树根)。
代码
#include<iostream>
#include<queue>
#include <algorithm>
#include <string>
using namespace std;
typedef struct node{
node *lchild,*rchild;
char ch;
int weight;
string huffCode;
node():lchild(NULL),rchild(NULL),ch('\0'),weight(0),huffCode(""){
}
}treeNode;
struct cmp{
bool operator()(node *a,node *b)
{
return a->weight > b->weight;//升序排列
}
};
priority_queue<node*,vector<node*>,cmp> HFT;//小顶堆
void createHuffCode(treeNode* root){
if(root->lchild){
root->lchild->huffCode=root->huffCode+"0";
createHuffCode(root->lchild);
}
if(!(root->lchild)&&!(root->rchild)){
cout<<root->ch<<" ";
cout<<root->huffCode;
cout<<endl;
}
if(root->rchild){
root->rchild->huffCode=root->huffCode+"1";
createHuffCode(root->rchild);
}
}
void del(treeNode* root){
if(root==NULL){
return;
}
del(root->lchild);
del(root->rchild);
delete root;
}
void createHuffTree(int n)
{
vector<int> vfreq;//概率
vector<char> vch;
char ch_temp;
int freq_temp;
while(n--){
cin>>ch_temp>>freq_temp;
vch.push_back(ch_temp);
vfreq.push_back(freq_temp);
}
for(int i=0;i<vch.size();i++){
treeNode *t=new treeNode;
t->ch=vch[i];
t->weight=vfreq[i];
HFT.push(t);
}
while(HFT.size()!=1)
{
treeNode *a,*b;
a=HFT.top();
HFT.pop();
b=HFT.top();
HFT.pop();
treeNode *temp=new treeNode;
temp->weight=a->weight+b->weight;
temp->lchild=a;
temp->rchild=b;
HFT.push(temp);
}
cout<<endl;
createHuffCode(HFT.top());
del(HFT.top());
}
int main(void)
{
int n;
cout<<"输入字符总数:"<<endl;
cin>>n;
cout<<"输入各字符及概率:"<<endl;
createHuffTree(n);
return 0;
}
算法时间复杂度: O(nlogn)
算法以freq为键值的优先队列Q用在贪心选择时有效地确定算法当前要合并的2棵具有最小频率的树。一旦2棵具有最小频率的树合并后,产生一棵新的树,其频率为合并的2棵树的频率之和,并将新树插入优先队列Q。经过n-1次的合并后,优先队列中只剩下一棵树,即所要求的树root。算法huffman用最小堆实现优先队列Q。初始化优先队列需要O(n)计算时间,由于最小堆的节点删除、插入均需O(logn)时间,n-1次的合并总共需要O(nlogn)计算时间。
参考
计算机算法设计与分析(第5版)