Dynamic Programming(动态规划)
一、定义
动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。
二、方法论
动态规划的核心是状态及状态转移方程
y = d p ( x 1 , x 2 , x 3 , . . . x n ) y = dp(x1,x2,x3,...x_n) y=dp(x1,x2,x3,...xn)
d p ( x 1 , x 2 , x 3 , . . . , x n ) = k d p ( x 1 1 , x 1 2 , x 1 3 , . . x 1 n ) . . . dp(x_1,x_2,x_3,...,x_n) = kdp(x_11,x_12,x_13,..x_1n)... dp(x1,x2,x3,...,xn)=kdp(x11,x12,x13,..x1n)...
确定状态转移方程需要明确
(1)自变量
确定决定每种状态的变量有几个
例如坐标中的x,y等
(2)因变量的意义
即 dp(x1,x2,x3,…x_n)的含义
(3)状态如何变化
即当前状态与其他状态之间的关系
具体如何实施和把握,我们通过下面的题目来探索
三、题解
在这里借用leetcode上关于动态规划的几个题,谈一些自己的见解。在这里和大家一起share。
按照我自己的理解,我循序渐进得通过几个例子谈一下动态规划
Problem One
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
这是一个简单且经典的动态规划(小学生都会做)问题。
根据上述方法论,
(1)**(自变量)**我们可以假定(i,j)代表第i行,第j列
(2)**(因变量)**dp[i][j]表示Start到(i,j)的路径数量
注意:(1)(2)的合理选取至关重要,可能影响整个算法的复杂度以及状态转移确定的难度
(3)**(状态转移)**dp[i][j]如何由前序状态决定?
分析具体问题发现
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − 1 ] dp[i][j] = dp[i - 1][j] + dp[i][j - 1] dp[i][j]=dp[i−1][j]+dp[i][j−1]
在此之前,我们还必须明确
初始化状态:
dp[0][0] = 1 dp[0][i] = 1 dp[i][0] = 1
变量细节控制
i - 1 > 0
j - 1 > 0
完成上述步骤,我们可以轻而易举写出这道题的代码
public int uniquePaths(int m, int n) {
//state equation
int[][] dp = new int[m][n];
//init
dp[0][0] = 1;
for (int i = 1; i < n; i++) {
dp[0][i] = dp[0][i - 1];
}
for (int j = 1; j < m; j++) {
dp[j][0] = dp[j - 1][0];
}
//transfer
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
Problem Two
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
这是Problem One的一个小变形,大家可以自行练习
只需要简单修改状态转移即可。
即对存在(i,j)位置存在障碍物时,修改dp[i][j] = 0
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
//state equation
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
//init
if(obstacleGrid[0][0] == 1) {
//障碍物
return 0;
} else {
dp[0][0] = 1;
}
for (int i = 1; i < n; i++) {
if (obstacleGrid[0][i] == 1) {
//障碍物
dp[0][i] = 0;
} else {
dp[0][i] = dp[0][i - 1];
}
}
for (int j = 1; j < m; j++) {
if (obstacleGrid[j][0] == 0) {
//障碍物
dp[j][0] = dp[j - 1][0];
} else {
dp[j][0] = 0;
}
}
//transfer
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 1) {
//障碍物
dp[i][j] = 0;
} else {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
}
前面的题目当然都只是小试牛刀,现在加点难度
Problem Three
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
我们依然严格执行我们的方法论
(1)自变量是什么?
不妨令(i,j)为从nums的第i个位置到第j个位置的子序列和
(2)因变量是什么?
子序列和
(3)状态怎么转移?
d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + n u m s [ j ] dp[i][j] = dp[i][j - 1] + nums[j] dp[i][j]=dp[i][j−1]+nums[j]
初始化状态
dp[0][0] = nums[0];
dp[i][i] = nums[i]
看到这,so easy!!!!!
但是不妨反思一下,这种做法的复杂度是**O(n^2)**吧
这个方法虽然simple但会不会复杂度太高了呢??
显然如果你在leetcode上使用这个复杂度的代码提交,TLE就会来到你身边
因此,我们需要注意:动态规划最大限度降低复杂的有效方法是
减少自变量数目
思考过后…
发现,其实自变量一个就够了
如果利用逐步求解,以连续数组结束位置i为每一步的解,dp[i]记录了以此位置作为子序列结束位置的最大和。
此时,我们需要的子序列一定是dp中最大的一个。
事情就变得简单了。
d p [ i ] = m a x { d p [ i − 1 ] + n u m s [ i ] , n u m s [ i ] } dp[i] = max\{dp[i - 1] + nums[i], nums[i]\} dp[i]=max{
dp[i−1]+nums[i],nums[i]}
因此,
public int maxSubArray(int[] nums) {
//state euqation
int[] dp = new int[nums.length];
//init
dp[0] = nums[0];
int maxNum = dp[0];
//transfer
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(nums[i], dp[i - 1] + nums[i]);
if (dp[i] > maxNum) {
maxNum = dp[i];
}
}
return maxNum;
}
未完待续…稍后更新新的题目