我们接上一期的内容,继续来讲动态规划:
动态规划原理
适合应用动态规划方法求解的最优化问题,应该具备两个要素:最优子结构和子问题重叠。
1.最优子结构
用动态规划方法求解最优化问题的第一步就是刻画最优解的结构,如果一个问题的最优解包含其子问题的最优解,就称此问题具有最优子结构性质。某个问题是否能用动态规划方法解决,是否具有最优子结构是关键因素,当然具有最优子结构的性质也适合贪心策略。
使用动态规划方法时,用子问题的最优解来构造原问题的最优解,因此,必须小心确保考察了最优解中用到的所有子问题。
在求解最优解的过程中,需要作出选择,这个选择产生出若干子问题。作为构成原问题最优解的组成部分,每个子问题的解也是最优解。
对于不同的问题领域,最优子结构的不同体现在:原问题的最优解涉及多少个子问题,以及在确定最优解使用哪些子问题时,需要观察多少种选择。
可以用子问题的总数,以及每个子问题需要考察多少种选择,这两个因素的乘积分析动态规划算法的运行时间。比如钢条切割问题,总共有Θ(n)个子问题,每个子问题最多需要考察n种选择,因此运行时间为O(n^2 )。矩阵链乘法问题中,总共有Θ(n^2 )个子问题,每个子问题最多需要考察n-1种选择,所以运行时间为O(n^3 )。
贪心算法与动态规划有类似之处,他们之间最大的区别在于:贪心算法并不是寻找子问题的最优解,然后在其中进行选择,而是首先做出“贪心”选择—在当时(局部)看来最优的选择。然后求解出该子问题。因而不必费心求解所有可能相关的子问题。
2.重叠子问题
适合用动态规划方法解决的最优化问题应该具备的第二个性质是子问题重叠性质,也就是问题的递归算法会反复求解相同的子问题。
利用子问题重叠的性质,可以再自顶向下的递归算法中,加入备忘机制,记录已经求解的子问题,这样,再次遇到事就不必重复计算了;或者采用自底向上的方法,从最小的子问题开始计算,这样在求解某个子问题时,它所依赖的子子问题已经知道答案了。
通常情况下,如果所有的子问题都至少要被计算一次,则一个自底向上的动态规划算法通常要比一个自顶向下的备忘算法好出一个常数因子,因为前者无需递归的代价。或者,如果子问题空间中的某些子问题根本没有必要求解。自顶向下的备忘方法就会体现出优势了,因为它只会求解哪些绝对必要的子问题。
斐波那契数列
斐波那契数列记为{f(n)},其表达式如下:
f(0)=0,f(1)=1,f(n)=f(n-1)+f(n-2),n>1.
具体写出前几项,就是:0,1,1,2,3,5,8,13,21,34,55,89,144,233......
接下来,我们用动态规划伪代码来求解该数列的第n项,即f(n)的值。
伪代码如下:
function fib(n)
var previousFib := 0, currentFib := 1
if n = 0
return 0
else if n = 1
return 1
repeat n−1 times
var newFib := previousFib + currentFib
previousFib := currentFib
currentFib := newFib
return currentFib
动态规划法会在运行过程中,保存上一个子问题的解,从而避免了重复求解子问题。对于求解斐波那契数列的第n项,我们在使用动态规划法时,需要保存f(n-1)和f(n-2)的值,牺牲一点内存,但是可以显著地提升运行效率。
最长递增子序列
已知一个序列 {S1, S2,...,Sn} ,取出若干数组成新的序列 {Si1, Si2,..., Sim},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 子序列 。
如果在子序列中,当下标 ix > iy 时,Six > Siy,称子序列为原序列的一个递增子序列 。
定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1, Si2,...,Sim},如果 im < n 并且 Sim < Sn ,此时 {Si1, Si2,..., Sim, Sn} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。
因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,因此需要对前面的求解方程做修改,令 dp[n] 最小为 1,即:
对于一个长度为 N 的序列,最长子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,即 max{ dp[i] | 1 <= i <= N} 即为所求。
最长公共子系列
对于两个子序列 S1 和 S2,找出它们最长的公共子序列。
定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况:
当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1 ,即 dp[i][j] = dp[i-1][j-1] + 1。
当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,与 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。
综上,最长公共子系列的状态转移方程为:
对于长度为 N 的序列 S1 和 长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。
与最长递增子序列相比,最长公共子序列有以下不同点:
针对的是两个序列,求它们的最长公共子序列。
在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j 。
由于 2 ,在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。
来做点真题!
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
分析
只有两种办法爬楼,每次一步或者每次两步。
可以这样想,n个台阶,一开始可以爬 1 步,也可以爬 2 步,那么n个台阶爬楼的爬楼方法就等于 一开始爬1步的方法数 + 一开始爬2步的方法数,这样我们就只需要计算n-1个台阶的方法数和n-2个台阶方法数,同理,计算n-1个台阶的方法数只需要计算一下n-2个台阶和n-3个台阶,计算n-2个台阶需要计算一下n-3个台阶和n-4个台阶……
是不是就是一个斐波那契数列?!
class Solution(object):
def climbStairs(self, n):
"""
:type n: int
:rtype: int
"""
if n == 1:
return 1
if n == 2:
return 2
a = 1
b = 1
for i in range(n):
# a = b
# b = a + b
a,b = b,a+b
return a
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
class Solution(object):
def rob(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if len(nums) == 0:
return 0
elif len(nums) == 1:
return nums[0]
elif len(nums) == 2:
return max(nums[0],nums[1])
elif len(nums) == 3:
return max(nums[0]+nums[2],nums[1])
else:
dp = [0]*len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0],nums[1])
dp[2] = max(nums[1],nums[0]+nums[2])
for i in range(3,len(nums)):
dp[i] = max(dp[i-2]+nums[i],dp[i-1])
return dp[-1]
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
class Solution(object):
def rob(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
n = len(nums)
if not n:
return 0
if n <= 3:
return max(nums)
dp1 = self.helper(nums[1:])
dp2 = self.helper(nums[:-1])
return max(dp1,dp2)
def helper(self,nums):
n = len(nums)
if not n:
return False
elif n == 1:
return nums
elif n == 2:
return max(nums)
elif n == 3:
return max(nums[1],nums[0]+nums[2])
dp = n*[0]
dp[0] = nums[0]
dp[1] = max(nums[1],nums[0]+nums[2])
for i in range(n):
dp[i] = max(dp[i-1],dp[i-2]+nums[i])
return dp[-1]
300. 最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
class Solution(object):
def lengthOfLIS(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
n = len(nums)
if n <= 1:
return n
dp = n*[1]
for i in range(1,n):
for j in range(i):
if nums[j]
dp[i] = max(dp[i],dp[j]+1)
return max(dp)
376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
示例 1:
输入: [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列。
示例 2:
输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。
示例 3:
输入: [1,2,3,4,5,6,7,8,9]
输出: 2
class Solution(object):
def wiggleMaxLength(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
def wiggleLength(type,nums):
n = len(nums)
if n == 0:
return 0
pre,cnt = nums[0],1
for i in range(1,n):
if type and nums[i] > pre:
type = False
cnt += 1
elif not type and nums[i] < pre:
type = True
cnt+= 1
pre = nums[i]
return cnt
return max(wiggleLength(True,nums),wiggleLength(False,nums))
希望本文能对你有所帮助!~!
最后打个小广告,我的公众号,会写点学习心得,喜欢可以关注下!~!