算法设计思想(1)——贪婪法

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

概念

贪婪法(greedy algorithm),又称为贪心算法,是寻找最优解问题的常用方法。这种方法模式一般将求解过程分成若干个步骤,在每个步骤都应用贪心原则,选取当前状态下最好的或者最优的选择,并以此希望最后堆叠出的结果也是最好的或者最优的解。

贪婪法每次决策都以当前情况为基础并根据某个最后原则进行选择,不从整体上考虑其他各种可的情况。

贪婪法和动态规划法以及分治法一样,都需要对问题进行分解,定义最优解的子结构。但是,贪婪法不同之处在于,贪婪法每一步选择完之后,局部最优解就确定了,不再进行回溯处理,也就是说,每个步骤的局部最优解确定以后,就不再修改,直到算法结束。因为不进行回溯处理,贪婪法只在很少的情况下可以得到真正的最优解,比如最短路径,图的最小生成树等。大多数情况下,由于选择策略的短视,贪婪法会错过真正的最优解,得不到问题的真正的答案。

比如:你现在有一个能装6斤苹果的袋子。苹果有两种,第一种3斤1个,第二种2斤3个。
问题是:你需要怎么装才能得到最重的苹果?
如果是人思考这个问题,很简单,拿3个2斤的。
但是计算机可没有这么灵活,用贪婪法拿的话,先拿个最大的3斤,然后只能拿2斤。但最后并没有得到最多苹果。

这个例子说明了贪婪法的局部最优和不考虑整体的所带来了的问题

现在图书馆只有一台电脑,有8个人预约,但由于时间出现冲突,如何分配并回复预约人分配序列中的用户:预约成功,其他人回复:预约失败。
才能使电脑利用率最大化?

编号 开始时间 结束时间
1 6 10
2 6 11
3 6 12
4 10 15
5 15 17
6 16 17
7 12 18
8 17 18

用贪婪法分配:谁用的时间越早结束,剩余的时间就是越多的?那我就找最早使用完电脑的,找到后在剩下人中再找最早结束的,以此类推。

虽然贪心算法的思想简单,但是贪心法不保证能得到问题的最优解,如果得不到最优解,那就不是我们想要的东西了,所以我们现在要证明的是在这个问题中,用贪心法能得到最优解。

现在要证明的是:

A是所有预约的集合,令E为这个集中最早结束的预约,则E在A结合。

证明:设F为A中最早结束的,如果E=F,那么就证明了最早结束E在A中,如果E!=F,

那么我们把F从A中剔除掉,然后换上E,依然能得到一个最优预约序列(E是所有预约中最早结束的),所以贪心法在这个问题里能得到最优解。

现在回过来看什么是贪心选择性质?就是能用贪心法得到得到全局最优解的性质,至于什么时候能获得,那就得自己判断了。

代码如下:

public static void main(String[] args){
    int[] startTime = new int[]{0,6,6,6,10,15,16,12,17};
    int[] endTime = new int[]{0,10,11,12,15,17,17,18,18};
    System.out.println(sort(startTime,endTime,0,8));
}

public static String sort(int[] startTime,int[] endTime,int m,int n){
    int k = m+1;
    while(k<=n && startTime[k]<endTime[m]){
        k=k+1;
    }
    if(k<=n){
        String empt = sort(startTime,endTime,k,n);
        return k+","+empt;
    }else{
        return "";
    }
}

什么时候用贪心算法?能否得到最优解?

我们能够依据贪心法的2个重要的性质去证明:贪心选择性质最优子结构性质

1、贪心选择
  什么叫贪心选择?从字义上就是贪心也就是目光短线。贪图眼前利益。在算法中就是仅仅依据当前已有的信息就做出选择,并且以后都不会改变这次选择。(这是和动态规划法的主要差别)  

  所以对于一个详细问题。要确定它是否具有贪心选择性质,必须证明每做一步贪心选择是否终于导致问题的总体最优解。

2、最优子结构
  当一个问题的最优解包括其子问题的最优解时,称此问题具有最优子结构性质。

  这个性质和动态规划法的一样,最优子结构性质是可用动态规划算法或贪心算法求解的关键特征。

区分动态规划

动态规划算法通常以自底向上的方式解各子问题,是递归过程。

贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。

以二叉树遍历为例
   贪心法是从上到下仅仅进行深度搜索。也就是说它从根节点一口气走到黑的,它的代价取决于子问题的数目,也就是树的高度,每次在当前问题的状态上作出的选择都是1。不进行广度搜索。所以终于它得出的解不一定是最优解。非常有可能是近似最优解。

  而动态规划法在最优子结构的前提下,从树的叶子节点开始向上进行搜索,而且在每一步都依据叶子节点的当前问题的状况作出选择,从而作出最优决策。所以她的代价是子问题的个数和可选择的数目。它求出的解一定是最优解。

贪心策略的选择

贪心算法不是对所有问题都能得到整体最优解的,因此选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

