贪心算法

141 阅读3分钟

概念

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

基本思路

1. 建立数学模型来描述问题;
2. 把求解的问题分成若干个子问题;
3. 对每一子问题求解,得到子问题的局部最优解;
4. 把子问题的解局部最优解合成原来解问题的一个解。

算法实现

1. 从问题的某个初始解出发。
2. 采用循环语句,当可以向求解目标前进一步时,就根据局部最优策略,得到一个部分解,缩小问题的范围或规模。
3. 将所有部分解综合起来,得到问题的最终解。

实例1 背包问题

• 问题描述
有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
• 问题分析
1.目标函数: ∑pi最大,使得装入背包中的所有物品pi的价值加起来最大。
2.约束条件:装入的物品总重量不超过背包容量:∑wi<=M( M=150)
3.贪心策略:
○ 选择价值最大的物品
○ 选择重量最小的物品
○ 选择单位重量价值最大的物品
有三个物品A,B,C,其重量分别为{30,10,20},价值分别为{60,30,80},背包的容量为50,分别应用三种贪心策略装入背包的物品和获得的价值如下图所示:
这里写图片描述
三种策略
• 算法设计:
我们采用先拿单位价值最大的物品的策略,输入n和m后,我们计算每个物品的单位价值,将其进行逆序排序,接着依次拿单位价值最大的物品,直到到达背包的容量为止。
• 代码实现

#include <iostream>
    #include <stdlib.h>
    #include <algorithm>

    using namespace std;

    //背包结构体
    struct bag{
        int w; //物品重量
        int v; //物品总价值
        double w_v;  //物品单位价值
    };

    //实现降序排列
    bool cmp(bag x,bag y){
        return x.w_v>y.w_v;
    }

    int main()
    {
        //n个物品,背包容量为m
        int n,m;

        while(cin>>n>>m){
            struct bag b[n];
            for(int i=0;i<n;i++){
                cin>>b[i].w>>b[i].v;
                b[i].w_v=b[i].v/(b[i].w)*1.0;
            }

            sort(b,b+n,cmp);

            int maxV=0;
            for(int i=0;i<n;i++){
                if(b[i].w<=m){
                    m-=b[i].w;
                    maxV+=b[i].v;
                }else
                    break;
            }
            cout<<"背包最大价值为:"<<maxV<<endl;
        }
        return 0;
    }
    /*
    7 150
    35 10
    30 40
    60 30
    50 50
    40 35
    10 40
    25 30
    */

• 运行结果
这里写图片描述

2 活动安排问题

• 问题描述:
设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动 i 都有一个要求使用该资源的起始时间si和一个结束时间 fi ,且si小于fi ,要求设计程序,使得安排的活动最多。(ps:活动结束时间按从小到大排序)
这里写图片描述

• 问题分析:
活动安排问题要求安排一系列争用某一公共资源的活动。用贪心算法可提供一个简单、漂亮的方法,使尽可能多的活动能兼容的使用公共资源。设有n个活动的集合{0,1,2,…,n-1},其中每个活动都要求使用同一资源,如会场等,而在同一时间内只有一个活动能使用这一资源。每个活动i都有一个要求使用该资源的起始时间starti和一个结束时间endi,且 starti小于endi。如选择了活动 i,则它在半开时间区间[starti,endi)内占用资源。若区间[starti,endi)与区间[startj,endj)不相交,称活动i与活动j是相容的。也就是说,当 startj≥endi 或 starti≥endj 时,活动i与活动j相容。活动安排问题就是在所给的活动集合中选出最多的不相容活动。

• 算法设计:
若被检查的活动i的开始时间starti小于最近选择的活动j的结束时间endj,则不选择活动i,否则选择活动i加入集合中。运用该算法解决活动安排问题的效率极高。当输入的活动已按结束时间的升序排列,算法只需O(n)的时间安排n个活动,使最多的活动能相容地使用公共资源。如果所给出的活动未按非减序排列,可以用O(nlogn)的时间重排。
• 代码实现:
代码

#include <iostream>
#include <stdlib.h>
#include <algorithm>

using namespace std;

struct pro{
    int s;  //开始时间
    int e;  //结束时间
};

bool cmp(pro a,pro b){
    return a.e<b.e;
}

