算法篇——贪心算法

150 阅读11分钟

贪心算法

定义

贪心算法(英语:greedy algorithm),是用计算机来模拟一个“贪心”的人做出决策的过程。这个人十分贪婪,每一步行动总是按某种指标选取最优的操作。而且他目光短浅,总是只看眼前,并不考虑以后可能造成的影响。

可想而知,并不是所有的时候贪心法都能获得最优解,所以一般使用贪心法的时候,都要确保自己能证明其正确性。

适用范围

贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。

证明方法

贪心算法有两种证明方法:反证法和归纳法。一般情况下,一道题只会用到其中的一种方法来证明。

  1. 反证法:如果交换方案中任意两个元素/相邻的两个元素后,答案不会变得更好,那么可以推定目前的解已经是最优解了。
  2. 归纳法:先算得出边界情况(例如n=1 )的最优解F1F_1,然后再证明:对于每个n ,Fn+1F_{n+1} 都可以由FnF_n 推导出结果。

贪婪法的基本步骤:

步骤1:从某个初始解出发; 步骤2:采用迭代的过程,当可以向目标前进一步时,就根据局部最优策略,得到一部分解,缩小问题规模; 步骤3:将所有解综合起来。

事例一:找零钱问题

假设你开了间小店,不能电子支付,钱柜里的货币只有 25 分、10 分、5 分和 1 分四种硬币,如果你是售货员且要找给客户 41 分钱的硬币,如何安排才能找给客人的钱既正确且硬币的个数又最少

这里需要明确的几个点: 1.货币只有 25 分、10 分、5 分和 1 分四种硬币; 2.找给客户 41 分钱的硬币; 3.硬币最少化

思考,能使用我们今天学到的贪婪算法吗?怎么做?

(回顾一下上文贪婪法的基本步骤,1,2,3)

1.找给顾客sum_money=41分钱,可选择的是25 分、10 分、5 分和 1 分四种硬币。能找25分的,不找10分的原则,初次先找给顾客25分; 2.还差顾客sum_money=41-25=16。然后从25 分、10 分、5 分和 1 分四种硬币选取局部最优的给顾客,也就是选10分的,此时sum_money=16-10=6。重复迭代过程,还需要sum_money=6-5=1,sum_money=1-1=0。至此,顾客收到零钱,交易结束; 3.此时41分,分成了1个25,1个10,1个5,1个1,共四枚硬币。

/**
 * @author SJ
 * @date 2020/10/29
 */
public class GiveChanges {
    /**
     * 假设你开了间小店,不能电子支付,钱柜里的货币只有
     * **25 分、10 分、5 分和 1 分**四种硬币,如果你是售货员且要找给客户 **41 分钱的硬币**,
     * 如何安排才能找给客人的钱既**正确**且硬币的个数又**最少**?
     * @param args
     */
    public static void main(String[] args) {
        int x=41;
        giveChanges(x);
        System.out.println("需要25的数量:"+num_25);
        System.out.println("需要10的数量:"+num_10);
        System.out.println("需要5的数量:"+num_5);
        System.out.println("需要1的数量:"+num_1);

    }
    public static int num_25=0;
    public static int num_10=0;
    public static int num_5=0;
    public static int num_1=0;
    public static void giveChanges(int money){
        while (money!=0){
            if (money>=25){
                money-=25;
                num_25++;

            } else if ( money >= 10) {

                money-=10;
                num_10++;
            }else if (money>=5){
                money-=5;
                num_5++;
            }
            else{
                money-=1;
                num_1++;
            }
        }
    }

}

测试结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" ...
需要25的数量:1
需要10的数量:1
需要5的数量:1
需要1的数量:1

Process finished with exit code 0

从这个例子大概理解了贪心,就是按照正常的思维优先找25,其次是10,5,1;

从这个例子也能理解,为什么贪心不一定能给出最优解。

也能理解为什么要排序,因为要从最好的选择开始迭代。

