@adamhand
2019-04-02T09:01:19.000000Z
字数 5518
阅读 918
动态规划是通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。对于一个复杂的问题,可以将其分解成很多小问题,再合并子问题的解以得出原问题的解。通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
动态规划和分治的思想很像,主要区别和联系如下:
所以,动态规划的核心就是记住已经解决过的子问题的解从而减少重复计算。
下面看一个例子。
给定一个三角形,寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或 右下走。只需要求出这个最大和即可,不必给出具体路径。 三角形的行数大于1小于等于100,数字为 0 - 99
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
思路:首先,肯定得用二维数组来存放数字三角形;然后用D( r, j)
来表示第r
行第 j
个数字(r
,j
从1
开始算),用MaxSum(r,j)
表示从D(r,j)
到底边的各条路径中,最佳路径的数字之和。
因此,此题的最终问题就变成了求 MaxSum(1,1)
上述问题可以用递归来解决。从D(r,j)
出发,下一步只能走D(r+1,j)
或者D(r+1, j+1)
。故对于N
行的三角形,我们可以写出如下的递归式:
if ( r == N)
MaxSum(r,j) = D(r,j)
else
MaxSum( r, j) = Max{ MaxSum(r+1,j), MaxSum(r+1,j+1) } + D(r,j)
完整代码如下:
/**
* n表示二维矩阵的行数
*/
public int maxSum(int i, int j, int n, int[][] matrix){
if (i == n){
return matrix[i][j];
}
int x = maxSum(i+1, j, n, matrix);
int y = maxSum(i+1, j+1, n, matrix);
return matrix[i][j] + Math.max(x, y);
}
但是用递归会存在存在很多重复计算的问题,如下图所示:
比如第三行数字1
,当计算从第2
行的数字3
开始的MaxSum
时会计算出从1
开始的MaxSum
,当计算从第二行的数字8
开始的MaxSum
的时候又会计算一次从1
开始的MaxSum
。
如果每算出一个MaxSum(r,j)
就保存起来,下次用到其值的时候直接取用,则可免去重复计算。
代码如下:
/**
* maxSum是用来记忆的数组,元素值初始化为-1
*/
public int maxSum_1(int i, int j, int n, int[][] matrix, int[][] maxSum){
if (maxSum[i][j] != -1){
return maxSum[i][j];
}
if (i == n){
maxSum[i][j] = matrix[i][j];
}else {
int x = maxSum_1(i+1, j, n, matrix, maxSum);
int y = maxSum_1(i+1, j+1, n, matrix, maxSum);
maxSum[i][j] = matrix[i][j] + Math.max(x, y);
}
return maxSum[i][j];
}
递归总是需要使用大量堆栈上的空间,很容易造成栈溢出,现在就要考虑如何把递归转换为递推。
首先需要计算的是最后一行,因此可以把最后一行直接写出,如下图:
现在开始分析倒数第二行的每一个数,现分析数字2
,2
可以和最后一行4
相加,也可以和最后一行的5
相加,但是很显然和5相加要更大一点,结果为7
,此时就可以将7
保存起来。
然后分析数字7
,7
可以和最后一行的5
相加,也可以和最后一行的2
相加,很显然和5
相加更大,结果为12
,因此将12
保存起来。以此类推,可以得到下面这张图:
然后按同样的道理分析倒数第三行和倒数第四行,最后分析第一行,可以依次得到如下结果:
根据上面的推断,就可以写出递推型动态规划程序:
public int maxSum_2(int i, int j, int n, int[][] matrix, int[][] maxSum){
for (int m = 1; m < n; m++){
maxSum[n][m] = matrix[n][m];
}
for (int m = n - 1; m >= 1; m--){
for (int p = 1; p <= m; p++){
maxSum[m][p] = Math.max(maxSum[m+1][p], maxSum[m+1][p+1]) + matrix[m][p];
}
}
return maxSum[1][1];
}
其实完全没必要用二维maxSum
数组存储每一个MaxSum(r,j)
,只要从底层一行行向上递推,那么只要存储一行的MaxSum
值就可以。
对于空间优化后的具体递推过程如下:
程序如下:
public int maxSum_3(int i, int j, int n, int[][] matrx, int[] maxSum){
maxSum = matrx[n];
for (int m = n-1; m >= 1; --m){
for (int p = 1; p <= m; p++){
maxSum[p] = Math.max(maxSum[p], maxSum[p+1]) + matrx[m][p];
}
}
return maxSum[1];
}
1. 将原问题分解为子问题
2.确定状态
3.确定一些初始状态(边界状态)的值
以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值。
4. 确定状态转移方程
定义出什么是“状态”,以及在该“状态”下的“值”后,就要找出不同的状态之间如何迁移――即如何从一个或多个“值”已知的 “状态”,求出另一个“状态”的“值”(递推型)。状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。
数字三角形的状态转移方程:
能用动规解决的问题的特点
描述:有 N
件物品和一个容量为 C
的背包。第 i
件物品占用的空间是 w[i]
,价值是 v[i]
。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
思路:先定义子问题的状态:f[i][v]
表示在面对第i
件物品,且背包容量为v
时所能获得的最大价值 。对于每一件物品,都有两种选择:放入或不放入。对于第i
件物品:
i
件物品,则f[i][v] = f[i-1][v-w[i]]+v[i]
,表示,前i-1
次选择后所选物品放入容量为v-w[i]
的背包所获得最大价值为f[i-1][v-w[i]]
,加上当前所选的第i
个物品的价值p[i]
即为f[i][v]
。i
件物品,则有f[i][v] = f[i-1][v]
,表示当不选第i
件物品时,f[i][v]
就转化为前i-1
次选择后所选物品占容量为v
时的最大价值f[i-1][v]
。则状态转移方程为:
if (背包体积j小于物品i的体积)
f[i][v] = f[i-1][v] //背包装不下第i个物体,目前只能靠前i-1个物体装包
else
f[i][v] = max{f[i-1][v], f[i-1][v-w[i]]+p[i]}
程序如下:
public int test(){
int V = 10; //背包容量
int N = 5; //物品个数
int[] values = {0,8,10,4,5,5}; //第i个物品的价值,下标为0的位置不是物品
int[] weight = {0,6,4,2,4,3}; //第i个物品占用的空间
int[][] f = new int[N+1][V+1];
for (int i = 0; i < N+1; i++){
for (int j = 0; j < V+1; j++){
f[i][j] = 0;
}
}
for (int i = 1; i <= N; i++){
for (int j = 1; j <= V; j++){
if (weight[i] > j){
f[i][j] = f[i-1][j];
}else {
f[i][j] = Math.max(f[i-1][j], f[i-1][j-weight[i]]+values[i]);
}
}
}
return f[N][V];
}
图解如下:
填表的时候自左向右、自上向下填。比如图中红色的10
,填这个位置的时候,weight
为4
,包容量为5
,说明如果包为空的话,这个物品能够放下。那么就要考虑放和不放两种情况哪种能够使得背包中的物品的值更大。
f[i][j] = f[i-1][j];
,这里的i
就是weight
,j
就是背包容量,对应着表的纵坐标和横坐标。所以f[i-1][j]
就是表中红10
上面一个元素,即蓝10
。f[i-1][j-weight[i]]+values[i]);
,j-weight[i]
的意思是要给物品i
腾出空间,所以可能会将原来放入的物品拿出来。计算得到j-weight[i]
的值为5-4=1
,所以此时f[i-1][]j-weight[i]
对应着表中的黄0
,再加上values[i]
,即5
,得到的结果为5。比较一下10
和5
,显然是10
更大,所以选择不放,此时的价值为10
。
描述:给定K
个整数的序列{ N1, N2, ..., NK }
,其任意连续子序列可表示为{ Ni, Ni+1, ..., Nj }
,其中 1 <= i <= j <= K
。最大连续子序列是所有连续子序中元素和最大的一个, 例如给定序列{ -2, 11, -4, 13, -5, -2 }
,其最大连续子序列为{ 11, -4, 13 }
,最大和为20
。
思路:
令状态 dp[i]
表示以 A[i]
作为末尾的连续序列的最大和,那么只有两种情况:
A[i]
开始,以 A[i]
结尾。对第一种情况,最大和就是 A[i]
本身。A[p]
开始 (p<i)
,一直到 A[i]
结尾。对第二种情况,最大和是 dp[i-1]+A[i]
。由此可得状态转移方程为:
dp[i] = max{A[i], dp[i-1]+A[i]}
边界为 dp[0] = A[0]
。
程序如下:
public int test(){
int[] nums = {-2, 11, -4, 13, -5, -2};
int len = nums.length;
int[] dp = new int[len];
dp[0] = nums[0];
for (int i = 1; i < len;i ++){
dp[i] = Math.max(nums[i], dp[i-1]+nums[i]);
}
int max = dp[0];
for (int i = 1; i < len; i++){
max = max > dp[i] ? max : dp[i];
}
return max;
}
动态规划(三)最长递增子序列LIS、最大连续子序列和、最大连续子序列乘积
动态规划:最长上升子序列(LIS)
动态规划
教你彻底学会动态规划——入门篇
算法-动态规划 Dynamic Programming--从菜鸟到老鸟
动态规划——最大连续子序列和
经典算法问题 - 最大连续子数列和
动态规划经典五题
回溯是一种算法思想。它的主要思想是:通过对所做的选择构造一颗状态空间树,按照深度优先的策略,从根开始深度搜索状态空间树。当搜索到某一结点,先判断该节点是否可以构成完整解(剪枝),如果可以就继续向下搜索;否则就逐层返回回溯,尝试其他路径。