简介
动态规划主要用来解决重叠子问题的空间浪费问题和求解最优化问题。动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题【百度百科】。
重叠子问题
重叠子问题,即对一个问题的重复求解,在使用递归法求解问题时常会产生重叠子问题,如用递归法求解斐波那契数列。
使递归法求解斐波那契数列:
public class T{
public static void main(String[] args) {
for(int i = 1;i<=10 ;i++){
System.out.println(getFeib(i));
}
}
public static int getFeib(int n){
if(n == 1){
return 1;
}else if(n == 2){
return 1;
}else{
return getFeib(n-1) + getFeib(n-2);
}
}
}
上述使用递归法求解斐波那契数列,在getFeib(n-1) + getFeib(n-2)时,当n=10时,将1-9都算了一遍,而n=9时,还将1-8算了一遍,那么就产生了大量的性能浪费。
因此,设法将之前计算过的值保存下来,在下次计算的时候直接使用。
public class T{
public static void main(String[] args) {
int n = 10;
int arr[] = createFeib(n);
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
//创建一个长度为n的斐波那契数列
public static int [] createFeib(int n){
int arr[] = new int[n];
if(n == 1){
arr[0] = 1;
}else if(n == 2){
arr[0] = 1;
arr[1] = 1;
}else{
arr[0] = 1;
arr[1] = 1;
for(int i = 2;i<n;i++){
arr[i] = arr[i-1] + arr[i-2];
}
}
return arr;
}
}
最优子问题
使用动态规划求解最优子问题,事实上就是利用递归的思想得出的最优子问题,如下例所示:
小明准备在周末找一份临时工,他在接了几个单子,但是每个单子都有时间要求,如下图所示:
如,第1个单子能得5元,第2个单子能得1元,,,在接了其中一个单子的时候,不能同时接其他单子,问:小明如何接单能够获得最多的钱。
解题思路
首先将条件分布在两个数组中,pre数组的第i位用来存放选择了i号单前面还能选的单号,例如选择了4号单,那么只能选1,则pre(4) = 1;money数组用来存放每个单号所能获得的钱,如money[8]=4。(值得注意的是,在写代码的时候,要将下标-1)。
设在选择接i号单时经过推测后的最优选择所得的钱为OPT(i),OPT(i)可表示为:
OPT(i) = MAX(money[i] + OPT(pre[i]) , OPT(i-1) )
如,在选择了8号单,则产生了两种情况,一种为:4 + OPT(5) 另一种为:OPT(7)
经过推导,得出如下树:
代码实现
public class T{
public static void main(String[] args) {
int money[] = {5,1,8,4,6,3,2,4};
int pre[] = {0,0,0,1,0,2,3,5};
System.out.println(getOPT(8,money,pre));
}
public static int getOPT(int n,int money[],int pre[]){
int opt[] = new int[n];
if(n == 1){
opt[0] = money[0];
return opt[0];
}else{
opt[0] = money[0];
for (int i = 1; i < n; i++) {
int a = pre[i] == 0?0:opt[pre[i]] + money[i];
opt[i] = a > opt[i-1]?a:opt[i-1];
}
return opt[n-1];
}
}
}
示例
有如下图所示的一组数据,选择了其中一个元素就不能选择其直接相邻的元素,问:怎样选择使得相加之和最大?
解题思路
设数组名为w,则w数组可表示为w = {1,2,4,1,7,8,3},从0计算到i的最优解为OPT(i)。
选i号元素的最优解为OPT(i-2)+w[i],不选i号元素的最优解为OPT(i-1)。
从而可画出如下求解最优解的树状图:
代码实现
public class T{
public static void main(String[] args) {
int w[] = {1,2,4,1,7,8,3};
System.out.println(getOPT(6, w));
}
public static int getOPT(int n,int [] w){
int opt[] = new int[n+1];
if(n == 0){
opt[0] = w[0];
return opt[0];
}else{
opt[0] = w[0];
opt[1] = w[1]>opt[0]?w[1]:opt[0];
for(int i=2;i<=n;i++){
int a = w[i] + opt[i-2];
opt[i] = a>opt[i-1]?a:opt[i-1];
}
return opt[n];
}
}
}
总结
一方面,动态规范利用一个数组来保存计算所得的最优解,当再次需使用该最优解的时候只需要从数组中读取即可,减少了性能消耗。另一方面,分成选用该值和不选用该值两种情况来获取最优解,若选用该值,则生成解1,即该值与选用该值前所能选择的最优解之和,若不选用该值,则生成解2,即为不选用该值所能选择的最优解,之后选取解1和解2中优者。