现在问题变了,还是需要找给顾客41分钱,现在的货币只有 25 分、20分、10 分、5 分和 1 分四种硬币;该怎么办?

按照贪心算法的三个步骤:

1.41分,局部最优化原则,先找给顾客25分; 2.此时,41-25=16分,还需要找给顾客10分,然后5分,然后1分; 3.最终,找给顾客一个25分,一个10分,一个5分,一个1分,共四枚硬币。

是不是觉得哪里不太对,如果给他2个20分,加一个1分,三枚硬币就可以了呢?^_^;

总结:贪心算法的优缺点

优点:简单,高效,省去了为了找最优解可能需要穷举操作,通常作为其它算法的辅助算法来使用;

缺点:不从总体上考虑其它可能情况,每次选取局部最优解,不再进行回溯处理,所以很少情况下得到最优解。

事例二:背包最大价值问题

有一个背包,最多能承载重量为 C=150的物品,现在有7个物品(物品不能分割成任意大小),编号为 1~7,重量分别是 wi=[35,30,60,50,40,10,25],价值分别是 pi=[10,40,30,50,35,40,30],现在从这 7 个物品中选择一个或多个装入背包,要求在物品总重量不超过 C 的前提下,所装入的物品总价值最高。

这里需要明确的几个点: 1.每个物品都有重量和价值两个属性; 2.每个物品分被选中和不被选中两个状态(后面还有个问题,待讨论); 3.可选物品列表已知,背包总的承重量一定。

思考:如何选,才使得装进背包的价值最大呢?

策略1:价值主导选择,每次都选价值最高的物品放进背包; 策略2:重量主导选择,每次都选择重量最轻的物品放进背包; 策略3:价值密度主导选择,每次选择都选价值/重量最高的物品放进背包。

