今日复习内容:动态规划(基础)
动态规划是一种解决多阶段决策过程中最优化问题的数学方法和算法思想。它通常用于解决具有重叠子问题和最优子结构性质的问题,通常将问题划分为相互重叠的子问题,利用子问题的解来求解原问题。
动态规划算法通常包括以下几个关键要素:
(1)最优子结构:指问题的最优解可以通过子问题的最优解来构造。换句话说,问题的整体最优解可以通过子问题的最优解递归地求解而得到。
(2)重叠子问题:指在求解问题的过程中,需要多次解决相同的子问题。为了避免重复计算,动态规划算法通常采用记忆化搜索或者自下而上的方式来保存子问题的解,从而提高效率。
(3)状态转移方程:描述了问题从一个阶段转移到下一个阶段的递推关系,通过定义合适的状态以及状态之间的转移关系,可以将问题分解成更小的子问题,并利用这些子问题的解来解决原问题。
(4)初始化条件:确定问题的初始状态,即最小问题的子问题的解。通常需要初始化一个表格或者数组来存储子问题的解,并设置初始状态的值。
(5)递推求解:根据状态转移方程,递归地求解问题的每个阶段,直到达到问题的最终目标。
动态规划算法常用于求解诸如最长公共子序列,最短路径,背包问题等优化问题。它的核心思想是将复杂问题分解成简单的子问题,并利用子问题的解来构造原问题的解,从而实现对问题的高效求解。
我一直没搞懂什么是“状态转移”,所以我就去搜了一下。
接下来我以斐波那契数列为例来说明动态规划的状态转移过程。
斐波那契数列是一个经典的递归序列,定义如下:
斐波那契数列的每一项都是前两项的和。
现在,我来使用动态规划算法来求解斐波那契数列。在这个过程中,我将定义状态转移方程来描述问题的解决过程。
将斐波那契数列的第n项定义为dp[n],则状态转移方程可以定义为:
根据这个状态转移方程,可以通过递推关系从前两项的值推出后面的项的值,直到达到目标项的位置。
OK,接下来我把它编成代码:
def fun(n):
if n == 0:
return 0
if n == 1:
return 1
dp = [0] * (n + 1)
dp[0] = 0
dp[1] = 1
for i in range(2,n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
print(fun(2))
运行结果:
在这个例子中,我使用了动态规划算法来计算斐波那契数列的第n项,通过递推关系
dp[i] = dp[i - 1] + dp[i - 2],就可以推导出来了。
dp[i] = dp[i - 1] + dp[i - 2] 这个就是动态规划中的一个重要推导式。
我来推导一下:
比如一个人上楼梯,规定一次可以走1步,也可以走两步,当他从第一级阶梯走到第级阶梯的时候,走到步数情况如下:
从第一级走到第二级:1
从第一级走到第三级:1 + 1,2
从第二级走到第四级:1 + 1,2
从第三级走到第四级:1
从第一级走到第四级:1 + 1 + 1,1 + 2,2 + 1
然后看我写的最后三行,以第四级为基准,记为i,则从第三级到第四级(跨度为1)就是i - 1,从第二级到第四级(跨度为2)就是i - 2,那么从我推的数据来看,1 + 1 + 1 = 1 + 1 + 1,1 + 2 = 1 + 2,1 + 2 = 2 + 1。
搞定,这就是我的推导过程。
做个题:
例题:破损的楼梯
题目描述:
小蓝来到了一座高耸的楼梯前,楼梯共有N级台阶,从第0级台阶出发,小蓝每次可以迈上1级或2级台阶。但是,楼梯上的第ai级,第a2级,...,以此类推,共有M级台阶的台阶面坏了,不能踩上去。
现在,小蓝想要到达楼梯的顶端,也就是第N级台阶,但他不能踩到坏了的台阶。请问他有多少种步踩到坏了的台阶但是能到达顶端的方案数。
由于方案数很大,请输出其对10^9 + 7取模。
输入格式:
第一行包含两个正整数N(1 <= N <= 10^5)和M(0 <= M <= N),表示楼梯的总级数和坏了的台阶数。
接下来一行,包含M哥正整数a1,a2,...,aM(1 <= a1 < a2 < a3 < ... < aM <= N),表示坏掉的台阶的编号。
输出格式:
输出一个整数,表示小蓝到达楼梯顶端的方案数,对10^9 + 7取模。
思路:
这个题的原型就是我上面推导的代码,它只是加了一个“有坏台阶”的约束条件。
参考答案:
N,M = map(int,input().split())
a = list(map(int,input().split()))
vis = [0]*(N + 1)
dp = [0]*(N + 1)
for x in a:
vis[x] = 1
dp[0] = 1
dp[1] = 1 - vis[1]
for i in range(2,N + 1):
if vis[i] == 1:
continue
dp[i] = (dp[i - 1] + dp[i - 2]) % 1000000007
print(dp[N])
运行结果:
以上是一维的,接下来我来分析二维的。
动态规划是一种常用的算法设计技巧,用于解决具有重叠子问题和最优子结构性质的问题。二维动态规划是动态规划中的一种形式,适用于解决二维状态的问题。
在二维动态规划中,我们通常使用一个二维数组来存储子问题的解。这个二维数组的每个元素通常表示一个状态,而状态之间的转移则通过状态转移方程来描述。二维DP通常适用于具有二维状态的问题,比如矩阵,网格等。
下面是二维动态规划的一般步骤:
(1)定义状态:首先确定问题的状态,即确定二维DP数组的含义。这通常涉及到问题的规模和限制条件。状态的定义应该能够唯一地描述问题的局部情况。
(2)状态转移方程:接下来确定状态之间的转移关系,也就是状态转移方程。状态转移方程描述了如何从一个状态转移到下一个状态,并且通常是问题的核心。通过状态转移方程,我们可以将原问题划分成若干个子问题,从而利用子问题的解来求解原问题。
(3)初始化:对DP数组进行初始化。初始化通常是为了处理边界情况或一些特殊情况,使得DP数组的初始状态满足状态转移方程。
(4)状态转移:通过状态转移方程,从初始状态开始逐步更新DP数组,直到达到目标状态。
(5)求解目标:最终,根据问题的要求,确定DP数组中哪些状态对应着我们需要的结果。
这么说太抽象了,我来举个例子,解释一下这个过程。
问题:在一个二维网格中,从左上角到右下角有多少条不同的路径?每次只能向右或向下移动。
(1)定义状态:令dp[i][j]表示从起点到达网格中坐标为(i,j)的位置的不同路径数。
(2)状态转移方程:对于网格中的每个位置(i,j),可以从左边位置(i - 1,j)或者上方位置(i,j - 1)到达,因此状态转移方程为dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。
(这个左边和上方坐标怎么来的,我以前的文章解释过两次,可以去参考一下)
(3)初始化:对于网格的第一行和第一列,由于只能向右或向下移动,因此到达这些位置的路径数都为1,所以初始化为1。
(4)状态转移:从左上角开始逐步更新dp数组,根据状态转移方程进行状态转移。
(5)求解目标:最终,dp[m - 1][n - 1]即为从左上角到右下角的不同路径数。
二维动态规划是动态规划中的重要形式之一,它使用于许多实际问题的求解,如最长公共子序列,最长递增子序列,矩阵路径等。通过合理定义状态和状态转移方程,我们可以高效解决许多复杂问题。
OK,我现在的头脑无比清晰,来做个题吧。
例题1:矩阵中的最长递增路径
题目描述:
给定一个整数矩阵,找出最长递增路径 的长度。对于每个单元格,你可以从当前单元格向上,向下,向左,向右移动,但不能移动到边界外(即不允许环绕)。
示例:
输入:
[
[9,9,4],
[6,6,8],
[2,1,1]
]
输出: 4
解释: 最长递增路径为 [1, 2, 6, 9]。
解题思路:
(1)定义状态:定义一个二维dp数组,其中dp[i][j]表示以(i,j)为起点的最长递增路径的长度。
(2)状态转移方程:对于每个位置(i,j),可以向上,向下,向左,向右四个方向移动,如果相邻位置的值大于当前位置,则可以移动到相邻位置。因此,状态转移方程为
dp[i][j] = max(dp[i][j],1 + dp[x][y]),其中(x,y)为相邻位置且大于(i,j)的位置。
(3)初始化:将dp数组所有元素初始化为1,因此最短路径长度至少为1。
(4)状态转移:从左上角开始遍历矩阵,对每个位置(i,j),根据状态转移方程更新dp[i][j]。
(5)求解目标:遍历整个dp数组,找出最大值即为最长递增路径的长度。
接下来一步一步写代码:
(1)函数定义:
def longestpath(a):
这里我定义了一个名为longestpath的函数 ,它接受一个二维整数矩阵a作为输入
(2)边界情况处理:
if not a:
return 0
如果输入的矩阵a为空,即没有任何元素,那么直接返回0,表示最长递增路径的长度为0.
(3)矩阵行数和列数获取:
r,c = len(a),len(a[0])
获取输入矩阵的行数和列数,以便后续的遍历和处理。
(4) dp数组的初始化
dp = [[1]*c for _ in range(r)]
创建一个与输入矩阵 相同大小的二维数组dp,并将所有元素初始化为1.这里dp[i][j]表示以(i,j)为起点的最长递增路径的长度,初始值设为1,表示最短路径长度为1。
(5)深度优先搜索(DFS)函数定义
def dfs(i,j):
if dp[i][j] != 1:
return dp[i][j]
directions = [(0,1),(0,-1),(1,0),(-1,0)]
for dx,dy in directions:
x,y = i + dx,j + dy
if 0 <= x < r and 0 <= y < c and a[x][y] > a[i][j]:
dp[i][j] = max(dp[i][j],1 + dfs(x,y))
return dp[i][j]
这里我用的是搜索法,用于在矩阵中查找最长递增路径,它接受两个参数i和j,表示当前位置的行和列。首先检查是否已经计算过(i,j)位置的最长递增路径长度,如果已经计算过,则直接返回结果;否则,尝试向四个方向进行移动,并更新dp[i][j]的值。
(6)全局最长路径变量的初始化
result = 0
初始化一个变量result,用于记录全局最长递增路径的长度。
(7) 矩阵变量及结果更新
for i in range(r):
for j in range(c):
result = max(result,dfs(i,j))
遍历整个矩阵,对每个位置(i,j),调用深度优先搜索函数dfs(i,j) ,并与返回的结果result比较,保留较大值作为全局最长路径的长度。
(8)返回结果
return result
返回全局最长递增路径的长度作为函数的输出结果。
OK,现在我把它完整的写出来。
def longestpath(a):
if not a:
return 0
r,c = len(a),len(a[0])
dp = [[1]*c for _ in range(r)]
def dfs(i,j):
if dp[i][j] != 1:
return dp[i][j]
directions = [(0,1),(0,-1),(1,0),(-1,0)]
for dx,dy in directions:
x,y = i + dx,j + dy
if 0 <= x < r and 0 <= y < c and a[x][y] > a[i][j]:
dp[i][j] = max(dp[i][j],1 + dfs(x,y))
return dp[i][j]
result = 0
for i in range(r):
for j in range(c):
result = max(result,dfs(i,j))
return result
a = [
[9, 9, 4],
[6, 6, 8],
[2, 1, 1]
]
print(longestpath(a))
运行结果:
答案是正确的。
经过我的不懈努力,我终于会一点动态规划了!
OK,这篇就写到这里,下一篇继续!