递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。 由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。 与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
最关键的是找出动态方程。
1、爬楼梯__fibnumber
假设你正在爬楼梯。需要 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 阶
class solution(object):
def climbstairs(self, n):
"""
:type n: int
:rtype: int
"""
if n <= 1:
return 1
f0 = 0
f1 = 1
for i in range(1, n 1):
f = f0 f1
f0, f1 = f1, f
return f
2、强盗抢劫
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 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 。
定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。
由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 或者 i - 3 的住户,所以 dp[i] = max(dp[i-1], dp[i-2] nums[i]) 。
class solution(object):
def rob(self, nums):
"""
:type nums: list[int]
:rtype: int
"""
if nums == []:
return 0
f0 = 0
f1 = 0
for i in range(len(nums)):
f = max(f0 nums[i], f1)
f0, f1 = f1, f
return f
3、强盗在环形街区抢劫
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 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
"""
if nums == []:
return 0
if len(nums) == 1:
return nums[0]
##########max(nums(0, n-2), nums(1,n-1))
return max(self.robcore(nums, 0, len(nums)-2), self.robcore(nums, 1, len(nums)-1))
def robcore(self, nums, first, last):
f0 = 0
f1 = 0
for i in range(first, last 1):
f = max(f0 nums[i], f1)
f0, f1 = f1, f
return f
4、矩阵的最小路径和
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
class solution(object):
def minpathsum(self, grid):
"""
:type grid: list[list[int]]
:rtype: int
"""
rows = len(grid)
cols = len(grid[0])
if rows == 0 and cols == 0:
return 0
dp = [0]*cols
for i in range(rows):
for j in range(cols):
if i == 0:
if j > 0:
dp[j] = dp[j-1]
else:
if j > 0:
dp[j] = min(dp[j-1], dp[j])
dp[j] = grid[i][j]
return dp[cols-1]
5、矩阵的总路径数
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“finish”)。
问总共有多少条不同的路径?
例如,上图是一个7 x 3 的网格。有多少可能的路径?
说明:m 和 n 的值均不超过 100。
示例 1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右
示例 2:
输入: m = 7, n = 3
输出: 28
class solution(object):
def uniquepaths(self, m, n):
"""
:type m: int
:type n: int
:rtype: int
"""
if m == 0 and n == 0:
return 0
dp = [1]*n
for i in range(1, m):
for j in range(1, n):
dp[j] = dp[j] dp[j-1]
return dp[n-1]
##################method2,排列组合,c(s,d)
s = m n -2 #all
d = m - 1 #down
ret = 1
for i in range(1, d 1):
ret = ret * (s-d i) / i
return ret
6、 编辑距离
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入: word1 = "horse", word2 = "ros"
输出: 3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入: word1 = "intention", word2 = "execution"
输出: 5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
以"jerry"和"jary"为例;
三种状态如下:
int delete = dp[i - 1][j] 1;
int insert = dp[i][j - 1] 1;
int replace = dp[i - 1][j - 1] 1;
class solution(object):
def mindistance(self, word1, word2):
"""
:type word1: str
:type word2: str
:rtype: int
"""
if word1 == none or word2 == none:
return 0
m = len(word1)
n = len(word2)
#create matrix
matrix = [0 for i in range((m 1) * (n 1))]
#init x axis
for i in range(m 1):
matrix[i] = i
#init y axis
for j in range(0, len(matrix), m 1):
if j % (m 1) == 0:
matrix[j] = j // (m 1)
for i in range(1, m 1):
for j in range(1, n 1):
if word1[i-1] == word2[j-1]:
cost = 0
else:
cost = 1
matrix[j*(m 1) i] = min(matrix[(j-1)*(m 1) i] 1,
matrix[j*(m 1) (i-1)] 1,
matrix[(j-1)*(m 1) (i-1)] cost)
return matrix[-1]
public int mindistance(string word1, string word2) { //java版
if (word1 == null || word2 == null) {
return 0;
}
int m = word1.length(), n = word2.length();
int[][] dp = new int[m 1][n 1];
for (int i = 1; i <= m; i ) { //横向
dp[i][0] = i;
}
for (int i = 1; i <= n; i ) { //纵向
dp[0][i] = i;
}
for (int i = 1; i <= m; i ) {
for (int j = 1; j <= n; j ) {
if (word1.charat(i - 1) == word2.charat(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = math.min(dp[i - 1][j - 1], math.min(dp[i][j - 1], dp[i - 1][j])) 1;
}
}
}
return dp[m][n];
}
7、母牛生产
假设农场中成熟的母牛每年只会生1头小母牛,并且永远不会死。第一年农场有1只成熟的母牛,从第二年开始,母牛将开始生小母牛。每只小母牛3年之后成熟又可以开始生小母牛。给定整数n,求出n年后牛的数量。
method1:递归o(2^n)
#/usr/bin/python
#!coding:utf-8
def c(n):
if n < 1:
return 0
if (n==1 or n==2 or n==3):
return n
return c(n-1) c(n-3)
method2:加速矩阵乘法o(logn)
#/usr/bin/python
#!coding:utf-8
def c(n): #[f(n),f(n-1),f(n-2)] = [f[1],f(2),f(3)]*{
{1,1,0},{0,0,1},{1,0,0}}^n-3
if n < 1:
return 0
if (n==1 or n==2 or n==3):
return n
base = [[1,1,0],[0,0,1],[1,0,0]]#construct matrix
res = 1
for i in range(3,n):#calculate base^(n-3)
res *= base
#res = np.power(base, n-3)
return 3*res[0][0] 2*res[1][0] res[2][0]#f[1]=1,f[2]=2,f[3]=3
总结: 。
8、子数组最大的和
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
class solution(object):
def maxsubarray(self, nums):#o(n)
"""
:type nums: list[int]
:rtype: int
"""
if nums == none or len(nums) == 0:
return 0
presum = nums[0] #当前
maxsum = presum #最大
for i in range(1, len(nums)):
if presum > 0:
presum = presum nums[i]
else:
presum = nums[i]
maxsum = max(maxsum, presum)
return maxsum
9、数组中等差递增子区间的个数
dp[i] 表示以 a[i] 为结尾的等差递增子区间的个数。
在 a[i] - a[i - 1] == a[i - 1] - a[i - 2] 的条件下,{a[i - 2], a[i - 1], a[i]} 是一个等差递增子区间。如果 {a[i - 3], a[i - 2], a[i - 1]} 是一个等差递增子区间,那么 {a[i - 3], a[i - 2], a[i - 1], a[i]} 也是等差递增子区间,dp[i] = dp[i-1] 1。
class solution(object):
def numberofarithmeticslices(self, a):
"""
:type a: list[int]
:rtype: int
"""
if a == none or len(a) == 0:
return 0
dp = [0]*len(a) #存
for i in range(2, len(a)):
if a[i]-a[i-1] == a[i-1]-a[i-2]:
dp[i] = dp[i-1] 1
return sum(dp)
10、最长摆动子序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [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
设定两个dp数组来存状态
class solution(object):
def wigglemaxlength(self, nums): #o(n)
"""
:type nums: list[int]
:rtype: int
"""
if nums == none or len(nums) == 0:
return 0
up = 1 #上升
down = 1 #下降
for i in range(1, len(nums)):
if nums[i] > nums[i-1]:
up = down 1
elif nums[i] < nums[i-1]:
down = up 1
return max(up, down)
11、最长公共子序列lcs
对于两个子序列 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。
- 在求最终解时,最长公共子序列中 dp[n][m] 就是最终解,而最长递增子序列中 dp[n] 不是最终解,因为以 sn 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。
#/usr/bin/python
#!coding:utf-8
def lcs(nums1, nums2):
dp = [[0]*(len(nums1) 1)]*(len(nums2) 1)
for i in range(1, len(nums1) 1):
for j in range(1, len(nums2) 1):
if nums1[i-1] == nums2[j-1]:
dp[i][j] = dp[i-1][j-1] 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[len(nums1)][len(nums2)]
12、最长递增子序列lis
已知一个序列 {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} 即为所求。
class solution(object):
def lengthoflis(self, nums): #o(n)
"""
:type nums: list[int]
:rtype: int
"""
if nums == none or len(nums) == 0:
return 0
dp = [0]*len(nums)
for i in range(len(nums)):
maxnum = 1
for j in range(i):
if nums[i] > nums[j]:
maxnum = max(maxnum, dp[j] 1)
dp[i] = maxnum
return max(dp)
可使用二分查找法,时间复杂度为o(nlogn)。
13、0-1 背包
有一个容量为 n 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:
- 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。
- 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] v。
第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:
#!/usr/bin/python
#coding:utf-8
def bagof01(w, n, weights, values):
dp = [[0 for i in range(n 1)] for j in range(w 1)]
for i in range(1, n 1):
w = weights[i-1]
v = values[i-1]
for j in range(1, w 1):
if j >= w:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] v)#
else:
dp[i][j] = dp[i-1][j]#
return dp[n][w]
空间优化
在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时,
因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解
#!/usr/bin/python
#coding:utf-8
def bagof01(w, n, weights, values):
dp = [0 for i in range(w 1)]
for i in range(1, n 1):
w = weights[i-1]
v = values[i-1]
for j in range(w, 0, -1):
if j >= w:
dp[j] = max(dp[j], dp[j-w] v)#
return dp[w]
无法使用贪心算法的解释
0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。
变种
- 完全背包:物品数量为无限个
-
多重背包:物品数量有限制
-
多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制
-
其它:物品之间相互约束或者依赖
14、划分数组为和相等的两部分
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
- 每个数组中的元素不会超过 100
- 数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
该问题可以看作是一个背包大小为sum/2的0-1背包问题来解决。
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
class solution(object):
def canpartition(self, nums):
"""
:type nums: list[int]
:rtype: bool
"""
if nums == none or len(nums) == 0:
return []
sumnums = sum(nums)
if sumnums % 2 != 0:
return false
w = sumnums / 2
dp = [false for i in range(w 1)]
dp[0] = true
nums.sort()
for i in range(len(nums)):
for j in range(w, 0, -1):#从后往前,先计算 dp[j] 再计算 dp[j-i]
if j >= nums[i]:
dp[j] = dp[j] or dp[j-nums[i]]
return dp[w]
15、字符串按单词列表分割
给定一个非空字符串 s 和一个包含非空单词列表的字典 worddict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
示例 1:
输入: s = "leetcode", worddict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
示例 2:
输入: s = "applepenapple", worddict = ["apple", "pen"] 输出: true 解释: 返回 true 因为"
applepenapple"
可以被拆分成"
apple pen apple"
。 注意你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", worddict = ["cats", "dog", "sand", "and", "cat"]
输出: false
dict 中的单词没有使用次数的限制,因此这是一个完全背包问题。
0-1 背包和完全背包在实现上的不同之处是,0-1 背包对物品的迭代是在最外层,而完全背包对物品的迭代是在最里层。
class solution(object):
def wordbreak(self, s, worddict):
"""
:type s: str
:type worddict: list[str]
:rtype: bool
"""
if s == none and worddict == none:
return true
if s == none and worddict != none:
return false
if s != none and worddict == none:
return false
length = len(s)
dp = [false for i in range(length 1)]
dp[0] = true
for i in range(1, length 1):
for word in worddict: #完全bag,一个物品可以使用多次
lens = len(word)
if lens <= i and word == s[i-lens:i]:
dp[i] = dp[i] or dp[i-lens]
return dp[length]
16、找零钱的最少硬币数
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1
。
示例 1:
输入: coins =[1, 2, 5]
, amount =11
输出:3
解释: 11 = 5 5 1
示例 2:
输入: coins =[2]
, amount =3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
因为硬币可以重复使用,因此这是一个完全背包问题。
动态规划步骤:
确定状态:f(x)表示凑足x的钱最少的硬币数
起始状态:f(0)=0
终止状态:f(amount)
决策:要想使本f(x)最小,每次所有钱币,取最小的 f(x-ci)
无后效性:收益只取决与当前状态和决策
收益表示:f(x)=min{ f(x-ci) } 1 , i=1,2,3…n
class solution(object):#此题python超时
def coinchange(self, coins, amount):
"""
:type coins: list[int]
:type amount: int
:rtype: int
"""
if coins == none or amount < 0:
return -1
dp = [(amount 1) for i in range(amount 1)]#
dp[0] = 0
coins.sort()
for i in range(1, amount 1):
for j in range(len(coins)):
if coins[j] <= i:
dp[i] = min(dp[i], dp[i-coins[j]] 1)#
if dp[amount] == amount 1:
return -1
else:
return dp[amount]
未完待更新