【算法】贪心算法

1 问题引入:

有一堆钞票,你可以拿走十张钞票,如果你想达到最带的金额,你要怎么拿?

每次拿最大面值的钞票,十次之后,你拿走的就是最大数额的钱。

这里"有便宜就占"用的就是贪心算法的思想,即每一次拿最大的就是局部优先,最后拿走的最大数额的钱就是全局最优

2 概述:

贪心算法(greedy algorithm,又称贪婪算法)是指在对问题求解时,总是做出在当前看来是最好的选择。也就说不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解

简单地说:贪心算法的本质就是从局部最优到全局最优

贪心算法的基本思想是:将问题的求解过程看作是一系列选择,每次选择一个输入,每次选择都是当前状态下的最好选择(局部最优解),每做一个选择后,所求问题会简化为一个规模更小的问题。从而通过每一步的最优解达到整体的最优解。

贪心算法的适用问题:具有贪心选择1和最优子结构性质2的最优化问题。

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

贪心算法也有很多的缺点:

  1. 不能保证最后解最优解3

  2. 不能用来求最大值和最小值的问题。4

  3. 智能求满足某些约束条件的可行解的范围。

贪心算法的解题步骤:

  1. 建立数学模型来描述问题

  2. 将问题分解为若干个子问题

  3. 找出适合的贪心策略

  4. 求解每一个子问题的最优解

  5. 将局部最优解 堆叠成 全局最优解

3 案例:

贪心算法有很多经典的应用,比如霍夫曼(Huffman)编码、普利姆(Prim)算法、克鲁斯卡尔(Kruskal)算法最小生成树算法还有迪杰斯特拉(Dijkstra)单源最短路径算法等。

选择排序

十大经典算法中的选择排序采用的就是贪心算法。选择排序是一种简单的排序算法,时间复杂度是$ O(n²) $。

选择排序的基本思想:是每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。

选择排序的算法步骤是:

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

  3. 重复第二步,直到所有元素均排序完毕。

例如给定一组数据,[23, 4, 6, 8, 10] 其排序过程如以下所示:

第一次:[23, 4, 6, 8, 10]

第二次:[4, 6, 23, 8, 10]

第三次:[4, 6, 8, 23, 10]

第四次:[4, 6, 8, 10, 23]

程序结束,得出[4, 6, 8, 10, 23]

代码实现:

class SelectionSort {
    public static int[] sort(int[] sourceArray) throws Exception {
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
        // 总共要经过 N-1 轮比较
        for (int i = 0; i < arr.length - 1; i++) {
            int max = i;
            // 每轮需要比较的次数 N-i
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] > arr[max]) {
                    // 记录目前能找到的最大值元素的下标
                    max = j;
                }
            }
            // 将找到的最大值和i位置所在的值进行交换
            if (i != max) {
                int tmp = arr[i];
                arr[i] = arr[max];
                arr[max] = tmp;
            }
        }
        return arr;
    }

    public static void main(String[] args) throws Exception {
        int[] arr = new int[]{23, 4, 6, 8, 10};
        System.out.println("原数组:" + Arrays.toString(arr));
        System.out.println("选择排序:" + Arrays.toString(sort(arr)));
    }
}

部分背包问题

有一个背包,容量是c,有若干个物品,价值各不相同,重量也不同。请你选择一部分物品装入背包,要保证不超过背包容量的前提下,背包中的物品的总价值最大。

注:在选择物品时候可以选择物品的一部分:比如值选择0.2 份物品1,0.5份物品2

假设背包的容量是10,有5个商品可以提供选择:

选择策略:每一个物品都有两个属性,不可以只靠价值5和重量6的单一属性来选择物品。我们应该先算出每一件物品的性价比,即单位重量下的价值,然后选择性价比最高的物品放到背包。

计算一下每一个物品的性价比,其结果为:

此时,性价比最高的的物品4,性价比是3,我们就把物品4放到背包中,此时背包容量就剩8。

之后我们从剩余的物品中选择性价比最高的物品1放入背包中,这时候背包的容量还剩下4。

剩余的物品中性价比最高的是物品5,但是物品5的重量是5,但是背包的容量是4,题目的条件允许选择物品的一部分,那就能选择0.8份的物品5,刚好占满整个背包。

此时的背包的总价值是: 9 + 6 + 5 ∗ 0.9 = 19 9 + 6 + 5 * 0.9 = 19 9+6+50.9=19为背包的最大总价值。

