版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_27717921/article/details/52959455
在说动态规划的例子之前,先说明一下动态规划和分治算法的区别
虽然两者都是通过组合子问题的解来求解原问题但是分治方法将问题划分为互不相交的子问题,递归的求解子问题再将它们的解组合起来求出原问题的解。
而动态规划算法应用于子问题重叠的情况,即不同的子问题具有公共的子子问题,在这种情况下,分治算法会做许多不必要的工作,它会重复的求解这些子问题,尽管这些子问题都曾经计算过。而动态规划算法就聪明了很多,它对每个子子问题都只求解一次。将其解保存在一个数据结构中,从而遇到曾经计算过的子子问题并不是再计算而是从这个数据结构直接取结果即可。
学习动态规划算法,首先要了解最优子结构这个概念
如果一个问题的最优解包含其子问题的最优解,我们就称此问题具有最优子结构性质,一个问题如果可以应用动态规划算法,那么它必然具有最优子结构。在使用动态规划方法时,我们要利用子问题的最优解来构造原问题的最优解。
下面举一个例子,其实也是书上的例子
例子1,如何切割钢条,长度为n的钢条,可以选择切割成多段也可以选择不切割。这完全要依靠收益来看。下面这个表示长度为i的钢条所对应的价格表
长度i 1 2 3 4 5 6 7 8 9 10
价格pi 1 5 8 9 10 17 17 20 24 30
比如一个长度为4的钢条,它的切割情况就有
方案 收益
(1,3)---------- 9
(2,2)-----------10
(3,1)-----------9
(1,1,2)---------7
(1,2,1)---------7
(2,1,1)---------7
(1,1,1,1)-------4
(4,0)-----------9
这些方案中将长度为4的钢条切割成(2,2)的收益最大,于是企业就会切割了卖。。。。
首先看这个问题满足使用DP的最优子结构的性质。
我们第一次选择切割钢条的问题,剩下的钢条则是与原问题相似的子问题。
其实原问题的最优解就是由第一次切割得到的两段钢条的最优切割方案组成的,符合最优子结构。这里为了尽可能减少子问题空间的大小,切割后剩下的钢条构成一个子问题。剩下的钢条可能还会被切割,也可能不被切割,这里我们不管,我们只要求得在当前切割方案下这个子问题的最优解,与当前切割方案组合最优则能得到原问题的最优切割方案。
对于第一步的切割,你假定已经知道第一部应该如何切割得到最优解了,并且你知道第一步切割会产生怎么的子问题。
可以利用剪切粘贴的思想来验证钢条切割问题用动态规划的思想来进行,假设子问题的解不是它自身的最优解,那么我们可以从原问题中把子问题的解减掉,将子问题一个更优的解放到原问题的最优解中,那么将形成一个比原问题最优解更优的方法,这与全问题最优解是矛盾的,所以如果要想达到原问题的最优解,必须子问题步步是最优的才是可行的。
将一个长度为4的钢条切割,仅仅使用了一个子问题,(长度为4-i钢条的切割),但i的值可能取0,1,2,3,就是第一次要切割的位置在长度为4的钢条上的位置,下面则是一棵递归调用树。可以看出要求长度4的钢条最优切割方案,我们需要求解长度为3、2的切割方案,对应的则是i=1,i=2,这是剩余各条需要切割的,节点为1和0的部分不需要切割,所以可以看到它的子树没有或者是0.
画框的部分代表如果不采取措施就会重复计算的部分。朴素的自顶向下的动态规划会造成重复计算的问题,时间复杂度高,加入了备忘机制的自顶向下的动态规划可以避免这一问题,将计算出的结果放在列表中,如果还需要这部分值则直接取即可。另外自底向上的动态规划可以更好的达到备忘机制的效果,想得到4的切割方案,先计算出3的方案,想得到2的切割方案,先得到1的方案,直到0,而0是初始设置,这里长度为0的钢条自然是没有收益的。用了就是先把小粒度的计算出来在得到大粒度的思想。下面针对这三种方案分别实现
package DP;
public class Cut_Rod {
//p为存放了不同长度对应的价格,而n则是要判断长度n的钢条最好的切割
private static int[] array;
private static int[] srray;
public static int cut_Rod(int[]p,int n){
//自顶向下朴素的动态规划
if(n==0){
return 0;
}
int q = Integer.MIN_VALUE;
for(int i=1;i<=n;i++){
q=max(q,p[i]+cut_Rod(p,n-i));
}
return q;
}
public static int memoized_cut_road(int[]p,int n){
//加入了备忘录的自顶向下的动态规划
int[] array = new int[n+1];
for(int i=0;i<array.length;i++){
array[i] = Integer.MIN_VALUE;
}
return memorized_cut_road_autx(p,n,array);
}
public static int memorized_cut_road_autx(int[]p,int n,int[]array){
if(array[n]>0){
//说明已经更新完,可以直接取值
return array[n];
}
int q;
if(n==0){
q = 0;
}else{
q = Integer.MIN_VALUE;
for(int i=1;i<=n;i++){
q = max(q,p[i]+memorized_cut_road_autx(p,n-i,array));
}
}
return q;
}
public static int bottom_up_road(int[]p,int n){
//自底向上的动态规划算法,先求解出小问题,逐步向上求解
int[] array = new int[n+1];
array[0] = 0;
int q = Integer.MIN_VALUE;
for(int j=1;j<=n;j++){
q = Integer.MIN_VALUE;
for(int i=1;i<=j;i++){
q = max(q,p[i]+array[j-i]);
}
array[j] = q;
}
return array[n];
}
public static void extended_bottom_up_rod(int[]p,int n){
//不仅求出切割的最大效益,并且保存切割方案,array保存的是子问题的解避免重复计算,而sarry保存的则是每步的最优切割方案
array = new int[n+1];
srray = new int[n+1];
array[0]=0;
int q = Integer.MIN_VALUE;
for(int i=1;i<=n;i++){
q = Integer.MIN_VALUE;
for(int j=1;j<=i;j++){
if(q<p[j]+array[i-j]){
q = p[j]+array[i-j];
srray[i] = j;
}
}
array[i] = q;
}
}
public static void print_road_solution(int[]p,int n){
extended_bottom_up_rod(p,n);
/*for(int i=1;i<=n;i++){
System.out.println(srray[i]);
}
System.out.print("-------");*/
while(n>0){
System.out.print(srray[n]);
n = n-srray[n];
}
}
static int max(int n1,int n2){
return n1>n2?n1:n2;
}
public static void main(String[] args) {
int[] p={0,1,5,8,9,10,17,17,20,24,30};
/*System.out.print(cut_Rod(p,4));*/
/* System.out.print(memoized_cut_road(p,4));
System.out.print(bottom_up_road(p,4));*/
print_road_solution(p,9);
}
}
例子2,矩阵链乘法,给定一个n个矩阵的序列<A1,A2.......An>,我们计算矩阵链乘积A1*A2*....An,用加括号的方式来明确计算次序,然后利用标准的矩阵相乘算法进行计算,使得计算量最小,这里的计算量用标量乘法的次数来表示作为计算的代价。
比如A1为10*100,A2为100*5,,A3为5*50,(A1*A2)*A3的计算量为10*100*5+10*5*50=7500,A1*(A2*A3)的计算量为10*100*50+100*5*50=75000,显然按照第一种计算顺序的计算代价更小一些,这里就是在这样的背景下提出了利用三种方式(自顶向下、加入备忘机制、自下向上)来分别计算的。
package DP;
public class Matrix_Multiply {
public static int[][]m;
public static int[][]s;
public static int recursive_Matrix_Chain(int[]p,int i,int j){
if(i==j){
return 0;
}
int q = Integer.MIN_VALUE;
int[][] m = new int[j+1][j+1];
m[i][j] = Integer.MAX_VALUE;
for(int k=i;k<=j-1;k++){
q = recursive_Matrix_Chain(p,i,k)+recursive_Matrix_Chain(p,k+1,j)+p[i-1]*p[k]*p[j];
if(q<m[i][j])
m[i][j] = q;
}
return m[i][j];
}
//加入了备忘机制d
public static int memoized_martix_chain(int[] p){
int n = p.length-1;
int q = Integer.MIN_VALUE;
int[][] m = new int[n+1][n+1];
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
//可见是矩阵的上三角部分
m[i][j] = Integer.MAX_VALUE;
}
}
return lookup_Chain(m,p,1,n);
}
public static int lookup_Chain(int[][]m,int[]p,int i,int j){
int q = Integer.MIN_VALUE;
if(m[i][j]<Integer.MAX_VALUE){
//只说明一种情况就是所求的值之前已经计算过,不需要重复计算,直接取值即可
return m[i][j];
}
if(i==j){
m[i][j] = 0;
}
else{
for(int k=i;k<=j-1;k++){
q = lookup_Chain(m,p,i,k)+lookup_Chain(m,p,k+1,j)+p[i-1]*p[k]*p[j];
if(q<m[i][j]){
m[i][j] = q;
}
}
}
return m[i][j];
}
public static void matrix_chain_Order(int[] p){
int n = p.length-1;
m = new int[n+1][n+1];
s = new int[n+1][n+1];
for(int i=1;i<=n;i++){
m[i][i] = 0;
}
for(int l=2;l<=n;l++){
for(int i=1;i<=n-l+1;i++){
int j = i+l-1;
m[i][j] = Integer.MAX_VALUE;
for(int k=i;k<=j-1;k++){
int q = m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if(q<m[i][j]){
m[i][j] = q;
s[i][j] = k;
}
}
}
}
}
public static void print_matrix(int[] p,int i,int j){
matrix_chain_Order(p);
print_optimal_parens(i,j);
}
public static void print_optimal_parens(int i,int j){
if(i==j){
System.out.print("A"+i);
}else{
System.out.print("(");
print_optimal_parens(i,s[i][j]);
print_optimal_parens(s[i][j]+1,j);
System.out.print(")");
}
}
public static void main(String[] args) {
int[] p ={30,35,15,5,10,20,25};
/*System.out.print(recursive_Matrix_Chain(p,1,6));*/
/*print_matrix(p,1,6);*/
System.out.print(memoized_martix_chain(p));
}
}