贪心算法基本原理
贪心算法的核心就是贪,就是总是做出当前看来最优的选择,因此可知,贪心算法不从整体去考虑,它做出的选择也是局部最优选择,从而达到全局优化选择。虽然贪心算法不一定能得到最优解,但是对很多问题,它是能够得到整体最优解的,因此贪心算法是否能到最优解,需要严格证明。
贪心算法产生有化解的条件
- 贪心选择性质:
若一个问题的全局最优解可以通过局部最优解来得到,则说明该问题具有贪心选择性质。 - 优化子结构:
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。
贪心算法和动态规划的区别
- 贪心算法是自顶向下的,而动态规划则是自底向上的。
- 动态规划是自底向上求出各子问题的有化解,最后汇集有化解从而得出问题的全局最优解(可以想象成各个小河流入大海)
贪心算法是自顶下向下,以迭代的方式一步一步做出贪心选择,从而把问题简化成规模更小的问题
贪心算法的基本思路
从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到算法中的某一步不能再继续前进时,算法停止。
该算法存在问题:
1. 不能保证求得的最后解是最佳的;
2. 不能用来求最大或最小解问题;
3. 只能求满足某些约束条件的可行解的范围。
实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解;
拿经典的背包问题来讲解贪心算法
01背包问题:有一个背包,背包容量是M=30。有3个物品,要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
我们制定贪心的策略总共有3种:
1. 每次挑选重量最小的放入背包
2. 每次挑选价值最大的放入背包
3. 每次挑选单位重量价值最大的放入背包
我们试着来证明这三种可行:
第一种:
物品:A B C
重量 10 30 10
价值 20 80 20
每次挑选重量最小,AC放入背包后,B就不能放入背包了,放入AC的价值为40,而选择放入B的价值为80,明显不成立
第二种:和第一种类似
第三种:
物品:A B C
重量 10 30 10
价值 10 30 10
每次挑选单位重量价值最大,ABC的单位重量价值都一样,无法选择,因此也不成立。
总结:
证明了,01背包问题是不能使用贪心算法来解决的,而是应该使用动态规划—— 动态规划 01背包问题
但是如果加一个条件,物品是可以拆分的,那么此背包问题就可以用贪心算法来解决,因此可以拆分,使得背包空间得到充分利用
可拆分物品背包问题代码实现
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN = 100;
//事先按照单位重量的商品价值排序了
float weight[MAXN] = {10,30,20,5};//商品重量
float value[MAXN] = {200,400,100,10}; //商品价值
float x[MAXN];//保存能存放物品的比例
int N = 4;
float M;
void bag(){
int i = 0;
for(i; i < N; i++){
if(weight[i] > M){
break;
}
x[i] = 1;
M -= weight[i];
}
if(i < N){
x[i] = M / weight[i];
}
}
int main(){
cin >> M;
bag();
for(int i = 0; i < N; i++){
cout << "背包能放入物品" << i+1 << "的比例:" << x[i] << endl;
}
}
在这里,我使用的是第三种策略
贪心算法经典题目
1.活动选择问题
有n个需要在同一天使用同一个教室的活动a1,a2,…,an,教室同一时刻只能由一个活动使用。每个活动ai都有一个开始时间si和结束时间fi 。一旦被选择后,活动ai就占据半开时间区间[si,fi)。如果[si,fi]和[sj,fj]互不重叠,ai和aj两个活动就可以被安排在这一天。该问题就是要安排这些活动使得尽量多的活动能不冲突的举行。例如下图所示的活动集合S,其中各项活动按照结束时间单调递增排序。
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN = 100;
struct Activity{
int start;
int end;
}act[MAXN];
int N;
//自己写一个排序函数,按照活动的结束时间来从早到晚排序
int cmp(Activity a, Activity b){
if(a.end < b.end){
return 1;
}else{
return 0;
}
}
void greedy_act(){
cout << "活动" << 1 << endl;
int i = 0, j;
for(j = 1; j < N; j++){
if(act[j].start > act[i].end){
cout << "活动" << j+1 << endl;
i = j;
}
}
}
int main(){
cin >> N;
for(int i = 0; i < N; i++){
cin >> act[i].start >> act[i].end;
}
sort(act,act + N , cmp);
greedy_act();
}
2.钱币找零问题
假设1元、2元、5元、10元、20元、50元、100元的纸币分别有c0, c1, c2, c3, c4, c5, c6张。现在要用这些钱来支付K元,至少要用多少张纸币?用贪心算法的思想,很显然,每一步尽可能用面值大的纸币即可。在日常生活中我们自然而然也是这么做的。在程序中已经事先将Value按照从小到大的顺序排好。
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 7;
int Count[N] = {3, 0, 2, 1, 2, 2, 3};
int value[N] = {1, 2, 5, 10, 20, 50, 100};
void solve(int money){
for(int i = N-1; i >= 0; i--){
int c = min(money / value[i], Count[i]);
money = money - c * value[i];
if(c != 0){
printf("面额:%d 数量:%d\n", value[i], c);
}
}
if(money > 0){
printf("\n不能找零");
}else{
printf("\n可以找零");
}
}
3.小船过河问题
只有一艘船,能乘2人,船的运行速度为2人中较慢一人的速度,过去后还需一个人把船划回来,问把n个人运到对岸,最少需要多久。先将所有人过河所需的时间按照升序排序,我们考虑把单独过河所需要时间最多的两个旅行者送到对岸去。
有两种方式:
1.最快的和次快的过河,然后最快的将船划回来;次慢的和最慢的过河,然后次快的将船划回来,所需时间为:t[0]+2*t[1]+t[n-1];
2.最快的和最慢的过河,然后最快的将船划回来,最快的和次慢的过河,然后最快的将船划回来,所需时间为:2*t[0]+t[n-2]+t[n-1]。
#include<iostream>
#include<algorithm>
using namespace std;
int t[1000], n, sum = 0;
int main(){
cin >> n;
for(int i = 0; i < n; i++){
cin >> t[i];
}
//按照过河时间从短到长排序
sort(t, t+n);
//按照贪心算法有两种方式
//1.最快和次快,最快回来;最慢和次慢,次快回来
//2.最快和最慢,最快回来;最快和次慢,最快回来
//每一次送过最慢和次慢,直到送完
while(n > 3){
sum = min(t[0] + 2 * t[1] + t[n-1], 2*t[0] + t[n-1] + t[n-2]);
n-=2;
}
if(n==3){
sum += 2*t[0] + t[n-1] + t[n-2];
}else if(n==2){
sum += t[0] + t[1];
}else{
sum += t[0];
}
cout << sum;
}
4.销售比赛
假设有偶数天,要求每天必须买一件物品或者卖一件物品,只能选择一种操作并且不能不选,开始手上没有这种物品。现在给你每天的物品价格表,要求计算最大收益。首先要明白,第一天必须买,最后一天必须卖,并且最后手上没有物品。那么除了第一天和最后一天之外我们每次取两天,小的买大的卖,并且把卖的价格放进一个最小堆。如果当前的两天买的价格都大于之前选的两个卖的价格,就选择之前两天买入,当前两天卖出,这样就一定能取得最大收益。
例如:
6天,物品的价格分别为1, 2, 3, 5, 6, 5;
第一天买入,最后一天卖出。2-3天买入,4-5天卖出。但是按照贪心算法2-3天应该选择价格低的买入,价格高的卖出,因为你不可能知道接下来的4-5天价格是否比2-3天的大,这就是res += buy + sell - 2*q.top();的意义。
#include<queue>
#include<vector>
#include<iostream>
#include<algorithm>
using namespace std;
priority_queue<int, vector<int>, greater<int> > q;
int price[1000], res;
int N;
int main(){
cin >> N;
for(int i = 1; i <= N; i++){
cin >> price[i];
}
res -= price[1];
res += price[N];
for(int i = 2; i < N; i+=2){
int buy = min(price[i], price[i+1]);
int sell = max(price[i], price[i+1]);
if(!q.empty()){
if(buy > q.top()){
res += buy + sell - 2*q.top();
q.pop();
q.push(buy);
q.push(sell);
}else{
res += sell - buy;
q.push(sell);
}
}else{
res = res - buy + sell;
q.push(sell);
}
}
cout << endl << res;
}
priority_queue是一个优先级队列,值越小,优先级越高。