贪心算法的定义
在某一个标准下,优先考虑最满足标准的样本,然后考虑不那么满足标准的样本,最终得到某一个答案的算法,就是贪心算法。也就是说,你得出的答案只是满足这个标准的最优解(局部最优),可能不是全局最优解。
根据下面这个例子来说明贪心算法
给出字符串数组strs,请找出一种排序方式,使得所有的字符串拼接后形成的字符串具有最小的字典序。
字典序:按照字母 abcdef…z 排序。如果有两个字符串“abc”和“abd”,则前者应排在后者之前(字典序的排序标准其实就是程序中字符串的比较标准),可以理解为通过比较两个字符串中各个字母代表的权重之和来进行排序。
首先提出一个策略(标准),将 strs 按自身的字典序排序后依次拼接,得到新的字符串。对于这个策略,我们可以轻易举出反例,两个字符串“ba” “b”,按照该种策略形成的字符串应该时“bba”,实际上字典序最小的拼接方式是“bab”。所以该种策略不能得到题目的正确答案(不是全局最优解)。
经过失败的尝试后再次提出新策略(标准),如果有两个字符串str1和str2,str1拼接上str2后的新字符串小于str2拼接str1形成的新字符串,就将str1排str2的前面。经过排序后按照排序顺序依次进行拼接。接下来证明该种策略可以得到全局最优解。
该策略重点在于提出了一个新的比较方式,所以必须保证该种比较方式具有传递性。
即如果 a.b <= b.a;(a.b代表字符串a拼接字符串b)
且 b.c <= c.b;
那么能否证明 a.c <= c.a。
因为a.b可以表示为a * k^(length(b)) + b(k表示字母a到z的进制,length(b)表示b的长度,a * k^(length(b))表示字符串a向左移动b的长度位,后面使用m(b)来代表k^(length(b))),所以证明可以改为
a * m(b) + b <= b * m(a) + a;
b * m(c) + c <= c * m(b) + b;
=> a * m(c) + c <= c * m(a) + a。
证明:
条件1 等式两边同时 - b,然后同时 * c,得到 a * m(b) * c <= b * m(a) * c + a * c - b * c;
条件2 等式两边同时 - b,然后同时 * a,得到 b * m© * a + c * a - b * a <= c * m(a) *a;
所以有 b * m© * a + c * a - b * a <= b * m(a) * c + a * c - b * c
=> b * m© * a - b * a <= b * m(a) * c - b * c
=> m© * a - a <= m(a) * c - c
即 a.c <= c.a。
证明过比较器具有传递性(即经过排序后得到的结果是唯一的,最终结果与各个单位的位置无关)后,接下来证明按照该种排序方式拼接后最终得到的字符串具有最小的字典序。
先证明任意一个靠前字符串拼接一个靠后的字符串形成的字符串一定比交换位置后拼接形成的字符串具有更小的字典序。
设排序后得到的字符串为 [。。。。。。A。。。。。。B。。。。。。],其中A和B是其中任意位置的两个字符串(A在B前)。
- 如果A和B相邻,那么 A.B <= B.A 成立。
2.如果AB不相邻,即 [。。。。。。A m n B。。。。。。] ,根据上面证明出的结论,有
[。。。。。。A m n B。。。。。。]
<= [。。。。。。m A n B。。。。。。]
<= [。。。。。。m n A B。。。。。。]
<= [。。。。。。m n B A。。。。。。]
<= [。。。。。。m B n A。。。。。。]
<= [。。。。。。B m n A。。。。。。]
证明结束。
接下来证明多个字符串交换位置后的情况,不想证了。。。(用数学归纳法)
经过证明,该种策略能得到题目的全局最优解。
——————————————————————————————————————————
#include <iostream>
#include <algorithm>
using namespace std;
bool compare(const string& str_1,const string& str_2){
return str_1 + str_2 < str_2 + str_1;
}
string minDirectoryOrderStr(string strs[],int num){
if(strs == NULL || num <= 0)
return " ";
sort(strs,strs + num,compare);
string res = "";
for(int i = 0; i < num; i++){
res += strs[i];
}
return res;
}
int main()
{
string strs[3] = {"ba","b","dhfj"};
cout << minDirectoryOrderStr(strs,3) << endl;
return 0;
}
通过这个题目我们可以总结出贪心算法的求解过程
-
通过题目提出一种解题策略;
-
证明该策略可以达到全局最优;
-
实现策略。
可以看出,贪心算法的证明过程非常繁琐,因此除了为了专门学习外,通常不会进行证明(如笔试或比赛时),而会采用对数器的方法来进行验证。即贪心算法问题的实际求解过程为:
-
可以通过暴力尝试来实现一个绝对正确的解法A;
-
提出策略1,2,3.。。。
-
通过解法A和对数器来验证自己的策略。
贪心算法相关题目
- 一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金 条,不管切成长度多大的两半,都要花费20个铜板。 一群人想整分整块金条,怎么分最省铜板?
例如,给定数组{10,20,30},代表一共三个人,整块金条长度为10+20+30=60。 金条要分成10,20,30三个部分。 如果先把长度60的金条分成10和50,花费60; 再把长度50的金条分成20和30,花费50;一共花费110铜板。 但是如果先把长度60的金条分成30和30,花费60;再把长度30金条分成10和20, 花费30;一共花费90铜板。
输入一个数组,返回分割的最小代价。
分析:这种求最小总代价的题目可以考虑使用哈夫曼编码。
#include <iostream>
#include <queue>
using namespace std;
int splitGold(int arr[], int n){
if(arr == NULL || n <= 0)
return 0;
int res = 0;
priority_queue<int,vector<int>,greater<int> > q;
for(int i = 0; i < n; i++){
q.push(arr[i]);
}
while(!q.empty() && q.size() > 1){
int min_1 = q.top();
q.pop();
int min_2 = q.top();
q.pop();
q.push(min_1 + min_2);
res = res + min_1 + min_2;
}
return res;
}
int main()
{
int arr[7] = {7,6,3,2,4,5,1};
cout << splitGold(arr,7);
return 0;
}
2.一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数 组,里面是一个个具体 的项目),你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。 返回这个最多的宣讲场次。
分析:在提出策略时可以发现不论是按照项目开始时间排序,还是按照项目时长排序,都不能达到问题的最优解,而按照项目的结束时间排序时,得到了最优解。
#include <iostream>
#include <algorithm>
using namespace std;
class project{
public:
int start;
int end;
project(int start,int end){
this->start = start;
this->end = end;
}
bool operator < (const project p){
return this->end < p.end;
}
};
struct Compare{
bool operator() (project p1,project p2){
return p1.end > p2.end;
}
};
int bestArrange(project arr[],int n){
if(arr == NULL || n <= 0){
return 0;
}
int cur = 0;
int res = 0;
sort(arr,arr + n);
for(int i = 0; i < n; i++){
if(arr[i].start >= cur){
cur = arr[i].end;
res++;
}
}
return res;
}
int main()
{
project p(10,12);
project p_1(11,13);
project p_2(9,12);
project p_3(13,15);
project p_4(14,15);
project p_5(16,17);
project arr[6] = {p,p_1,p_2,p_3,p_4,p_5};
cout << bestArrange(arr,6);
return 0;
}
- 一个不停吐出数字的数据流中,随时可以取得已突出数字的中位数。
分析:这里需要使用大根堆和小根堆,将第一个数先存入大根堆,第二个数如果小于大根堆堆首,加入大根堆,否则加入小根堆。当两个堆结构的size的差值大于等于2时,将较多的堆首元素加入到数量较小的堆中。取中位数时,如果两个堆的数量相等,取两个堆堆首的平均值,否则取size值较大的堆的堆首元素。
#include <iostream>
#include <queue>
#include <cmath>
using namespace std;
class MediumNumber{
public:
priority_queue<int> maxHeap;
priority_queue<int,vector<int>,greater< int> > minHeap;
void medifyHeap(priority_queue<int>& maxHeap,priority_queue<int,vector<int>,greater< int> >& minHeap){
if(maxHeap.size() > minHeap.size()){
minHeap.push(maxHeap.top());
maxHeap.pop();
}else{
maxHeap.push(minHeap.top());
minHeap.pop();
}
}
void addNumber(int num){
if(maxHeap.empty() || maxHeap.top()> num){
maxHeap.push(num);
}else{
minHeap.push(num);
}
//cout << num <<endl;
if(abs(maxHeap.size() - minHeap.size()) >= 2)
medifyHeap(maxHeap,minHeap);
}
int getMedium(){
if(maxHeap.size() == minHeap.size())
return (maxHeap.top() + minHeap.top()) / 2;
return maxHeap.size() > minHeap.size() ? maxHeap.top() : minHeap.top();
}
};
int main()
{
MediumNumber mu;
mu.addNumber(1);
mu.addNumber(4);
mu.addNumber(6);
mu.addNumber(8);
mu.addNumber(3);
mu.addNumber(9);
cout << mu.getMedium() << endl;
return 0;
}
- 输入: 正数数组costs 正数数组profits 正数k 正数m
含义: costs[i]表示i号项目的花费 profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润) k表示你只能串行的最多做k个项目 m表示你初始的资金
说明: 你每做完一个项目,马上获得的收益,可以支持你去做下一个项目。
输出: 你最后获得的最大钱数
#include <iostream>
#include <queue>
using namespace std;
typedef struct Project{
int cost;
int profites;
Project(int cost,int profites){
this->cost = cost;
this->profites = profites;
}
Project(Project& p){
this->cost = p.cost;
this->profites = p.profites;
}
Project(){this->cost = INT_MAX;this->profites = INT_MIN;}
bool operator < (Project* p){
return this->cost < p->cost;
}
}Project;
struct CompareProfites{
bool operator()(Project* p_1,Project* p_2){
return p_1->profites < p_2->profites;
}
};
struct CompareCosts{
bool operator()( Project* p_1,Project* p_2){
return p_1->cost > p_2->cost;
}
};
int getMaxProfits(int k,int m,Project p[],int n){
priority_queue<Project*,vector<Project*>,CompareProfites> profitesHeap;
priority_queue<Project*,vector<Project*>,CompareCosts> costHeap;
for(int i = 0; i < n; i++){
costHeap.push(&p[i]);
}
while(k > 0 && costHeap.top()->cost <= m){
while(costHeap.top()->cost <= m && !costHeap.empty()){
//cout << costHeap.top()->cost << endl;
profitesHeap.push(costHeap.top());
costHeap.pop();
}
//cout << profitesHeap.top()->profites << endl;
m += profitesHeap.top()->profites;
k--;
profitesHeap.pop();
}
return m;
}
int main()
{
Project p(10,13);
Project p_1(11,13);
Project p_2(9,12);
Project p_3(13,15);
Project p_4(14,15);
Project p_5(16,17);
Project arr[6] = {p,p_1,p_2,p_3,p_4,p_5};
cout << getMaxProfits(3,10,arr,6) << endl;
return 0;
}
在解决贪心算法的问题时,可以根据某标准建立一个比较器来排序、或建立一个比较器组成堆。