(贪心法则:求解过程分成若干个步骤,但每个步骤都应用贪心原则,选取当前状态下最好的或最优的选择(局部最有利的选择),并以此希望最后堆叠出的结果也是最好或最优的解

import java.util.ArrayList;
import java.util.List;


/**
 * @author SJ
 * @date 2020/10/29
 */
public class Goods {
    public int weight;//物品重量
    public int value;//物品价值
    public boolean status;//物品是否被选中
    public static List<Goods> goodsList=new ArrayList<>();

    public Goods(int weight, int value, boolean status) {
        this.weight = weight;
        this.value = value;
        this.status = status;
    }

    @Override
    public String toString() {
        return "Goods{" +
                "weight=" + weight +
                ", value=" + value + '}';
    }

    public static void newGoods(){
       goodsList.add(new Goods(35,10,false)) ;
       goodsList.add(new Goods(30,40,false)) ;
       goodsList.add(new Goods(60,30,false)) ;
       goodsList.add(new Goods(50,50,false)) ;
       goodsList.add(new Goods(40,35,false)) ;
       goodsList.add(new Goods(10,40,false)) ;
       goodsList.add(new Goods(25,30,false)) ;

    }
}

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * @author SJ
 * @date 2020/10/29
 */
public class PickUpGoods {
    public static int C=150;//背包载重150

    //优先挑选价值大的
    public static List<Goods> pickUpByValue(){
        C=150;
        List<Goods> picked=new ArrayList<>();
        List<Goods> temp=Goods.goodsList;
        temp.sort((o1, o2) -> o1.value - o2.value);
        int index=temp.size()-1;
        //最小的重量是10
        while (C>=10&&index>=0) {

            if (C>=temp.get(index).weight){
                C-=temp.get(index).weight;
                picked.add(temp.get(index));
                temp.get(index).status=true;

            }
            index--;

        }
        return picked;

    }
    //优先挑选重量最轻的
    public static List<Goods> pickUpByWeight(){
        C=150;
        List<Goods> picked=new ArrayList<>();
        List<Goods> temp=Goods.goodsList;
        temp.sort((o1, o2) -> o1.weight - o2.weight);
        int index=0;
        while (C>=10&&index<7){
            if (C>=temp.get(index).weight){
                C-=temp.get(index).weight;
                picked.add(temp.get(index));
                temp.get(index).status=true;

            }
            index++;
        }
        return picked;
    }
    //优先挑选价值密度最大的
    public static List<Goods> pickUpByValueDividedWeight(){
        C=150;
        List<Goods> picked=new ArrayList<>();
        List<Goods> temp=Goods.goodsList;
        temp.sort(Comparator.comparingDouble(o -> (double) o.value / o.weight));
        int index=temp.size()-1;
        while (C>=10&&index>=0){
            if (C>=temp.get(index).weight){
                C-=temp.get(index).weight;
                picked.add(temp.get(index));
                temp.get(index).status=true;

            }
            index--;
        }
        return picked;
    }
    //计算总重量和总价值
    public static void print(List<Goods> goods){
        int totalValue=0;
        int totalWeight=0;
        for (Goods good : goods) {
            totalValue+=good.value;
            totalWeight+=good.weight;
        }
        System.out.println("总价值为:"+totalValue);
        System.out.println("总重量为:"+totalWeight);
    }
}
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * @author SJ
 * @date 2020/10/29
 */
public class TestPickUpGoods {
    public static void main(String[] args) {
        Goods.newGoods();
        List<Goods> goods = PickUpGoods.pickUpByValue();
        System.out.println("按价值挑选的结果");
        System.out.println(Arrays.toString(goods.toArray()));
        PickUpGoods.print(goods);
        System.out.println("按重量挑选的结果");
        List<Goods> goods1 = PickUpGoods.pickUpByWeight();
        System.out.println(Arrays.toString(goods1.toArray()));
        PickUpGoods.print(goods1);

        System.out.println("按价值密度挑选的结果");
        List<Goods> goods2 = PickUpGoods.pickUpByValueDividedWeight();
        System.out.println(Arrays.toString(goods2.toArray()));
        PickUpGoods.print(goods2);

    }
}


测试结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe"...
按价值挑选的结果
[Goods{weight=50, value=50}, Goods{weight=10, value=40}, Goods{weight=30, value=40}, Goods{weight=40, value=35}]
总价值为:165
总重量为:130
按重量挑选的结果
[Goods{weight=10, value=40}, Goods{weight=25, value=30}, Goods{weight=30, value=40}, Goods{weight=35, value=10}, Goods{weight=40, value=35}]
总价值为:155
总重量为:140
按价值密度挑选的结果
[Goods{weight=10, value=40}, Goods{weight=30, value=40}, Goods{weight=25, value=30}, Goods{weight=50, value=50}, Goods{weight=35, value=10}]
总价值为:170
总重量为:150

Process finished with exit code 0

三种的测试方案都给出了结果,此时,我们按照题意选择第三种即可。

写一个方法,用于比较这三种方法结果的好坏,输出最好的方法。

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * @author SJ
 * @date 2020/10/29
 */
public class TestPickUpGoods {
    public static void main(String[] args) {
        choose();

    }

    public static void choose() {
        Goods.newGoods();
        List<Goods> goods = PickUpGoods.pickUpByValue();
        List<Goods> goods1 = PickUpGoods.pickUpByWeight();
        List<Goods> goods2 = PickUpGoods.pickUpByValueDividedWeight();

        int print = PickUpGoods.print(goods);
        int print2 = PickUpGoods.print(goods1);
        int print3 = PickUpGoods.print(goods2);
        System.out.println("最好的方案是:");
        int max = Math.max(print, print2);
        max = Math.max(max, print3);
        if (max == print) {
            System.out.println(Arrays.toString(goods.toArray()));
            System.out.println("总价值为:" + PickUpGoods.print(goods));
        } else if (max == print2) {
            System.out.println(Arrays.toString(goods1.toArray()));
            System.out.println("总价值为:" + PickUpGoods.print(goods1));

        } else {
            System.out.println(Arrays.toString(goods2.toArray()));
            System.out.println("总价值为:" + PickUpGoods.print(goods2));

        }


    }
}


"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe"
最好的方案是:
[Goods{weight=10, value=40}, Goods{weight=30, value=40}, Goods{weight=25, value=30}, Goods{weight=50, value=50}, Goods{weight=35, value=10}]
总价值为:170

Process finished with exit code 0

常见题型

在提高组难度以下的题目中,最常见的贪心有两种。

  • 「我们将 XXX 按照某某顺序排序,然后按某种顺序(例如从小到大)选择。」。
  • 「我们每次都取 XXX 中最大/小的东西,并更新 XXX。」(有时「XXX 中最大/小的东西」可以优化,比如用优先队列维护)

二者的区别在于一种是离线的,先处理后选择;一种是在线的,边处理边选择。

排序解法

用排序法常见的情况是输入一个包含几个(一般一到两个)权值的数组,通过排序然后遍历模拟计算的方法求出最优值。

后悔解法

思路是无论当前的选项是否最优都接受,然后进行比较,如果选择之后不是最优了,则反悔,舍弃掉这个选项;否则,正式接受。如此往复。

与动态规划的区别

贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。

排序法的例题

国王游戏

恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。然后,让这 n 位大臣排成一排,国王站在队伍的最前面。排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。

国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。注意,国王的位置始终在队伍的最前面。

image-20201030101757295
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * @author SJ
 * @date 2020/10/30
 */
//http://oj.yachedu.com/problem.php?cid=1155&pid=4
public class KingGame {
    //本题需要使用大整数来实现
    public static class Minister{
        public int left;
        public int right;
        public static List<Minister> ministers=new ArrayList<>();

        public Minister(int left, int right) {
            this.left = left;
            this.right = right;
        }

        public static void newMinister(){
            ministers.add(new Minister(2,3));
            ministers.add(new Minister(7,4));
            ministers.add(new Minister(4,6));
        }

        @Override
        public String toString() {
            return "Minister{" +
                    "left=" + left +
                    ", right=" + right +
                    '}';
        }
        //打印列表测试
        public static void printList(){
            for (int i = 0; i < ministers.size(); i++) {
                System.out.println(ministers.get(i).toString());
            }
        }
    }
    public static void sort(List<Minister> ministers){
        Collections.sort(ministers, new Comparator<Minister>() {
            @Override
            public int compare(Minister o1, Minister o2) {
                return Math.max(o2.right,o1.left*o1.right)-Math.max(o1.right,o2.left*o2.right);
            }
        }
        );

    }
    public static BigInteger MaxAward(){
        BigInteger bigInteger=new BigInteger("1");
        BigInteger MaxValue=new BigInteger("1");
        //把国王排在最前面
        Minister.ministers.add(0,new Minister(1,1));
        List<Minister> temp=Minister.ministers;

        for (int i = 1; i < temp.size(); i++) {
            //把int类型转成BigInterger
            BigInteger award=bigInteger.divide(BigInteger.valueOf(temp.get(i).right));
            System.out.println("第"+i+"位拿到的奖赏为"+award);

            BigInteger bigInteger1 = BigInteger.valueOf(temp.get(i).left);
            bigInteger = bigInteger.multiply(bigInteger1);

            MaxValue=MaxValue.max(award);
        }



        return MaxValue;
    }

    public static void main(String[] args) {
        Minister.newMinister();
        System.out.println("排序前:");
        Minister.printList();
        sort(Minister.ministers);
        System.out.println("排序后:");
        Minister.printList();
        BigInteger bigInteger = MaxAward();
        System.out.println(bigInteger);
    }
}

测试结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" ...
排序前:
Minister{left=2, right=3}
Minister{left=7, right=4}
Minister{left=4, right=6}
排序后:
Minister{left=2, right=3}
Minister{left=4, right=6}
Minister{left=7, right=4}
第1位拿到的奖赏为02位拿到的奖赏为03位拿到的奖赏为2
2

Process finished with exit code 0