动态规划
基本思想
动态规划是将问题分解成若干个子问题,依次求解子问题,且前一个子问题的解为后一个子问题的求解提供信息。最后一个子问题的解即为原问题的解。
动态规划中的每个子问题只求解一次,一旦子问题的解被求出,则将该解存储起来,方便之后的子问题求解。相比递归算法,动态规划中每个子问题只求解一次,具有天然的剪枝功能,大大减少了计算量与时间。
动态规划三要素
- 状态转移方程
- 最优子结构
- 边界
我们要针对问题,设计状态转移方程,寻找最优子结构和边界。这是实现动态规划的关键。
例子
国王与金矿
问题描述:
10个工人挖5座金矿,每个金矿含有的黄金量和需要的人数为:500斤/5人、400斤/5人、350斤/3人、300斤/4人、200斤/3人。且要求每个金矿要么全挖,要么不挖,问怎么分配工作能尽可能多地挖出黄金?
设置参数:
假设挖出的黄金总量为 F(n,w),n为开挖的金矿数量,w为工人数量。g[n]={500,400,350,300,200}表示第n个金矿能够挖出的黄金数量,p[n]={5,5,3,4,3}表示开挖第n个金矿需要的工人数量。
最优子结构
首先构建第一个子问题,我们的人数 w=10 能够挖5座金矿吗?这时候,挖出的黄金数量就存在两种情况:
能挖5座金矿:F(5,w)=F(4,w-p[4])+g[4]
不能挖5座金矿:F(5,w)=F(4,w)
以上两个便是第一个子问题的两个最优子结构。
状态转移方程
由第一个子问题的最优子结构,我们可以推导出其他子问题的最优子结构,即状态转移方程:
F(n,w)=F(n-1,w-p[n-1])+g[n-1]
F(n,w)=F(n-1,w)
假如工人数量能开挖n座金矿(能获得黄金数量F(n-1,w-p[n-1])+g[n-1]),也能开挖n-1座金矿(能获得黄金数量F(n,w)=F(n-1,w))。为了尽可能地获取黄金,F(n,w)要取其中的最大值:
F(n,w) = max {F(n-1,w), F(n-1,w-p[n-1])+g[n-1]}
注意,前面的n座金矿挖出的黄金量不一定比后面的n-1座金矿挖出的黄金量多。举个例子,我们有10个工人,现在有两种情况,我们能挖500斤/5人和400斤/5人的2座金矿,也能挖350斤/3人、300斤/4人、200斤/3人的3座金矿。为了尽可能多的挖出黄金,我们必然让10个人去2座金矿(共900斤)。
边界
该问题的边界为:只有一座金矿。而这座金矿挖与不挖取决于我们的人数w是否足够:
若 w>=p[0],F(1,w)=g[0]
若 w<p[0],F(1,w)=0
求解过程
首先,我们有10个工人。
如果挖1座金矿,我们就要找出能找出满足该条件的矿的组合,显然5座矿都满足,我们将挖出最多的黄金记录下来,即F(1,10)=500;
如果挖2座金矿,任意两两金矿组合均满足条件,记录最多的黄金数量,并与F(1,10)=500比较取最大值,结果为第1、2金矿的总黄金量最多,即F(2,10)=900;
如果挖3座金矿,仅有挖第3、4、5座金矿符合,共有黄金850斤,然而与F(2,10)相比少了,故F(3,10)=900;
显然,10个人不可能挖4或者5座金矿,故F(4,10)=F(5,10)=900。
求解过程中,F(n,10)只被计算过一次,并存储下来,这就是动态规划求解的过程。
java代码
在程序中,我同时用动态规划与递归算法对问题进行求解,并记录求解需要的时间。对比发现,动态规划求解问题的明显比递归算法求解高效太多了。
public class King_Gold {
//动态规划用时的确比递归算法少
public static void main(String[] args) {
int[] gold = {
400, 500, 200, 300, 350};
int[] people = {
5, 5, 3, 4, 3};
long startTime_1 = System.nanoTime();
System.out.println(getMostGold_1(5, 10, gold, people));
long endTime_1 = System.nanoTime();
System.out.println("递归算法(用时):"+(endTime_1 - startTime_1)+"ns");
long startTime_2 = System.nanoTime();
System.out.println(getMostGold_2(5, 10, gold, people));
long endTime_2 = System.nanoTime();
System.out.println("动态规划(用时):"+(endTime_2 - startTime_2)+"ns");
}
public static int getMostGold_1(int n, int worker, int[] g, int[] p) {
//递归算法
if(n==1&&worker<p[0])
return 0;
if(n==1&&worker>=p[0])
return g[0];
if(n>1&&worker<p[n - 1])
return getMostGold_1(n - 1, worker, g, p);
return Math.max(getMostGold_1(n - 1, worker, g, p), (getMostGold_1(n - 1, worker - p[n - 1], g, p) + g[n - 1]));