一:贪心算法的概念:
求解最优化问题的算法通常需要经过一系列的步骤,在每个步骤都面临多重选择,动态规划算法是通过比较这么多选择来得到一个最优的选择,而贪心算法不用比较而是直接选出当时看起来最佳的选择,通过做出局部最优的选择来得到全局最优解。设计贪心算法有如下三个步骤:
- 将最优化问题转化为这样的形式:对其做出一次选择后,只剩下一个子问题需要求解;
- 证明贪心选择是最优解的一部分;
- 证明做出贪心选择后,剩余子问题的最优解与贪心选择结合在一起能得到原问题的最优解。
贪心算法求解最优化问题实例:
本章节列举了活动选择问题,背包问题,赫夫曼编码,任务调度问题,找零问题。
活动选择问题:
在这个问题中,贪心选择是选择原问题中结束时间最早的那个活动。
//数组activityTime存储的活动从下标1开始而不是0开始,并且已按活动结束时间从早到晚排序了;
//数组activityTime中pair存储的第一个元素是活动开始时间,第二个元素是活动结束时间;
//数组activityCandidate存储的就是最终求解的活动。
void greedyActivitySelector(const vector< pair<int,int> >& activityTime,vector<int>& activityCandidate)
{
activityCandidate.push_back(1);
int k=1;
for(int i=2;i!=activityTime.size();++i)
{
if(activityTime[k].second<=activityTime[i].first){
activityCandidate.push_back(i);
k=i;
}
}
}
背包问题:
背包问题分为能用贪心算法求解的分数背包问题和只能用动态规划算法求解0-1背包问题。
分数背包问题:
贪心选择是选择每磅价值最大的商品
//pair<int,int>类型第一个元素存储商品的价值,第二个元素存储商品的重量;
bool compare1(const pair<int,int>& lhs,const pair<int,int>& rhs)
{
double tmp1=(double)(lhs.first)/lhs.second;
double tmp2=(double)(rhs.first)/rhs.second;
return tmp2<tmp1;
}
//commodity[0]不存储商品,从commodity[1]开始存储商品;
double fractionalKnapsackProblem(int totalWeight,vector< pair<int,int> >& commodity,vector< pair< pair<int,int>,int> >& commodityChoice)
{
sort(commodity.begin(),commodity.end(),compare1);//根据商品单位重量价值,从大到小排序;
int remainingWeight=totalWeight;
double maxProfit=0;
for(int i=1;i!=commodity.size()&&remainingWeight>0;++i)
{
if(commodity[i].second<=remainingWeight){
commodityChoice.push_back(make_pair(make_pair(commodity[i].first,commodity[i].second),commodity[i].second));
maxProfit+=commodity[i].first;
remainingWeight-=commodity[i].second;
}
else{
commodityChoice.push_back(make_pair(make_pair(commodity[i].first,commodity[i].second),remainingWeight));
maxProfit+=(double)(commodity[i].first)/commodity[i].second*remainingWeight;
remainingWeight=0;
}
}
return maxProfit;
}
0-1背包问题:
void zeroOneKnapsackProblem(int totalWeight,vector< pair<int,int> >& commodity,vector< vector<int> >& profit)
{
int commodityCount=commodity.size()-1;
profit.resize(commodityCount+1);
for(int i=0;i!=profit.size();++i)
profit[i].resize(totalWeight+1);
for(int i=0;i!=profit.size();++i)
{
profit[0][i]=0;
profit[i][0]=0;
}
for(int i=1;i<=commodityCount;++i)
for(int w=1;w<=totalWeight;++w)
{
if(commodity[i].second<=w)
profit[i][w]=max(commodity[i].first+profit[i-1][w-commodity[i].second],profit[i-1][w]);
else
profit[i][w]=profit[i-1][w];
}
}
void zeroOneKnapsackProblem(int totalWeight,vector< pair<int,int> >& commodity)
{
vector< vector<int> > profit;
zeroOneKnapsackProblem(totalWeight,commodity,profit);
int commodityCount=commodity.size()-1;
cout<<"the maximum profit: "<<profit[commodityCount][totalWeight]<<endl;
cout<<"the corresponding commodity: "<<endl;
int w=totalWeight;
int i=commodityCount;
while(w>0&&i>0){
if(commodity[i].second<=w){
if(commodity[i].first+profit[i-1][w-commodity[i].second]>=profit[i-1][w]){
cout<<"("<<commodity[i].first<<" , "<<commodity[i].second<<" )"<<endl;
w-=commodity[i].second;
i=i-1;
}
else{
i=i-1;
}
}
else{
i=i-1;
}
}
}
赫夫曼编码:
1:编码树中结点定义如下:
struct node{
char character;
int freq;
node* left;
node* right;
node(const char& c=char(),int f=0,node* l=0,node* r=0):character(c),freq(f),left(l),right(r){}
};
2:为了实现编码树,在队列中存储的是结点指针,但在c++中结点指针不能比较,因此可以创立一个类来存储结点指针同时通过比较这个类来比较结点存储的频率大小。
struct nodePointer{
node* pointee;
explicit nodePointer(node* p=0):pointee(p){}
};//存储结点指针
struct compare{
bool operator()(const nodePointer& lhs,const nodePointer& rhs) { return lhs.pointee->freq>rhs.pointee->freq; }
};//比较结点中频率的大小
3:队列的定义如下:
priority_queue<nodePointer,vector<nodePointer>,compare > characterSetPriorityQueue;
4:构造编码树:
void huffmanCode::buildCode()
{
int size=characterSetPriorityQueue.size();
for(int i=1;i!=size;++i)
{
node* tmp1=characterSetPriorityQueue.top().pointee;
characterSetPriorityQueue.pop();
node* tmp2=characterSetPriorityQueue.top().pointee;
characterSetPriorityQueue.pop();
node* tmp=new node();
tmp->left=tmp1;
tmp->right=tmp2;
tmp->freq=tmp1->freq+tmp2->freq;
characterSetPriorityQueue.push(nodePointer(tmp));
}
root=characterSetPriorityQueue.top().pointee;
characterSetPriorityQueue.pop();
}
5:解码的过程如下:
现在有了二进制编码,要根据已经建立的编码树,将这些二进制编码转化为字符信息:
void huffmanCode::decode(const string& binaryCodeString)
{
buildCode();
node* currentNode=root;
for(int i=0;i!=binaryCodeString.size();++i)
{
if(binaryCodeString[i]=='0')
currentNode=currentNode->left;
else if(binaryCodeString[i]=='1')
currentNode=currentNode->right;
else
throw runtime_error("the binary code string is illegal!");
if(currentNode->left==0){
cout<<currentNode->character;
currentNode=root;
}
}
}
任务调度问题:
1:任务类定义如下:
struct task{
int label;//存储任务标号;
int deadline;//存储任务要完成的截止时间;
int punishment;//存储任务没有在截止时间完成所受的惩罚;
task(int l=0,int d=0,int p=0):label(l),deadline(d),punishment(p){}
};
2:在任务调度问题中,我们需要将惩罚值从大到小排序,将任务的截止时间从小到大排序。定义的比较函数如下:
//将截止时间从小到大排序;
bool compareDeadline(const task& lhs,const task& rhs)
{
return lhs.deadline<rhs.deadline;
}
//将惩罚值从大到小排序;
bool comparePunishment(const task& lhs,const task& rhs)
{
return rhs.punishment<lhs.punishment;
}
3:我们需要判断一个任务是否属于书中所定义的独立集合。代码如下:
bool isIndependent(vector<int>& deadlineCount,int candidateTaskDeadline)
{
for(int i=candidateTaskDeadline;i!=deadlineCount.size();++i)
deadlineCount[i]++;
bool dependent=true;
for(int i=candidateTaskDeadline;dependent&&i!=deadlineCount.size();++i)
if(deadlineCount[i]>i)
dependent=false;
if(!dependent){
for(int i=candidateTaskDeadline;i!=deadlineCount.size();++i)
deadlineCount[i]--;
}
return dependent;
}
4:有了上面一系列的类和函数后,求解任务调度的主要函数如下:
//optimumTaskOrder数组中存储的就是最优的任务排序。
int taskArrangment(vector<task>& taskSet,vector<task>& optimumTaskOrder)
{
sort(taskSet.begin(),taskSet.end(),comparePunishment);//将惩罚值从大到小排序;
vector<task> maxIndependentSet;
vector<task> remainingTaskSet;
vector<int> deadlineCount(taskSet.size()+1,0);
for(int i=0;i!=taskSet.size();++i)
if(isIndependent(deadlineCount,taskSet[i].deadline))
maxIndependentSet.push_back(taskSet[i]);
else
remainingTaskSet.push_back(taskSet[i]);
sort(maxIndependentSet.begin(),maxIndependentSet.end(),compareDeadline);//将最大独立集合中任务的截止时间从早到晚排序;
for(int i=0;i!=maxIndependentSet.size();++i)
optimumTaskOrder.push_back(maxIndependentSet[i]);
int minPunishment=0;
for(int i=0;i!=remainingTaskSet.size();++i)
{
optimumTaskOrder.push_back(remainingTaskSet[i]);
minPunishment+=remainingTaskSet[i].punishment;
}
return minPunishment;
}
找零问题:
在找零问题中如果面额设置合适可以用贪心算法求解,如果设置不合适就只能用动态规划算法求解。
1:贪心算法如下:
//denomination数组中存储了从大到小的面额。
//solution[i]中存储了对应的面额denomination[i]所使用的数目
void changeGreedyChoice(int totalChange,const vector<int>& denomination,vector<int>& solution)
{
solution.resize(denomination.size(),0);
for(int i=0;i!=denomination.size();++i)
{
solution[i]=totalChange/denomination[i];
totalChange%=denomination[i];
if(totalChange==0)
break;
}
}
void printChangeGreedyChoice(int totalChange,const vector<int>& denomination)
{
vector<int> solution;
changeGreedyChoice(totalChange,denomination,solution);
for(int i=0;i!=solution.size();++i)
if(solution[i]!=0)
cout<<denomination[i]<<" : "<<solution[i]<<endl;
}
2:动态规划算法如下:
//数组denomination中存储的硬币面额没有必要经过排序,可以任意;
//solution[i]存储的是为了找i美分零钱所使用的第一个面额树。这个和贪心算法中数组solution的定义是不一样的。
void changeDynamicProgramming(int totalChange,const vector<int>& denomination,vector<int>& solution)
{
solution.resize(totalChange+1,0);
vector<int> minCoinCount(totalChange+1,0);
for(int change=1;change<=totalChange;++change)
{
minCoinCount[change]=INT_MAX;
for(int i=0;i!=denomination.size();++i)
if(denomination[i]<=change){
int coinCount=1+minCoinCount[change-denomination[i]];
if(coinCount<=minCoinCount[change]){
minCoinCount[change]=coinCount;
solution[change]=denomination[i];
}
}
}
}
void printChangeDynamicProgramming(int totalChange,const vector<int>& denomination)
{
vector<int> solution;
changeDynamicProgramming(totalChange,denomination,solution);
cout<<"denomination required below: "<<endl;
while(totalChange!=0){
cout<<solution[totalChange]<<" ";
totalChange-=solution[totalChange];
}
}