分析步骤:数学建模

  1. 建立数学模型来描述最优化问题;
  2. 把求解的最优化问题转化为这样的形式:对其做出一次选择后,只剩下一个子问题需要求解
  3. 证明做出贪心选择后:
    原问题总是存在全局最优解,即贪心选择始终安全;
    剩余子问题的局部最优解与贪心选择组合,即可得到原问题的全局最优解。

经典问题分析

找零钱问题

要找K元的零钱,零钱的种类已知,保存在数组coins[]中,要求:求出构成K所需的最少硬币的数量和零钱的具体数值。
分析:

(1)贪心算法:,先从面额最大的硬币开始尝试,一直往下找,知道硬币总和为N。但是贪心算法不能保证能够找出解(例如,给,2,3,5,然后N=11,导致无解5,5,1)。

(2)动态规划:
思想:快速实现递归(将前面计算的结果保存在数据里,后面重复用的时候直接调用就行,减少重复运算)

动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。

public class Arithmetic {
    public static void main(String[] args){
        List<Money> moneyList  = new ArrayList<>();
        moneyList.add(new Money(11,3));
        moneyList.add(new Money(5,2));
        moneyList.add(new Money(7,3));
        moneyList.add(new Money(10,3));
        //需要找零钱总金额
        int k=80;
        Map<String,Object>  map = getOptimalData(k,moneyList);
        System.out.println("==="+map.get("msg"));
        for(Money item : (List<Money>)map.get("data")){
            System.out.println("金额:"+item.getCount()+"  数量:"+item.getNum());
        }

    }

    public static Map<String,Object>  getOptimalData(int k, List<Money> moneyList){
        Map<String,Object> map = new HashMap<>();
        //排序,防止金额不是按照从大到小的排序的
        Collections.sort(moneyList,new Comparator<Money>(){
            public int compare(Money arg0, Money arg1){
                return arg0.getCount()> arg1.getCount()?1:-1;
            }
        });
        //最小面额
        int minMount = moneyList.get(0).getCount();
        //最大面额
        int maxMount =  moneyList.get(moneyList.size()-1).getCount();
        //如果小于最小面额
        if (k < minMount) {
            map.put("success",false);
            map.put("msg","比最低最低面额还小");
            map.put("data",Collections.EMPTY_LIST);
            return map;
        }
        //如果总金额不够
        int sum=0;
        for(Money item :moneyList){
            sum+=item.getNum()*item.getCount();
        }
        if(sum<k){
            map.put("success",false);
            map.put("msg","总金额不够");
            map.put("data",Collections.EMPTY_LIST);
            return map;
        }
        //如果存在同面额的金币
        for(Money item :moneyList){
            if(item.getCount() ==k){
                if(k%item.getCount()==0 && k<=maxMount){
                    Money dataMoney = new Money(k,k%item.getCount());
                    map.put("msg","存在同面额的硬币");
                    map.put("data",new ArrayList<>().add(dataMoney));
                    map.put("success",true);
                    return map;
                }
            }
        }
        //获取面额组合
        List<Money> optimalData = getOptimalCount(k,new ArrayList<>(),moneyList,moneyList.size()-1);
        //分析是否相等
        int resultAcount = 0;
        for(Money item :optimalData){
            resultAcount+=item.getCount()*item.getNum();
        }
        if(resultAcount!=k){
            map.put("msg","总金额:"+k+",计算金额:"+resultAcount+",少了:"+(k-resultAcount));
            map.put("success",false);
            map.put("data",optimalData);
            return map;
        }
        map.put("msg","成功");
        map.put("success",true);
        map.put("data",optimalData);
        return map;
    }

    /**
    * 获取面额组合
     *  k  需要找回的金额
     *  optimalData 符合条件的面额组合集合
     *  optimalArray  面额总集合
     *  index         optimalArray的下标,用于递归获取指定下标的面额和数量。从大到小,所以使用金币数量才是最少的。
    */
    private static List<Money> getOptimalCount(int k,List<Money> optimalData,List<Money> optimalArray,int index){
        List<Money> _optimalData = optimalData;
        if(index<0){
            return _optimalData;
        }
        //目前总金额
        int c = 0 ;
        for(Money item:_optimalData){
            c+=item.getCount()*item.getNum();
        }
        Money moneyItem = optimalArray.get(index);
        int t = 0;
        //循环面额个数
        for(int i=1;i<=moneyItem.getNum();i++){
            //如果目前总金额+当前面额>找回总金额。说明超出其,进行下个面额分析
            if(c + moneyItem.getCount()>k) {
                index --;
                return getOptimalCount(k,_optimalData,optimalArray,index);
            }else{
                //否则添加总金额,并记录当前金额的个数
                c += moneyItem.getCount();
                t++;
            }
        }
        Money dataMoney = new Money(moneyItem.getCount(), t);
        _optimalData.add(dataMoney);
        //如果index下标的面额和个数不够,那就进入下个面额进行计算
        index --;
        return getOptimalCount(k,_optimalData,optimalArray,index);
    }

    static class Money{
        //面额
        private int count;
        //该面额个数
        private int num;
        public Money(){}
        public Money(int count,int num){
            this.count = count;
            this.num = num;
        }
        public int getCount() {
            return count;
        }

