巧用贪心算法

2,287 阅读4分钟

「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」。

五大常用算法(分治、动态规划、贪心、回溯、分支界限(深广优先遍历)),我们之前的文章基本上都有涵盖,唯独差一个贪心算法,本篇文章我们将一起走进贪心算法的妙用。

基本概念

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

贪心算法不是对所有问题都能得到整体最优解,但许多问题他能产生整体最优解或者是整体最优解的近似解。

题目特点

我们如何能够确定一道题是否可以用贪心算法求解呢?

因为贪心算法求得的结果不一定是整体最优解,所以我们很难断定它就一定可以用贪心算法。

可以用贪心算法的题目一般具有以下特性

  • 贪心选择性质

    贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。

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

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

  • 最优子结构性质

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

    问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。

贪心算法和动态规划如何选择?

以背包问题为例,给定我们一个有容量限制的背包,和若干不同价值,不同重量的物品,如何选择可以使我们背包内物品价值最大?

这种问题在游戏场景里极为常见,比如自动拾取装备功能,肯定为玩家选择价值最大的装备拾取。

这类问题一般还可以细分为两种类型:

  • 可以拾取物品的一部分 (可以使用贪心,优先拾取性价比高的装备)
  • 物品不可分割,只能拾取,或者不拾取(不可以使用贪心,这类问题也叫0-1背包问题)

image.png

求解思路

一般可以使用贪心算法的题,也可以使用动态规划去做,但是考虑到动态规划的特点是求整体的最优解,在大部分题中贪心要比动态规划时空复杂度更低,也更容易实现。

无论哪些题,贪心算法最重要的是寻找子问题最具性价比的解,也就是局部最优解。

求解过程:

  • 把求解的问题分成若干个子问题。
  • 对每一子问题求解,得到子问题的局部最优解。
  • 把子问题的解局部最优解,合成原来解问题的一个解。

举例:上图背包问题,就可以通过贪心算法来解

步骤:

  • 子问题:装入某件物品
  • 子问题局部最优解:装入性价比最高的物品
  • 合并(所有装入物品价值和)
package com.zhj.interview;
​
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
​
public class Test10 {
    public static void main(String[] args) {
        int capacity = 15;
        int[] weights = {1,1,2,4,12};
        int[] values = {1,2,2,10,4};
        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 int weight;
        private int value;
        //物品的性价比
        private 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;
        }
    }
}