贪心算法详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jeffleo/article/details/53526721

贪心算法基本原理

贪心算法的核心就是贪,就是总是做出当前看来最优的选择,因此可知,贪心算法不从整体去考虑,它做出的选择也是局部最优选择,从而达到全局优化选择。虽然贪心算法不一定能得到最优解,但是对很多问题,它是能够得到整体最优解的,因此贪心算法是否能到最优解,需要严格证明。

贪心算法产生有化解的条件

  1. 贪心选择性质:
    若一个问题的全局最优解可以通过局部最优解来得到,则说明该问题具有贪心选择性质。
  2. 优化子结构:
    当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。

贪心算法和动态规划的区别

  1. 贪心算法是自顶向下的,而动态规划则是自底向上的。
  2. 动态规划是自底向上求出各子问题的有化解,最后汇集有化解从而得出问题的全局最优解(可以想象成各个小河流入大海)
    这里写图片描述
    贪心算法是自顶下向下,以迭代的方式一步一步做出贪心选择,从而把问题简化成规模更小的问题
    这里写图片描述

贪心算法的基本思路

从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到算法中的某一步不能再继续前进时,算法停止。
该算法存在问题:
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是一个优先级队列,值越小,优先级越高。

展开阅读全文

没有更多推荐了,返回首页