代码实现:

  public static void main(String[] args) {
        int capacity = 10;
        int[] weights = {4, 6, 3, 2, 5};
        int[] values = {9, 3, 1, 6, 5};
        for (int i = 0; i < weights.length; i++) {
            System.out.println(String.format("物品%d,价值:%d,重量:%d,性价比:%.2f",
                    (i + 1), values[i], weights[i], (double) values[i] / weights[i]));
        }
        System.out.println("背包最大价值:" + getHighestValue(capacity, weights, values));
    }

    public static double getHighestValue(int capacity, int[] weights, int[] values) {
        //创建物品列表并按照性价比倒序
        List<Item> itemList = new ArrayList<>();
        for (int i = 0; i < weights.length; i++) {
            itemList.add(new Item(weights[i], values[i]));
        }
        itemList = itemList.stream().sorted(Comparator.comparing(Item::getRatio).reversed()).collect(Collectors.toList());

        //背包剩余容量
        int restCapacity = capacity;
        //当前背包物品的最大价值
        double highestValue = 0;

        //按照性价比从高到低选择物品
        for (Item item : itemList) {
            if (item.weight <= restCapacity) {
                highestValue += item.value;
                restCapacity -= item.weight;
            } else {
                //背包装不下完整物品时,选择该件物品的一部分
                highestValue += (double) restCapacity / (double) item.weight * item.value;
                break;
            }
        }

        return highestValue;
    }

    static class Item {
        private final int weight;
        private final int value;
        //物品的性价比
        private final double ratio;

        public Item(int weight, int value) {
            this.weight = weight;
            this.value = value;
            this.ratio = (double) value / (double) weight;
        }

        public double getRatio() {
            return ratio;
        }
    }

这时候,有一个贪心算法的缺点:贪心算法具有一定的局限性。因为有些时候局部的最优解不一定是全局的最优解。

**01背包问题:**如果有一个容量为10的背包,有3个物品可以选择,但是我们只允许选择整个物品,不能选择一个物品的一部分。

按照贪心算法的思想,我们首先选择的是性价比最高的物品1,那我们背包的剩余容量就剩下4,再也装不了其他的物品了,而此时的总价值是6:

但这样的选择,并不一定是最优解,我们可以发现,我们不用贪心算法的思想,不选择物品1,而是选择物品2和物品3,他们的重量正好是10,总价值是7。

此时,7 > 6 ,所以依靠贪心算法算出的结果,未必是全局最优解。但是也是比较接近最优解。

对于01背包问题,不适合用贪心算法,而是应该用动态规划算法求解。

4 贪心和动态规划:

动态规划算法和贪心算法的思想都是通过分治的思想,求一个问题的最优解。主要过程是:将一个大问题分解成许多的小问题,分别求每个子问题的局部最优解,分而治之,最终合成大问题的最优解。

动态规划的思想是“反复回头”,也就是,我们分解小问题存在联系性,在一个子问题上得到的最优解,也许在下一个问题上被证伪,那么算法就进行回溯处理。在这种思想上,获取的将是“全局的绝对最优解”。

贪心算法的思想就是“有便宜就占,绝不回头”,也就是在每个子问题上取最优解,直到所有子问题被处理完,再合成大问题的最优解,过程中不会回头修改任何计算过的子问题的解。在这种思想上,获得将是“局部的相对最优解”。

简单地说,有两个人甲和乙都有一个正向的大目标,两个人都将大的目标分成一个一个的小目标,然后去完成小目标。对于甲来说,在完成小目标之后,会去反复回头检查,如果出错就去调整自己的目标。但是对于乙来说,绝不会回头,一直完成定好的小目标就行。到最后,甲会完成大目标,而乙可能不会完成大目标,但是离大目标也比较接近。

5 总结:

贪心算法的总体思想就是:局部优先从而达到全局优先。

贪心算法是局部的相对优先,如果想要达到全局的绝对优先,那还是得选择动态规划。

我们主要介绍了贪心算法的典型应用:背包问题。

贪心算法题目类型多样,很多情况下要具体情况具体分析。


  1. 整体的最优解可以通过一系列局部最优解达到,即贪心选择到达。 ↩︎

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

  3. 贪心算法不从总体上考虑,而是每次选取局部最优解,不进行回溯处理,所以在某些情况下不能得到最优解。即使贪心算法不能得到整体最优解,其最终结果很接近最优解。 ↩︎

  4. 贪心算法从局部的相对最优解的算法。而最大值和最小值是全局的问题。 ↩︎

  5. 比如有一个物品是汽车,价值是最高的,但是不能放进背包里。 ↩︎

  6. 比如有很多的水和钻石,如果只按重量来选择物品,一背包的水也抵不上一颗钻石值钱。 ↩︎

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值