动态规划
1)、定义数组元素的含义
2)、找出数组元素间的关系式
3)、找出初始条件
问题描述:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
(可以从最后的阶段来假设,比如有10层台阶,青蛙要怎么跳,可以直接从第9层直接到达,也可以从第8层跳到达所以有f(10)=f(9)+f(8))
def f(n):
dp=[0 for i in range(n+1)]
dp[0]=0
dp[1]=1
dp[2]=2
for i in range(3,(n+1)):
dp[i]=dp[i-1]+dp[i-2]
return dp[n]
时间复杂度为2的n次方。
使用动态规划时:
n为1和2时是显而易见的,当n=3时,可通过1和2的结果来得出,当n=4时,可通过2和3的结果来得出,我们只需要保存最近的两个结果就可以了。所以,还可以进一步优化。
def dp(n):
if(n<=2):
return n
a=1
b=2
temp=0
for i in range(3,(n+1)):
temp=a+b
a=b
b=temp
return temp
案例二:二维数组的 DP
问题描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
步骤一、定义数组元素的含义
由于我们的目的是从左上角到右下角一共有多少种路径,那我们就定义 dp[i] [j]的含义为:当机器人从左上角走到(i, j) 这个位置时,一共有 dp[i] [j] 种路径。那么,dp[m-1] [n-1] 就是我们要的答案了。
注意,这个网格相当于一个二维数组,数组是从下标为 0 开始算起的,所以 右下角的位置是 (m-1, n - 1),所以 dp[m-1] [n-1] 就是我们要找的答案。
步骤二:找出关系数组元素间的关系式
想象以下,机器人要怎么样才能到达 (i, j) 这个位置?由于机器人可以向下走或者向右走,所以有两种方式到达
一种是从 (i-1, j) 这个位置走一步到达
一种是从(i, j - 1) 这个位置走一步到达
因为是计算所有可能的步骤,所以是把所有可能走的路径都加起来,所以关系式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]。
步骤三、找出初始值
显然,当 dp[i] [j] 中,如果 i 或者 j 有一个为 0,那么还能使用关系式吗?答是不能的,因为这个时候把 i - 1 或者 j - 1,就变成负数了,数组就会出问题了,所以我们的初始值是计算出所有的 dp[0] [0….n-1] 和所有的 dp[0….m-1] [0]。这个还是非常容易计算的,相当于计算机图中的最上面一行和左边一列。因此初始值如下:
dp[0] [0….n-1] = 1; // 相当于最上面一行,机器人只能一直往左走
dp[0…m-1] [0] = 1; // 相当于最左面一列,机器人只能一直往下走
def uniquePaths(m,n):
if (m <= 0 | n <= 0):
return 0
dp=[[0 for i in range(n)] for i in range(m)]
#初始化
for i in range(0,m):
dp[i][0]=1
for j in range(0,n):
dp[0][j]=1
for i in range(1,m):
for j in range(1,n):
dp[i][j]=dp[i-1][j]+dp[i][j-1]
return dp[m-1][n-1]
O(n*m) 的空间复杂度
优化:二维变一维
然后根据公式 dp[i][j] = dp[i-1][j] + dp[i][j-1] 来填充矩阵的其他值。下面我们先填充第二行的值。
大家想一个问题,当我们要填充第三行的值的时候,我们需要用到第一行的值吗?答是不需要的,不行你试试,当你要填充第三,第四…第 n 行的时候,第一行的值永远不会用到,只要填充第二行的值时会用到。
根据公式 dp[i][j] = dp[i-1][j] + dp[i][j-1],我们可以知道,当我们要计算第 i 行的值时,除了会用到第 i - 1 行外,其他第 1 至 第 i-2 行的值我们都是不需要用到的,也就是说,对于那部分用不到的值我们还有必要保存他们吗?
答是没必要,我们只需要用一个一维的 dp[] 来保存一行的历史记录就可以了。然后在计算机的过程中,不断着更新 dp[] 的值
此时,dp[i] 将完全保存着第二行的值,并且我们可以推导出公式
dp[i] = dp[i-1] + dp[i]
dp[i-1] 相当于之前的 dp[i-1][j],dp[i] 相当于之前的 dp[i][j-1]。
于是按照这个公式不停着填充到最后一行
最后 dp[n-1] 就是我们要求的结果了。
def uniquePaths(m,n):
if m <= 0 | n <= 0:
return 0
dp=[1 for i in range(n)]
for i in range(m-1):#因为之前已经为第一行赋好了值,所以此处可以少一行
for j in range(n):
dp[j]=dp[j-1]+dp[j]
dp[0] = 1 #注意dp[0]应该放在后面
return dp[n-1]
有时候自己画一下图会更好理解
问题描述
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
举例:
输入:
arr = [
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
步骤一、定义数组元素的含义
由于我们的目的是从左上角到右下角,最小路径和是多少,那我们就定义 dp[i] [j]的含义为:当机器人从左上角走到(i, j) 这个位置时,最下的路径和是 dp[i] [j]。那么,dp[m-1] [n-1] 就是我们要的答案了。
注意,这个网格相当于一个二维数组,数组是从下标为 0 开始算起的,所以 由下角的位置是 (m-1, n - 1),所以 dp[m-1] [n-1] 就是我们要走的答案。
步骤二:找出关系数组元素间的关系式
想象以下,机器人要怎么样才能到达 (i, j) 这个位置?由于机器人可以向下走或者向右走,所以有两种方式到达
一种是从 (i-1, j) 这个位置走一步到达
一种是从(i, j - 1) 这个位置走一步到达
不过这次不是计算所有可能路径,而是计算哪一个路径和是最小的,那么我们要从这两种方式中,选择一种,使得dp[i] [j] 的值是最小的,显然有
dp[i] [j] = min(dp[i-1][j],dp[i][j-1]) + arr[i][j];// arr[i][j] 表示网格种的值
步骤三、找出初始值
显然,当 dp[i] [j] 中,如果 i 或者 j 有一个为 0,那么还能使用关系式吗?答是不能的,因为这个时候把 i - 1 或者 j - 1,就变成负数了,数组就会出问题了,所以我们的初始值是计算出所有的 dp[0] [0….n-1] 和所有的 dp[0….m-1] [0]。这个还是非常容易计算的,相当于计算机图中的最上面一行和左边一列。因此初始值如下:
dp[0] [j] = arr[0] [j] + dp[0] [j-1]; // 相当于最上面一行,机器人只能一直往左走
dp[i] [0] = arr[i] [0] + dp[i] [0]; // 相当于最左面一列,机器人只能一直往下走
def uniquePathes(grid):
m=len(grid)
n=len(grid[0])
if (m<0|n<0):
return 0
dp = [[0 for i in range(n)] for i in range(m)]
#初始化
dp[0][0]=grid[0][0]
for i in range(1,n):
dp[0][i]=dp[0][i-1]+grid[0][i]
for j in range(1,m):
dp[j][0]=dp[j-1][0]+grid[j][0]
#关系式
for i in range(1,m):
for j in range(1,n):
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j]
return dp[m-1][n-1]
优化----二维数组变一维数组
def uniquePathes(grid):
m=len(grid)
n=len(grid[0])
if (m<0|n<0):
return 0
dp = [0 for i in range(n)]
#初始化
dp[0]=grid[0][0]
for i in range(1,n):
dp[i]=dp[i-1]+grid[0][i]
for i in range(1,m):
dp[0] = dp[0] + grid[i][0]
for j in range(1,n):
dp[j]=min(dp[j-1],dp[j])+grid[i][j]
return dp[n-1]