        public void setCount(int count) {
            this.count = count;
        }

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }
    }
}

运行结果

===总金额:80,计算金额:73,少了:7
金额:11  数量:3
金额:10  数量:3
金额:5  数量:2

完全背包问题

一个旅行者有一个最多能用m公斤的背包,现在有n种物品,每件的重量分别是W1,W2,…,Wn,
每件的价值分别为C1,C2,…,Cn.若的每种物品的件数足够多.
求旅行者能获得的最大总价值。

用贪心法求解背包问题的关键是如何选定贪心策略,使得按照一定的顺序选择每个物品,并尽可能的装入背包,知道背包装满。至少有三种看似合适的贪心策略。

  • 选择价值最大的物品,因为这可以尽可能快的增加背包的总价值,但是,虽然每一步选择获得了背包价值的极大增长,但背包容量却可能消耗的太快,使得装入背包的物品个数减少,从而不能保证目标函数达到最大。
  • 选择重量最轻的物品,因为这可以装入尽可能多的物品,从而增加背包的总价值。但是,虽然每一步选择使背包的容量消耗的慢了,但背包的价值却没能保证迅速的增长,从而不能保证目标函数达到最大。
  • 以上两种贪心策略或者只考虑背包价值的增长,或者只考虑背包容量的消耗,而为了求得背包问题的最优解,需要在背包价值增长和背包容量消耗二者之间寻找平衡。正确的贪心策略是选择单位重量价值最大的物品。

例如:有三个物品,其重量分别为{20,30,10},价值分别为{60,120,50},背包的容量为50,应用三种贪心策略装入背包的物品和获得的价值如下图所示:
这里写图片描述

思路

设背包容量为C,共有n个物品,物品重量存放在数组W[n]中,价值存放在数组V[n]中,问题的解存放在数组X[n]中,贪心法求解背包问题的算法如下:

算法:贪心法求解背包问题
输入:背包的容量C,物品重量W[n],物品价值V[n]
输出:数组X[n]

  1. 改变数组W和V的排列顺序,使其按单位重量价值V[i]/W[i]降序排列;
  2. 根据排序后的结果进行获取重量maxW+W[i]<=C
  3. 如果超出了C,需要拆分maxValue+=Double.valueOf(C-maxW)/W[i]*V[i];
  4. 只要拆分了,就可以跳出循环了

分析

算法的时间主要消耗在将各种物品按照单位重量的价值从大到小的排序,因此,其时间复杂性为O(nlog2n)

背包问题与0/1背包问题类似,所不同的是在选择物品i(1)装入背包时。可以选择一部分,而不一定要全部装入背包。背包问题可以用贪心法求解,而0/1背包问题却不能用贪心法求解,下图给出了一个贪心法求解0/1背包问题的示例。从上面的图可以看出,对于0/1背包问题,贪心法之所以不能得到最优解,是由于物品不允许分割,因此,无法保证最终能将背包装满,部分闲置的背包容量使背包的单位重量价值降低了。事实上,在考虑0/1背包问题时,应比较选择该物品和不选择该物品所导致的方案,然后再做出最优选择,由此导出许多相互重叠的子问题,所以,0/1背包问题合适用动态规划法求解。

代码:

public class Greedy {
    public static final int C = 15;                       // 背包容量
    public static final int n = 4;                        // 物品个数
    public static final int[] w = {7, 3, 4, 5};           // 物品重量
    public static final int[] v = {14, 12, 40, 25};       // 物品价值


    public static void main(String[] args)
    {
        System.out.println(knapSack(w,v,n,C));
    }
    public static double knapSack (int W[],int V[],int N,int C)
    {
        double VW[] = new double[W.length];
        //1.排序 按照单位重量价值V[i]/W[i]降序排列;
        for(int i=0;i<W.length;i++){
            VW[i]=Double.valueOf(V[i])/W[i];
        }
        for(int i=0;i<VW.length-1;i++){
            for(int j=i+1;j<VW.length;j++){
                if(VW[i]<VW[j]){
                    double temp;
                    temp =VW[i];
                    VW[i]=VW[j];
                    VW[j]=temp;

                    int _temp;
                    _temp = V[i];
                    V[i] = V[j];
                    V[j] = _temp;

                    _temp = W[i];
                    W[i] = W[j];
                    W[j] = _temp;

                }
            }
        }
        //2.将数组X[n]初始化为0
        double maxValue=0;
        int maxW = 0;
        for(int i=0;i<W.length;i++)
        {
            if(maxW+W[i]<=C){
                maxValue+=V[i];
                maxW+=W[i];
            }else{
                maxValue+=Double.valueOf(C-maxW)/W[i]*V[i];
                break;
            }
        }
        return maxValue;
    }
}

参考文档:

简单理解算法篇–贪心算法
五大算法思想—贪心算法
【算法】—-贪心算法(背包问题)

猜你喜欢

转载自blog.csdn.net/Allen202/article/details/82657944
今日推荐