动态规划的思想 是将一个问题分解为若干子问题,并且子问题之间还有重叠,通过先求解这些子问题的最优解,将原问题的最优解通过这些子问题的最优解构造出来,从而得到原问题的最优解。
由此可以得到动态规划算法的应用场景:若求一个问题的最优解(通常是求最大值或者最小值),而且该问题能够分解成若干个子问题,并且子问题之间还有重叠的更小的子问题,就可以考虑用动态规划来解决这个问题。
由此总结一下判断一个问题是否能用动态规划来解决的基本步骤:
1.看这个问题是否是求最优解(最大值或者最小值)
2.看该问题能否分解为若干个子问题,且整体问题的最优解是可以由各个子问题的最优解得出的。(最优子结构)
3.看分解的各个子问题之间是否有重叠(这不是应用动态规划的必要条件,但如果子问题没有重叠,那么使用动态规划算法与其他算法相比就没有什么优势,完全可以直接用分治思想解决,不必要用动态规划)
如果一个问题满足以上三个特征,那么就可以用动态规划的思想解决,使用动态规划的步骤为:
1.划分状态,即划分子问题。把问题划分为若干个小问题,划分时要注意,较大的问题的最优解一定可以由较小的问题的最优解得到。最后的求解思路就是先得到小问题的最优解,然后由小问题的最优解逐步构造大问题的最优解,最后得到想解决问题的最优解。
2.状态表示,即如何让计算机理解子问题。确定怎么描述各个不同大小的问题的状态,将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来,作为得到最优解的必要信息。
3.找出状态转移方程,即父问题是如何由子问题推导出来的,也即是一个较大的问题的最优解到底怎么由较小问题的最优解得到,换句话说,就是找到较大问题的最优解和较小问题的最优解之间的具体联系。
4.寻找边界条件,确定初始状态是什么?最小的子问题?最终状态又是什么?
其中的难点主要在于:怎样划分子问题,怎样得到较大子问题和较小子问题的联系(怎么从前一个状态的最优解推得后一个状态的最优解)。
算法实现的步骤:
1、创建一个一维数组或者二维数组,保存每一个子问题的结果,具体创建一维数组还是二维数组看题目而定,基本上如果题目中给出的是一个一维数组进行操作,就可以只创建一个一维数组,如果题目中给出了两个一维数组进行操作或者两种不同类型的变量值,比如背包问题中的不同物体的体积与总体积,找零钱问题中的不同面值零钱与总钱数,这样就需要创建一个二维数组。
2、设置数组边界值,一维数组就是设置第一个数字,二维数组就是设置第一行跟第一列的值。
3、找出状态转换方程,也就是说找到每个状态和其上一个状态的关系,根据状态转化方程写出代码。
4、返回需要的值,一般是数组的最后一个元素或者二维数组的最右下角的元素。
可以用动态规划解决的问题示例有:斐波那契数列、数组最大不连续递增子序列、数组最大连续子序列和、两个字符串最大公共子序列、背包问题以及找零钱问题等等。
动态规划和分治算法的主要区别就是,动态规划算法通过先计算并保存小问题的最优解,然后通过小问题的最优解逐步得到最终需要求解问题的最优解,避免了重复计算,本质就是通过付出额外的空间来节约计算时间。
下面用01背包问题说明动态规划算法:
问题描述:
一个旅行者有一个最多能装C公斤的背包,现在有n件物品,每件的重量分别是W1、W2、……、Wn,每件物品的价值分别为V1、V2、……、Vn, 需要将物品放入背包中,要怎么样放才能保证背包中物品的总价值最大?
思路分析:
首先,这个问题是一个求极值问题,求最大值价值,也即是求最大值。那么之后可以看这个问题是否具有最优子结构。本来是要求n件物品装入容量为C的背包的最大价值,如果我们知道了n-1件物品装入容量为C的背包的最大价值,能不能推得n件物品装入容量为C的背包的最大价值,感觉好像是可以的(具体状态转移方程等会再详细分析)。接下来看各个子问题是否有重合,我觉得其实这一步最好是在推出状态转移方程之后再进行。如果要用动态规划,找不出状态转移方程,根本就没法得到最终需要求的解,所以我觉得分析状态转移方程是重点。
接下来就试着分析状态转移方程:
首先明确我们要求的问题:n件物品装入容量为C的背包的最大价值。那么可以怎么划分子问题,一种方式是考虑m(m<n)件物品装入容量为C的背包的最大价值,将其作为子问题,我们如果能得到这些子问题的最优解,最终解就可以通过这些子问题的最优解得到。那么该怎样描述各个状态呢?其实分析一下这个问题就可以看出,决定物品装入背包的最大价值的关键因素就是:背包的总容量到底是多大以及这些物品的重量和价值分别是多少,因此我们可以提炼出两个量表示这类问题,一个当然就是背包容量,另一个就是待装入物品的数量(有了待装入物品的数量,自然可以得到它们分别的价值和重量)。因此我们用一个二元组<i,j>(前i件物品装入容量为j的背包的最大价值)来描述各个问题的状态,找出对应的最优解,用数组result[i][j]表示。现在我们就完成了划分子问题和状态表示两步,之后就是推出状态转移方程。
假设我们已经求出前i-1件物品装入容量j的背包的价值总和最大值为result[i-1][j],固定容量j的值不变,则对第i件物品的装法讨论如下:
首先第i件物品的重量w[i]必须小于等于容量j才行,即
1、若w[i]>j,则第i件物品肯定不能装入容量为j的背包,此时result[i][j]=result[i-1][j]
2、若w[i]<=j,则首先明确的是这件物品是可以装入容量为j的背包的,那么如果我们将该物品装入,则有 result[i][j]=result[i-1][j-c[i]]+v[i] ,需要注意的是,这里求将第i件物品装入容量为j的背包的最大价值时,必须根据将前i-1个物品装入容量为j-c[i]的背包的最大价值来得到,这点很重要。
随之而来的问题是我们要判断第i件物品装到容量为j的背包后,背包内的总价值是否是最大?那么就要对比装入第i件物品和不装入第i件物品哪种方式得到的总价值更大,即如果装了第i件物品后的总价值result[i-1][j-c[i]]+v[i]大于没装之前的总价值最大值result[i-1][j],则应该装入第i件物品;反之则说明第i件物品不必装入容量为j的背包。
故,状态转移方程如下:
result[i][j] = max(result[i-1][j-c[i]]+v[i],result[i-1][j])
注意:这里的前i件物品是给定次序的。
一种实现方式如下:
public static int MySolver(int capacity,int[] weight,int[] value){
int allthingsnum = weight.length;
//状态表示,用一个二维数组result[i][c]表示前i件物品能装入容量为c的背包中能得到的物品价值总和的最大值,这样就表示了子问题的最优解
int[][] result = new int[allthingsnum+1][capacity+1];
//初始状态是什么?
for (int i = 0;i <= allthingsnum;i++){
result[i][0] = 0;
}
for (int j = 0;j <= capacity;j++){
result[0][j] = 0;
}
for (int j = 1;j <= capacity;j++){//背包容量应该怎样取值
for (int i = 1;i <= allthingsnum;i++){
if (weight[i-1] > j){//如果第i个物品数量大于此时的背包容量,那么这个物品放不进背包,这里需要注意weight数组和result数组索引的含义不同,需要减一
result[i][j] = result[i-1][j];
}else {//如果第i件物品可以放入背包
result[i][j] = Math.max(result[i-1][j-weight[i-1]]+value[i-1],result[i-1][j]);
}
}
}
int myresult = result[allthingsnum][capacity];
//下面的程序是为了输出选出的商品编号
int j = capacity;
String numstr = "";
for (int i = allthingsnum;i > 0;i--){
//如果result[i][j]>result[i-1][j],这说明第i件物品是放入背包的
if (result[i][j] > result[i-1][j]){
numstr = numstr + i + " ";
j -= weight[i-1];
}
if (j==0)
break;
}
System.out.println("选中的商品编号为:"+numstr);
return myresult;
}