一、贪心算法介绍
贪心算法,也就是可以通过局部的最优得到全局最优,其证明很多时候是复杂的。
注意点:
- 很多贪心算法使用的时候需要进行排序,这个时候需要我们规定排序灯的规则,那么要保证规定的排序方式是有传递性的,也就是正确的
- 在贪心解决的问题的时候很多情况会使用到堆,所以注意使用堆进行排序
二、贪心问题举例
1、字符串连接
题目描述:给定一个字符串类型的数组strs,找一种拼接的方式,使得把所有的字符串拼起来之后形成的字符串具有最小的字典序。
贪心策略:对字符串排序,排序的规则是:有两个字符串str1和str2,那么如果str1str2的字典序小于str2str1那么就将str1排在str2之前。排序结束之后,将各个字符串顺序连接,得到的字符串就是字典序最小的字符串。
字典序:可以理解为26进制的数字之间的比较,但是两个字符串的长度需要相等。那么如果是长度不等的字符串,那么就将短的字符串后默认补上空字符,空字符的值比a要小。
证明:
这里我们将字符串理解为数字,而字符串的连接:
有字符串a和b,那么ab的值就相当于a * 26^(b.length)+b,而ba的值相当于b * 26^(a.length)+a。
首先证明这个比较的规则有传递性,也就是如果ab<=ba,bc<=cb,那么也就是在字符的大小顺序中为a,b,c,那么应当可以推出ac<=ca。
证明:首先ab=a * 26^(b.legth)+b
ba=b * 26^(a.leght)+a,又因为ab<=ba,那么
a26^(b.length)+b <= b * 26^(a.length)+a (1)
同理有:
b * 26^(c.legth)+c <= c * 26^(b.leght)+b (2)
对(1)减去a在乘上c得到
a * 26^(b.length) * c <= (b * 26^(a.length)+a-b) * c
对(2)减去b乘上a得到
(b * 26^(c.length)+c-b) * a <= c * 26^(b.length) * a
综合上述式子得到:
(b * 26^(c.length)+c-b) * a <= (b * 26^(a.length)+a-b) * c
打开括号得到:
ab26^(c.len)+ac-ab <= bc26^(a.len)+ac-bc
两边消去ac和b得到
a*26^(c.len)-a <= c * 26^(a.len)-b
即ac<=ca
之后再证明局部最优得到全局最优:可以这样想任意字符串s1排在s2前面,那么如果交换两者的位置的得到的字符串的字典序肯定比不换要大(根据排序的规则)。
2、Huffman编码在贪心问题中的使用
Huffman编码使用的场景:
- 要求与顺序无关
- 代价为两个部合起来的某个指标
题目:一块金条只要切一次就要花费和金条长度相等的代价。一群人想分整块金条,怎么分最省铜板。
例如:给定数组{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。
输入一个数组,返回分割的最小代价。
分析:没有指定分割的顺序,也就是与顺序无关,并且每次的代价都是两部分的代价和,考虑使用huffman编码的方法。
使用一个小根堆,将数组放入小根堆中,每次弹出堆顶的两个元素,再将其和压入堆中,每次得到的和相加就是最后的最小的代价。
**总体的思路就是先尽量将大的部分分出去。**否则大的部分可以分出去,但是没有分出去的话就会多次贡献代价,导致最后代价过大。
代码:
//C++中priority_queue是大根堆
int leastMoney(vector<int> a)
{
int sum = 0;
priority_queue<int,vector<int>,greater<int> > q;
int len = a.size();
//cout<<"size:"<<len<<endl;
for(int i = 0;i<len;++i)
{
q.push(a[i]);
}
int tmp = 0;
//当只有一个元素的时候停止
//返回堆顶的两个元素,然后将其和压入堆中
while(q.size()>1)
{
tmp = q.top();
q.pop();
//cout<<"tmp1:"<<tmp<<endl;
tmp += q.top();
//cout<<"tmp2:"<<tmp<<endl;
q.pop();
sum += tmp;
q.push(tmp);
}
return sum;
}
int main()
{
vector<int> a;
a.push_back(10);
a.push_back(20);
a.push_back(30);
int sum = leastMoney(a);
cout<<sum<<endl;
return 0;
}
3、获得最大利润
题目:输入代价和纯利润数组,k表示只能串行的最多做k个项目;m表示初始的资金。假设每做完一个项目,马上获得的纯利润可以支持去做下一个项目。请输出最后获得的最大钱数。
思路:在能做的项目中先做利润最大的项目。
实现:使用两个堆,一个大根堆,一个小根堆。先将项目加入小根堆中,然后将小根堆中弹出代价小于现有资本的项目到大根堆中,大根堆按照利润进行排序,依次做利润最大的。
代码:
//项目结构,因为将项目加入堆中会打乱数组中代价和纯利润的对应关系
struct project
{
int cost;
int profit;
project(int x,int y)
{
cost = x;
profit = y;
}
} ;
//c++中自定义优先队列的比较器
//根据代价定义小根堆
//注意自己定义比较器的时候,优先队列是按照返回false进行比较的
struct cmp1{
bool operator()(project x,project y)
{
return y.cost<x.cost;
}
};
struct cmp2
{
bool operator()(project x,project y)
{
return x.profit<y.profit;
}
};
//返回最大的代价
int mostProfit(int k,int m,vector<int> cost,vector<int> profit)
{
priority_queue<project,vector<project>,cmp1 > minQ;
priority_queue<project,vector<project>,cmp2 > maxQ;
int len = cost.size();
for(int i = 0;i<len;++i)
{
minQ.push(project(cost[i],profit[i]));
}
//可以进行k个项目
for(int i = 0;i<k;++i)
{
while(!minQ.empty()&&minQ.top().cost<=m)
{
maxQ.push(minQ.top());
minQ.pop();
}
//说明即使还没有做k个项目,但是剩下钱已经不够了
if(maxQ.empty())
return m;
//选出符合的利润最大的项目
m+=maxQ.top().profit;
maxQ.pop();
}
return m;
}
int main()
{
vector<int> cost;
vector<int> profit;
cost.push_back(3);
cost.push_back(1);
cost.push_back(5);
cost.push_back(2);
profit.push_back(2);
profit.push_back(4);
profit.push_back(9);
profit.push_back(5);
int res = mostProfit(3,3,cost,profit);
cout<<res<<endl;
return 0;
}
总结:在这个贪心问题中,贪心的方法是显示中我们使用的,但是在实现的时候,利用堆可以很大简化代码的实现。
可以看出在贪心的问题中会经常使用排序和堆,一般如果我们需要将原来的值进行一定的操作并将得到的新的值和原来的值进行比较那么一般使用堆,否则就进行简单排序即可。
4、获得中位数
题目:不断给定数字,求所给数字的中位数。
思路:使用堆,求中位数,那么我们就要将数据分为比较小的一半和比较大的一半。
同样使用两个堆,一个小根堆,一个大根堆。
开始的时候将第一个数放进大根堆中,然后对于后来的每一个数,如果其小于等于大根堆堆顶的数就将其加入到大根堆中,否则就加到小根堆中。
每次将数字加入堆中之后,检查两个堆中的数字的个数,如果两个堆的数字的数量相差为2,那么就把个数多的那个堆的堆顶的元素弹出压入到个数少的堆中。
这样我们就能保证大根堆中是比较小的N/2个数,小根堆中的是比较大的N/2个数。
如果大根堆与小根堆的数字数量不同,那么说明总的数是奇数个,弹出个数多的堆的堆顶的元素就是中位数;如果相等就是偶数个,弹出两个堆的堆顶,去平均得到中位数。
代码:
int main()
{
priority_queue<int> maxQ;//大根堆
priority_queue<int,vector<int>,greater<int> > minQ;//大根堆
int num;
int size1;
int size2;
//假设不断输入10个数
for(int i = 0;i<10;++i)
{
cin>>num;
//如果是第一个数字或者其小于等于大根堆的堆顶,进入大根堆
if(maxQ.empty()||num<=maxQ.top())
{
maxQ.push(num);
}
//否则进入小根堆
else
minQ.push(num);
//判断两个堆的个数之间的差
size1 = maxQ.size();
size2 = minQ.size();
if(size1==size2+2)
{
minQ.push(maxQ.top());
maxQ.pop();
size2++;
size1--;
}
else if(size2==size1+2)
{
maxQ.push(minQ.top());
minQ.pop();
size2--;
size1++;
}
//得到结果
if(size1==size2)
cout<<(minQ.top()+maxQ.top())/2<<endl;
else if(size1>size2)
cout<<maxQ.top()<<endl;
else if(size2>size1)
cout<<minQ.top()<<endl;
}
return 0;
}