int main()
{
    int n;  //活动的数量
    while(cin>>n){
        struct pro p[n];
        struct pro t[n];
        for(int i=0;i<n;i++){
            cin>>p[i].s>>p[i].e;
        }
        sort(p,p+n,cmp);

        int maxN=0;
        int q=-1;
        for(int i=0;i<n;i++){
            if(q<=p[i].s){
                maxN++;
                q=p[i].e;
                t[i].s=p[i].s;
                t[i].e=p[i].e;
            }
        }
        cout<<"最多的活动个数是:"<<maxN<<endl;

        //cout<<"每个活动的开始时间和结束时间是:"<<endl;

        //for(int i=0;i<n;i++){
            //cout<<t[i].s<<" "<<t[i].e<<endl;
        //}

    }
    return 0;
}

/*
11
1 4
3 5
0 6
5 7
3 8
5 9
6 10
8 11
8 12
2 13
12 14

*/

运行结果:
这里写图片描述

3 最小生成树(克鲁斯卡尔算法)

• 问题描述
求一个连通无向图的最小生成树的代价(图边权值为正整数)。

输入

输出
每个用例,用一行输出对应图的最小生成树的代价。

样例输入
1
6
0 6 1 5 0 0
6 0 5 0 3 0
1 5 0 5 6 4
5 0 5 0 0 2
0 3 6 0 0 6

样例输出
15

• Kruskal算法简述

假设 WN=(V,{E}) 是一个含有 n 个顶点的连通网,则按照克鲁斯卡尔算法构造最小生成树的过程为:先构造一个只含 n 个顶点,而边集为空的子图,若将该子图中各个顶点看成是各棵树上的根结点,则它是一个含有 n 棵树的一个森林。之后,从网的边集 E 中选取一条权值最小的边,若该条边的两个顶点分属不同的树,则将其加入子图,也就是说,将这两个顶点分别所在的两棵树合成一棵树;反之,若该条边的两个顶点已落在同一棵树上,则不可取,而应该取下一条权值最小的边再试之。依次类推,直至森林中只有一棵树,也即子图中含有 n-1条边为止。

• 算法难点:

(1)边的选择要求从小到大选择,则开始显然要对边进行升序排序。
(2)选择的边是否需要,则从判断该边加入后是否构成环入手。
• 算法设计:
(1)对边升序排序
在此采用链式结构,通过插入排序完成。每一结点存放一条边的左右端点序号、权值及后继结点指针
(2)边的加入是否构成环
一开始假定各顶点分别为一组,其组号为端点序号。选择某边后,看其两个端点是否在同一组中,即所在组号是否相同,如果是,表示构成了环,则舍去。 如果两个端点所在的组不同,则表示可以加入,则将该边两端的组合并成同一组。

• 代码实现:

    #include <iostream>

    using namespace std;

    //图顶点的结构体
    struct node{
        int l;
        int r;
        int len;
        node *next;
    };

    //插入排序
    void insert(node *&h,node *p){
        node *q=h;
        while(q->next && q->next->len<=p->len)
        {
            q=q->next;
        }
        p->next=q->next;
        q->next=p;
    }

    int main(){
        node *h,*p;
        int n,m,x,temp;
        int *a;
        int i,j;
        int sum;
        cin>>n;
        while(n--){
            sum=0;
            cin>>m;
            a=new int[m+1];
            for (i=1; i<=m; i++){
                a[i]=i;
            }
            h=new node;
            p=h;
            p->next=NULL;
            for (i=1; i<=m; i++)
                for (j=1; j<=m; j++){
                    cin>>x;
                    if (i>j && x!=0){
                    p=new node;
                    p->l=i;
                    p->r=j;
                    p->len=x;
                    p->next=NULL;
                    insert(h,p); //调用插入排序
                }
            }
            p=h->next;
            while (p){
                if (a[p->l]!=a[p->r]){
                    sum+=p->len;
                    temp=a[p->l];
                    for(i=1; i<=m; i++)
                        if (a[i]==temp){
                            a[i]=a[p->r];
                        }
                    }
                p=p->next;
            }
            cout<<sum<<endl;
        }
        return 0;
    }
    /*
    1
    6
    0 6 1 5 0 0
    6 0 5 0 3 0
    1 5 0 5 6 4
    5 0 5 0 0 2
    0 3 6 0 0 6
    0 0 4 2 6 0
    */

运行结果
这里写图片描述