算法必修课:什么是贪心算法和动态规划?

avatar
java开发 @天鹅到家

一、背景

在经典的各种算法专项训练中,动态规划是一门必修课程,它往往在学完一些查找、排序等基础算法后出现,而学会它也能标志着一个算法小白的诞生。在一些编排地很完善的书籍以及课程中,它又常常在贪心算法后出现,因为贪心算法代表着局部最优解,而动态规划代表着全局最优解。在一些贪心解决不了的场景中,动态规划便可以闪亮登场。

二、贪心:极致贪婪,局部最优

贪心算法,又称贪婪算法, 是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,得到的是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择。总而言之,在面临选择时根据自身的贪心策略,做出最大利益化选择,即为贪心算法。

下面来看一个场景:

printidea1690537219940.jpeg

假设在某个晴朗的下午,你突然传送至一个资源丰富的矿洞,经常挖矿的同学都知道,每种矿物的价值可谓是天差地别。假设一个体积单位的矿物价值如下:钻石5、金4、铜2。现在你拥有一个豪华版野外求生必备大背包,拥有十个体积单位。矿洞中有若干矿物,钻石块都是3体积单位的块状物,金块是2体积,铜块为2体积。且上帝赐予你一个可以均匀切割所有矿石的电锯,可以将矿物切割成一体积单位。请问你能带出矿洞的最大价值是多少?

此时,利用贪心算法,贪心策略为:价值最大的优先切割成小块塞进背包。

最终算出来价值为,5 * 10 = 50。这种情况的局部最优解还是等同于全局最优解的。

那假设没有那个万能的电锯呢?

过程会变成:

  1. 拿一块钻石放入背包,背包空间剩余7
  2. 拿一块钻石放入背包,背包空间剩余4
  3. 拿一块钻石放入背包,背包空间剩余1
  4. 一个体积单位无法放入任何矿石,最终价值为3 * 5 * 3 = 45。

这是局部最优解算出的结果,实际上的最优解呢?还是那句话,经常挖矿的同学能发现,两块钻石加两块金才是最优解,在体积不超的前提下,价值为: 2 * 5 * 3 + 2 * 4 * 2 = 46。

但如果背包体积为100呢?可能经常挖矿的同学也得迷糊。但我们可以使用全局最优解算法-动态规划来解决这个问题。

三、动态规划:步步回顾,全局最优

动态规划与分治方法类似,都是通过组合子问题的解来来求解原问题的。分治方法会做许多不必要的工作,他会反复求解那些公共子子问题。而动态规划对于每一个子子问题只求解一次,将其结果保存在一个表格里面,从而无需每次求解一个子子问题时都重新计算,避免了不必要的计算工作。

使用动态规划来解决问题,大致可以分为四步:

  1. 根据场景,定义状态
  2. 找出递推公式
  3. 边界赋值,赋予初始值
  4. 启动代码,返回结果

现在以第二部分的场景为例,演示四个步骤。

3.1、定义状态

定义一个income[i],{i=1,2,3.....,背包最大体积}的一维数组,其中i代表体积单位,income[i]表示在背包的空间被占用i个体积单位的情况下,可以获得的最大的价值。

3.2、递推公式

会员制目的.png

如图所示,每一个最优解income[i]都可以用四个数字比较所得,也代表着四个动作。即:什么也不做沿用i-1体积单位的最大价值、腾出一个钻石块的体积放钻石块、腾出一个金块的体积放金块、腾出一个铜块的体积放铜块,这四个数的最大值即为体积为i时的最大价值数。

递推公式是动态规划的核心,只有成功定义出公式才能解出题目,公式的核心就是如何保持每一步均为全局最优解。这需要我们对场景的充分分析和推导。

3.3、边界赋值

这一步是让3.2的公式能跑起来的关键,没有边界的初始值,推导公式就没有开始始终停留在理论上。由例子中的题意可得,income[0]=0,income[1]=0,income[2]=8,income[3]=15。剩下的就可以由推导公式得出。实际上也可以仅赋初值income[0]=0,程序中做一个判断即可。

3.4、coding,得到结果

public class Main {

    public static void main(String[] args) {
        //矿物初始化
        List<Ore> oreList = new ArrayList<>();
        oreList.add(new Ore(3,15));
        oreList.add(new Ore(2,8));
        oreList.add(new Ore(2,6));
        //输入背包最大体积
        System.out.println("请输入最大背包体积:");
        Scanner scanner = new Scanner(System.in);
        int bagSize = scanner.nextInt();

        //定义最大价值数组
        int []income = new int[bagSize+1];
        //初始值
        income[0]=0;
        for(int i=1;i<=bagSize;i++){
            //套入公式
            //存储四个数
            ArrayList<Integer> nums = new ArrayList<>();
            //啥也不做 沿用i-1值
            nums.add(income[i-1]);
            //尝试腾出空间 加入某种矿石
            for(int j=0;j<3;j++){
                if(i >= oreList.get(j).getVolume()){
                    int value = income[i-oreList.get(j).getVolume()] + oreList.get(j).getValue();
                    nums.add(value);
                }
            }
            //排序
            Collections.sort(nums);
            //赋值
            income[i] = nums.get(nums.size()-1);
        }
        //输出结果
        System.out.println("背包体积为"+bagSize+"时,最大价值为"+income[bagSize]);
    }

}

//矿物
class Ore{
    //体积
    private int volume;
    //价值
    private int value;

    public Ore(int volume, int value) {
        this.volume = volume;
        this.value = value;
    }

    public int getVolume() {
        return volume;
    }

    public void setVolume(int volume) {
        this.volume = volume;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

效果如下:

image.png

四、结论

动态规划是获取全局最优解的算法,它的核心思想在于将每一步的最优解存储下来,当要计算下一步最优解时,依托于已经存储的最优解数据去进行比较,最终可以使得每一步均是全局最优解。可谓是一步一回顾,步步皆最优。它可以避免贪心算法的只顾眼下的局部最优,放眼全局,从而得到全局最优解。

但它也有局限之处。比如会消耗内存资源,需要建立用于存储每一步最优解的数组,本文例子是一维数组,实际上远不止如此简单。随着维度的增加,总的计算量及存贮量急剧增大。因而,受计算机的存贮量及计算速度的限制,当今的计算机仍不能用动态规划方法来解决较大规模的问题,这就是“维数障碍”。