一、背景
在经典的各种算法专项训练中,动态规划是一门必修课程,它往往在学完一些查找、排序等基础算法后出现,而学会它也能标志着一个算法小白的诞生。在一些编排地很完善的书籍以及课程中,它又常常在贪心算法后出现,因为贪心算法代表着局部最优解,而动态规划代表着全局最优解。在一些贪心解决不了的场景中,动态规划便可以闪亮登场。
二、贪心:极致贪婪,局部最优
贪心算法,又称贪婪算法, 是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,得到的是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择。总而言之,在面临选择时根据自身的贪心策略,做出最大利益化选择,即为贪心算法。
下面来看一个场景:
假设在某个晴朗的下午,你突然传送至一个资源丰富的矿洞,经常挖矿的同学都知道,每种矿物的价值可谓是天差地别。假设一个体积单位的矿物价值如下:钻石5、金4、铜2。现在你拥有一个豪华版野外求生必备大背包,拥有十个体积单位。矿洞中有若干矿物,钻石块都是3体积单位的块状物,金块是2体积,铜块为2体积。且上帝赐予你一个可以均匀切割所有矿石的电锯,可以将矿物切割成一体积单位。请问你能带出矿洞的最大价值是多少?
此时,利用贪心算法,贪心策略为:价值最大的优先切割成小块塞进背包。
最终算出来价值为,5 * 10 = 50。这种情况的局部最优解还是等同于全局最优解的。
那假设没有那个万能的电锯呢?
过程会变成:
- 拿一块钻石放入背包,背包空间剩余7
- 拿一块钻石放入背包,背包空间剩余4
- 拿一块钻石放入背包,背包空间剩余1
- 一个体积单位无法放入任何矿石,最终价值为3 * 5 * 3 = 45。
这是局部最优解算出的结果,实际上的最优解呢?还是那句话,经常挖矿的同学能发现,两块钻石加两块金才是最优解,在体积不超的前提下,价值为: 2 * 5 * 3 + 2 * 4 * 2 = 46。
但如果背包体积为100呢?可能经常挖矿的同学也得迷糊。但我们可以使用全局最优解算法-动态规划来解决这个问题。
三、动态规划:步步回顾,全局最优
动态规划与分治方法类似,都是通过组合子问题的解来来求解原问题的。分治方法会做许多不必要的工作,他会反复求解那些公共子子问题。而动态规划对于每一个子子问题只求解一次,将其结果保存在一个表格里面,从而无需每次求解一个子子问题时都重新计算,避免了不必要的计算工作。
使用动态规划来解决问题,大致可以分为四步:
- 根据场景,定义状态
- 找出递推公式
- 边界赋值,赋予初始值
- 启动代码,返回结果
现在以第二部分的场景为例,演示四个步骤。
3.1、定义状态
定义一个income[i],{i=1,2,3.....,背包最大体积}的一维数组,其中i代表体积单位,income[i]表示在背包的空间被占用i个体积单位的情况下,可以获得的最大的价值。
3.2、递推公式
如图所示,每一个最优解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;
}
}
效果如下:
四、结论
动态规划是获取全局最优解的算法,它的核心思想在于将每一步的最优解存储下来,当要计算下一步最优解时,依托于已经存储的最优解数据去进行比较,最终可以使得每一步均是全局最优解。可谓是一步一回顾,步步皆最优。它可以避免贪心算法的只顾眼下的局部最优,放眼全局,从而得到全局最优解。
但它也有局限之处。比如会消耗内存资源,需要建立用于存储每一步最优解的数组,本文例子是一维数组,实际上远不止如此简单。随着维度的增加,总的计算量及存贮量急剧增大。因而,受计算机的存贮量及计算速度的限制,当今的计算机仍不能用动态规划方法来解决较大规模的问题,这就是“维数障碍”。