动态规划:运筹学中一种求最值的算法
套路:明确状态和选择;明确dp定义;梳理每次选择的逻辑
注:以下题号为leetcode题号,可以在leetcode上搜索找到原题
目录
矩阵路径
47. 礼物的最大价值&64. 最小路径和
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
动态规划 这次用的方法不需要额外的存储空间,直接在原数组上进行修改 时间复杂度O(m*n),空间复杂度O(1)
class Solution: def maxValue(self, grid): for i in range(len(grid)): for j in range(len(grid[0])): if i == 0 and j==0: continue if i== 0 and j >= 1: #只能来源于左侧的格子 grid[i][j] = grid[i][j-1]+grid[i][j] elif j == 0 and i >=1: grid[i][j] = grid[i-1][j] +grid[i][j] else: A = grid[i-1][j] + grid[i][j] B = grid[i][j-1]+ grid[i][j] grid [i][j] = max(A,B) return grid[len(grid)-1][len(grid[0])-1]
62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
class Solution: def uniquePaths(self, m: int, n: int) -> int: dp = [[0]*(n) for _ in range(m)] for i in range(m): for j in range(n): if i==0 or j==0: dp[i][j] = 1 else: dp[i][j] = dp[i-1][j]+dp[i][j-1] return dp[m-1][n-1]
字符串问题
647. 回文子串
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被计为是不同的子串。
示例 1:
输入: "abc"
输出: 3
解释: 三个回文子串: "a", "b", "c".
示例 2:
输入: "aaa"
输出: 6
说明: 6个回文子串: "a", "a", "a", "aa", "aa", "aaa".
使用动态规划, dp[i][j] 代表str[i] - str[j]是否是回文子串 考虑单字符和双字符的特殊情况 状态转移方程:dp[i][j] = dp[i+1][j-1] && str[i]==str[j]
class Solution: def countSubstrings(self, s: str) -> int: L=len(s) if L==0:return 0 if L==1:return 1 dp=[[False for _ in range(L)]for _ in range(L)] res=0 for r in range(L): for l in range(r+1): if r==l: dp[l][r]=True res+=1 elif s[l]==s[r] and(r-l<=2 or dp[l+1][r-1]): dp[l][r]=True res+=1 return res
72. 编辑距离
编辑距离问题就是给我们两个字符串 s1 和 s2,只能用三种操作,让我们把 s1 变成 s2,求最少的操作数。解决两个字符串的动态规划问题,一般都是用两个指针 i,j 分别指向两个字符串的最后,然后一步步往前走,缩小问题的规模。
if s1[i] == s2[j]: 啥都别做(skip) i, j 同时向前移动 else: 三选一: 插入(insert) 删除(delete) 替换(replace)
「三选一」到底该怎么选择呢?很简单,全试一遍,哪个操作最后得到的编辑距离最小,就选谁。这里需要递归技巧,理解需要点技巧,
def minDistance(s1,s2): def dp(i,j): if i==-1: return j+1 if j==-1: return i+1 if s1[i]==s2[j]: return dp(i-1,j-1) else: return min( dp(i,j-1)+1, #插入,直接在 s1[i] 插入一个和 s2[j] 一样的字符 前移 j,继续跟 i 对比 dp(i-1,j)+1, dp(i-1,j-1)+1 ) return dp(len(s1)-1,len(s2)-1)
这个解法是暴力解法,存在重叠子问题,需要用动态规划技巧来优化。
怎么能一眼看出存在重叠子问题呢?这里再简单提一下,需要抽象出本文算法的递归框架:
def dp(i, j): dp(i - 1, j - 1) #1 dp(i, j - 1) #2 dp(i - 1, j) #3
对于子问题 dp(i-1, j-1),如何通过原问题 dp(i, j) 得到呢?有不止一条路径,比如 dp(i, j) -> #1 和 dp(i, j) -> #2 -> #3。一旦发现一条重复路径,就说明存在巨量重复路径,也就是重叠子问题。
动态规划优化
对于重叠子问题呢,优化方法无非是备忘录或者 DP table。
备忘录很好加,原来的代码稍加修改即可:
class Solution: def minDistance(self, word1: str, word2: str) -> int: memo = dict() def dp(i,j): if (i,j) in memo: return memo[(i,j)] if i == -1: return j+1 if j == -1: return i+1 if word1[i]==word2[j]: memo[(i,j)] = dp(i-1,j-1) else: memo[(i,j)] = min(dp(i-1,j)+1,dp(i,j-1)+1,dp(i-1,j-1)+1) return memo[(i,j)] return dp(len(word1)-1,len(word2)-1)
主要说下 DP table 的解法:
首先明确 dp 数组的含义,dp 数组是一个二维数组,长这样:
有了之前递归解法的铺垫,应该很容易理解。dp[..][0] 和 dp[0][..] 对应 base case,dp[i][j] 的含义和之前的 dp 函数类似:
def dp(i, j) -> int # 返回 s1[0..i] 和 s2[0..j] 的最小编辑距离 dp[i-1][j-1] # 存储 s1[0..i] 和 s2[0..j] 的最小编辑距离
dp 函数的 base case 是 i,j 等于 -1,而数组索引至少是 0,所以 dp 数组会偏移一位。
既然 dp 数组和递归 dp 函数含义一样,也就可以直接套用之前的思路写代码,唯一不同的是,DP table 是自底向上求解,递归解法是自顶向下求解:
class Solution: def minDistance(self, word1: str, word2: str) -> int: m = len(word1) n = len(word2) dp = [[0] * (n + 1) for _ in range(m + 1)] dp[0][0] = 0 for i in range(1,m+1): dp[i][0] = i for j in range(1,n+1): dp[0][j] = j for i in range(1,m+1): for j in range(1,n+1): if word1[i-1]==word2[j-1]: dp[i][j] = dp[i-1][j-1] else: dp[i][j] = min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+1) return dp[m][n]
139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
class Solution: def wordBreak(self, s: str, wordDict: List[str]) -> bool: n=len(s) dp=[False]*(n+1) dp[0]=True for i in range(n): for j in range(i+1,n+1): if(dp[i] and (s[i:j] in wordDict)): dp[j]=True return dp[-1]
474. 一和零
假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。
你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。
输入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
输出: 4
解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。
示例 2:
输入: Array = {"10", "0", "1"}, m = 1, n = 1
输出: 2
解释: 你可以拼出 "10",但之后就没有剩余数字了。更好的选择是拼出 "0" 和 "1" 。
0-1 背包问题,有两个背包大小,0 的数量和 1 的数量
可用0和1的个数可以看成不同容量的背包(二维)
dp[i][j] i 表示可用0的个数, j 表示可用的1的个数
对应每一个01串, 做的事情:
对于可以放得下的背包 ①不放,则查看原旧背包容量 ②放,则 1(当前01串)+ 变小的 旧背包容量
dp[i][j] = max(dp[i][j], 1 + dp[ i-item_count0 ][ j-item_count1 ])
class Solution: def findMaxForm(self, strs: List[str], m: int, n: int) -> int: if len(strs) == 0: return 0 dp = [[0]*(n+1) for _ in range(m+1)] for str in strs: count0 = str.count('0') count1 = str.count('1') for i in range(m,count0-1,-1): for j in range(n,count1-1,-1): dp[i][j] = max(dp[i][j],1+dp[i-count0][j-count1]) return dp[-1][-1]
650. 只有两个键的键盘
最初在一个记事本上只有一个字符 'A'。你每次可以对这个记事本进行两种操作:
Copy All (复制全部) : 你可以复制这个记事本中的所有字符(部分的复制是不允许的)。
Paste (粘贴) : 你可以粘贴你上一次复制的字符。
给定一个数字 n 。你需要使用最少的操作次数,在记事本中打印出恰好 n 个 'A'。输出能够打印出 n 个 'A' 的最少操作次数。
示例 1:
输入: 3
输出: 3
解释:
最初, 我们只有一个字符 'A'。
第 1 步, 我们使用 Copy All 操作。
第 2 步, 我们使用 Paste 操作来获得 'AA'。
第 3 步, 我们使用 Paste 操作来获得 'AAA'。
class Solution: def minSteps(self, n: int) -> int: if n == 1: return 0 dp = [[n]*n for _ in range(n)] #dp[i][j] 记事本上有i+1个A,粘贴板上有j+1个A dp[0][0] = 1 for i in range(1,n): for j in range(i): dp[i][j] = dp[i-j-1][j] + 1 dp[i][i] = min(dp[i]) + 1 return dp[-1][-1] -1
序列问题
152 乘积最大子序列——动态规划
给定一个整数数组 nums ,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)。
示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
看到求什么什么的连续子序列,一般都是DP(动态规划),而且DP【i】代表的是以nums[i]结尾的连续子序列的某种状态。
基础的dpmax用来表示乘积最大的子序列的乘积,
比较特殊的地方在于,因为是算乘积而且没有限定输入的范围,所以需要考虑负数的情况。
所以要额外开一个dpmin表示乘积最小的子序列的乘积。
class Solution(object): def maxProduct(self, nums): l = len(nums) dpmax = [0 for _ in range(l)] dpmin = [0 for _ in range(l)] dpmax[0] = nums[0] dpmin[0] = nums[0] for i in range(1,l): dpmax[i] = max(nums[i],max(nums[i]*dpmax[i-1],nums[i]*dpmin[i-1])) dpmin[i] = min(nums[i], min(nums[i]*dpmax[i-1], nums[i]*dpmin[i-1])) return max(dpmax)
300. 最长递增子序列
dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
定义一个dp数组与原数组等长,dp[i]表示,以nums[i]为结尾的最长子序列长度。 遍历数组,得到nums[i],遍历nums[i]前面的数,如果该数比nums[i]小,则dp[i] = max(dp[i],dp[j]+1) 最后取dp数组中最大的值返回
class Solution: def lengthOfLIS(self, nums: List[int]) -> int: if not nums: return 0 dp = [1]*len(nums)# dp[0],dp[len(nums)-1] for i in range(len(nums)): for j in range(0,i): if nums[j]<nums[i]: dp[i] = max(dp[i],dp[j]+1) res = max(dp) return res
时间复杂度为O(n^2)
如果需要时间复杂度为O(NlogN),需要用二分查找解法。
376. 最长摆动子序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
要求:使用 O(n) 时间复杂度求解。
使用两个状态 up 和 down。
class Solution: def wiggleMaxLength(self, nums: List[int]) -> int: n = len(nums) if n==0: return 0 up = 1 down = 1 for i in range(1,n): if nums[i]>nums[i-1]: up = down+1 elif nums[i]<nums[i-1]: down = up+1 return max(up,down)
1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
def longestCommonSubsequence(str1, str2) -> int: m, n = len(str1), len(str2) # 构建 DP table 和 base case dp = [[0] * (n + 1) for _ in range(m + 1)] # 进行状态转移 for i in range(1, m + 1): for j in range(1, n + 1): if str1[i - 1] == str2[j - 1]: # 找到一个 lcs 中的字符 dp[i][j] = 1 + dp[i-1][j-1] else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) return dp[-1][-1]
583. 两个字符串的删除操作-同最长公共子序列
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
输入: "sea", "eat"
输出: 2
解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
m,n = len(str1),len(str2)
return m+n-2*dp[-1][-1]
53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
令 sum[i] 为以 num[i] 为结尾的子数组最大的和,可以由 sum[i-1] 得到 sum[i] 的值,如果 sum[i-1] 小于 0,那么以 num[i] 为结尾的子数组不能包含前面的内容,因为加上前面的部分,那么和一定会比 num[i] 还小。
class Solution: def maxSubArray(self, nums: List[int]) -> int: if not nums: return 0 dp = [nums[0]] res = dp[0] for i in range(1,len(nums)): dp.append(max(dp[i-1]+nums[i],nums[i])) if dp[-1] > res: res = dp[-1] return res
413. 等差数列划分
A = [1, 2, 3, 4]
返回: 3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]。
对于 (1,2,3,4),它有三种组成递增子区间的方式,而对于 (1,2,3,4,5),它组成递增子区间的方式除了 (1,2,3,4) 的三种外还多了一种,即 (1,2,3,4,5),因此 dp[i] = dp[i - 1] + 1。
class Solution: def numberOfArithmeticSlices(self, A: List[int]) -> int: n = len(A) dp = [0]*(n) for i in range(2,n): if A[i]-A[i-1]==A[i-1]-A[i-2]: dp[i] += dp[i-1]+1 return sum(dp)
分割整数
343. 整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
它代表着数字 i 可以拆分成 j + (i - j)
class Solution: def integerBreak(self, n: int) -> int: dp = [1]*(n+1) for i in range(2,n+1): for j in range(1,i): dp[i] = max(dp[i],max(j*dp[i-j],j*(i-j))) return dp[n]
279.按平方数来分割整数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
class Solution(object): def numSquares(self, n): dp = [0]*(n+1) for i in range(1,n+1): dp[i] = i #全是1 的话是i个 j = 1 while (i - j*j) >= 0: dp[i] = min(dp[i],dp[i-j*j]+1) j += 1 return dp[n]
49. 丑数 &264
我们把只包含因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
// 一个十分巧妙的动态规划问题 // 1.我们将前面求得的丑数记录下来,后面的丑数就是前面的丑数*2,*3,*5 // 2.但是问题来了,我怎么确定已知前面k-1个丑数,我怎么确定第k个丑数呢 // 3.采取用三个指针的方法,p2,p3,p5 // 4.index2指向的数字下一次永远*2,p3指向的数字下一次永远*3,p5指向的数字永远*5 // 5.我们从2*p2 3*p3 5*p5选取最小的一个数字,作为第k个丑数 // 6.如果第K个丑数==2*p2,也就是说前面0-p2个丑数*2不可能产生比第K个丑数更大的丑数了,所以p2++ // 7.p3,p5同理 // 8.返回第n个丑数
class Solution: def nthUglyNumber(self, n: int) -> int: if n==1: return 1 p2,p3,p5 = 0,0,0 dp = [1 for _ in range(n)] dp[0] = 1 for i in range(1,n): dp[i] = min(dp[p2]*2,min(dp[p3]*3,dp[p5]*5)) if dp[i]==dp[p2]*2: p2 +=1 if dp[i]==dp[p3]*3: p3 +=1 if dp[i] == dp[p5]*5: p5 +=1 return dp[n-1]
338. 比特位计数
给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。
输入: 2
输出: [0,1,1]
对于数字 6(110),它可以看成是数字 2(10) 前面加上一个 1 ,因此 dp[i] = dp[i&(i-1)] + 1;
class Solution: def countBits(self, num: int) -> List[int]: dp=[0]*(num+1) for i in range(1,num+1): dp[i] = dp[i&(i-1)]+1 return dp
60. n个骰子的点数
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
动态规划
dp[i][j],表示投掷完 i 枚骰子后,点数之和 j 的出现次数。
点数之和j总共有 6*n种
class Solution: def twoSum(self, n: int) -> List[float]: dp = [[0] * (6*n+1) for _ in range(n+1)] for i in range(1, 7): dp[1][i] = 1 for i in range(2, n+1): for j in range(i, 6*i+1, 1): for k in range(1, 7, 1): if j-k < i-1: break dp[i][j] += dp[i-1][j-k] return [x/6**n for x in dp[n][n:6*n+1]]
背包问题
(1)求最大价值
给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?
一个典型的动态规划问题。这个题目中的物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。这也许就是 0-1 背包这个名词的来历。
第一步要明确两点,「状态」和「选择」。
只要给定几个可选物品和一个背包的容量限制,就形成了一个背包问题。所以状态有两个,就是「背包的容量」和「可选择的物品」
选择就是「装进背包」或者「不装进背包」
套框架:
for 状态1 in 状态1的所有取值: for 状态2 in 状态2的所有取值: for ... dp[状态1][状态2][...] = 择优(选择1,选择2...)
第二步要明确dp数组的定义。
dp数组是什么?其实就是描述问题局面的一个数组。我们刚才明确问题有什么「状态」,现在需要用dp数组把状态表示出来。
dp[i][w]的定义如下:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值是dp[i][w]。
根据这个定义,我们想求的最终答案就是dp[N][W]。base case 就是dp[0][..] = dp[..][0] = 0,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。
第三步,根据「选择」,思考状态转移的逻辑。
如果你没有把这第i个物品装入背包,那么很显然,最大价值dp[i][w]应该等于dp[i-1][w]。
如果你把这第i个物品装入了背包,那么dp[i][w]应该等于dp[i-1][w-wt[i-1]] + val[i-1]。由于i是从 1 开始的,所以对val和wt的取值是i-1。
最后一步,处理一些边界情况。
N = 3, W = 4
wt = [2, 1, 3]
val = [4, 2, 3]
def func(wt,val,N,W): dp = [[0]*(W+1) for _ in range(N+1)] #填入0,相当于basecase for i in range(1,N+1): for j in range(1,W+1): if w-wt[i-1]<0: #当前背包容量装不下,只能选择不装入背包 dp[i][j] = dp[i-1][j] else: dp[i][j] = max(dp[i-1][j],dp[i-1][j-wt[i-1]]+val[i-1]) return dp[-1][-1]
(2)求装包方法
问题:背包容量为w。 一共有n袋零食, 第i袋零食体积为v[i]。 想知道在总体积不超过背包容量的情况下,一共有多少种零食放法(总体积为0也算一种放法)。
输入:
第一行 两个正整数n和w(1 <= n <= 30, 1 <= w <= 2 * 10^9),表示零食的数量和背包的容量。
第二行 n个正整数v[i](0 <= v[i] <= 10^9),表示每袋零食的体积。
3 10 1 2 4
输出:
8
代码
N,W = list(map(int,input().split())) V = list(map(int,input().split())) def count(W,V): if W<=0 or len(V)<=0 : return 1 if sum(V)<=W: return 2**len(V) #每个物品都可以放与不放V if V[0]<=W: return count(W-V[0],V[1:])+count(W,V[1:]) else: return count(W,V[1:]) print(count(W,V))
322 零钱兑换-完全背包问题
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
dp【i】钱数为i有几枚硬币
class Solution: def coinChange(self, coins: List[int], amount: int) -> int: if not coins or amount<0: return -1 dp = [float('INF')]*(amount+1) dp[0] = 0 for i in range(1,amount+1): for coin in coins: if i>=coin: dp[i] = min(dp[i],dp[i-coin]+1) if dp[-1] == float('INF'): return -1 else: return dp[-1]
518. 零钱兑换 II -完全背包
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
class Solution: def change(self, amount: int, coins: List[int]) -> int: dp = [0]*(amount+1) dp[0] = 1 for coin in coins: for i in range(1,amount+1): if i-coin>=0: dp[i] += dp[i-coin] return dp[amount]
494. 目标和--完全背包
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
一共有5种方法让最终目标和为3。
先使用数学思路,可以知道赋予标号后,集合中包含负数和正数,则有 sum(Positive)-sum(Negtive) = S。因为sum(Positive)+sum(Negtive)=sum(nums),则有2*sum(Positive)=sum(nums)+S,故sum(Positive)=(sum(nums)+S)/2,由于(sum(nums)+S)/2是固定的整数,所以只需要找到和为它的组合数即可。 经过上述解析,可以将问题转化为从nums中找到和为(sum(nums)+S)/2的组合个数。这个问题可以通过动归来解决,用dp[i]存储和为i的组合数,然后对nums中的整数n进行遍历,对于所有i>=num的i,则有dp[i]=dp[i]+dp[i-n]
class Solution: def findTargetSumWays(self, nums: List[int], S: int) -> int: n_sum = sum(nums) if n_sum<S: return 0 if (n_sum+S)%2==1: return 0 target = (n_sum+S)//2 dp = [0]*(target+1) dp[0]=1 for i in range(len(nums)): for j in range(target,-1,-1): if j-nums[i]>=0: dp[j] += dp[j-nums[i]] return dp[target]
416. 分割等和子集-完全背包
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
对于这个问题,我们可以先对集合求和,得出sum,把问题转化为背包问题: 给一个可装载重量为sum/2的背包和N个物品,每个物品的重量为nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满? 1.状态就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。 2.dp[i][j] = x表示,对于前i个物品,当前背包的容量为j时,若x为true,则说明可以恰好将背包装满,若x为false,则说明不能恰好将背包装满。 我们想求的最终答案就是dp[N][sum/2].base case 就是dp[..][0] = true和dp[0][..] = false,因为背包没有空间的时候,就相当于装满了,而当没有物品可选择的时候,肯定没办法装满背包。 3.状态转移。dp[i][j] = dp[i - 1][j] | dp[i - 1][j-nums[i-1]] 如果不把nums[i]算入子集,或者说你不把这第i个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态dp[i-1][j],继承之前的结果。如果把nums[i]算入子集,或者说你把这第i个物品装入了背包,那么是否能够恰好装满背包,取决于状态dp[i - 1][j-nums[i-1]]。首先,由于i是从 1 开始的,而数组索引是从 0 开始的,所以第i个物品的重量应该是nums[i-1],这一点不要搞混。dp[i - 1][j-nums[i-1]]也很好理解:你如果装了第i个物品,就要看背包的剩余重量j - nums[i-1]限制下是否能够被恰好装满。换句话说,如果j - nums[i-1]的重量可以被恰好装满,那么只要把第i个物品装进去,也可恰好装满j的重量;否则的话,重量j肯定是装不满的。
注意到dp[i][j]都是通过上一行dp[i-1][..]转移过来的,之前的数据都不会再使用了。 所以,我们可以进行状态压缩,将二维dp数组压缩为一维,节约空间复杂度。 dp[j] = dp[j] || dp[j - nums[i]],只在一行dp数组上操作,i每进行一轮迭代,dp[j]其实就相当于dp[i-1][j],所以只需要一维数组就够用了。时间复杂度 O(n*sum),空间复杂度 O(sum)。
class Solution: def canPartition(self, nums: List[int]) -> bool: if sum(nums)%2==1: return False n_sum = sum(nums)//2 dp = [False]*(n_sum+1) #dp = [0,1,...,n_sum],长度为n_sum+1 dp[0] = True #如果重量要求为0,不拿即可,可行 for i in range(len(nums)): for j in range(n_sum,-1,-1): if j-nums[i]>=0: dp[j] = dp[j] or dp[j-nums[i]] return dp[n_sum]
377. 组合总和 Ⅳ
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
nums = [1, 2, 3]
target = 4
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
class Solution: def combinationSum4(self, nums: List[int], target: int) -> int: dp = [0]*(target+1) dp[0] = 1 for i in range(1,target+1): for j in range(len(nums)): if nums[j]<=i: dp[i]+=dp[i-nums[j]] return dp[-1]
打家劫舍系列
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
【选择】:抢和不抢,如果你抢了这间房子,那么你肯定不能抢相邻的下一间房子了,只能从下下间房子开始做选择。如果你不抢这间房子,那么你可以走到下一间房子前,继续做选择。
当你走过了最后一间房子后,你就没得抢了,能抢到的钱显然是 0(base case)。
你面前房子的索引就是状态,抢和不抢就是选择。
dp(nums,start+1) 不抢,去下家
nums[start]+dp(nums,start+2) 抢,然后去下下家
只和索引有关,只用一维数组
def rob(self,nums): n = len(nums) dp = [0]*(n+2) for i in range(2,n+2): dp[i] = max(dp[i-1],nums[i-2]+dp[i-2]) return dp[-1]
非优化空间复杂度O(n),时间复杂度O(n)
def rob(self,nums): cur,pre = 0,0 for i in nums: cur, pre = max(pre+i,cur),cur return cur
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
其实就是把环拆成两个队列,一个是从0到n-1,另一个是从1到n,然后返回两个结果最大的。
class Solution: def rob(self, nums: [int]) -> int: def my_rob(nums): cur, pre = 0, 0 for num in nums: cur, pre = max(pre + num, cur), cur return cur return max(my_rob(nums[:-1]),my_rob(nums[1:])) if len(nums) != 1 else nums[0]
337. 打家劫舍 III
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
这道题在递归返回二个值, 一个值是偷这家或者不偷这家最大值,一个值不偷的(为了使偷下一家准备的)
class Solution: def rob(self,root): def dp(root): if not root: return 0,0 left,prev1 = dp(root.left) right,prev2 = dp(root.right) a = max(prev1+prev2+root.val,left+right) b = left+right return a,b return dp(root)[0]
博弈游戏
借石头游戏来讲讲「假设两个人都足够聪明,最后谁会获胜」这一类问题该如何用动态规划算法解决。
「石头游戏」改的更具有一般性:
你和你的朋友面前有一排石头堆,用一个数组 piles 表示,piles[i] 表示第 i 堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。
石头的堆数可以是任意正整数,石头的总数也可以是任意正整数,这样就能打破先手必胜的局面了。比如有三堆石头 piles = [1,100,3],先手不管拿 1 还是 3,能够决定胜负的 100 都会被后手拿走,后手会获胜。
假设两人都很聪明,请你设计一个算法,返回先手和后手的最后得分(石头总数)之差。比如上面那个例子,先手能获得 4 分,后手会获得 100 分,你的算法应该返回 -96。
博弈问题的通用设计框架:
dp数组:
首先要找到所有「状态」和每个状态可以做的「选择」,然后择优。
根据前面对 dp 数组的定义,状态显然有三个:开始的索引 i,结束的索引 j,当前轮到的人。
dp[i][j][fir or sec] 其中: 0 <= i < piles.length i <= j < piles.length
写出状态转移方程
根据 dp 数组的定义,我们也可以找出 base case,也就是最简单的情况:
这里需要注意一点,我们发现 base case 是斜着的,而且我们推算 dp[i][j] 时需要用到 dp[i+1][j] 和 dp[i][j-1]:所以说算法不能简单的一行一行遍历 dp 数组,而要斜着遍历数组:
def stoneGame(piles): col = len(piles) dp = [[[0,0] for _ in range(col)] for _ in range(col)] for i in range(col): dp[i][i][0] = piles[i] #填充对角线,basecase for k in range(2,col+1): #对角线遍历 for i in range(0,col-1): j = k+i-1 if j==col: break #防止溢出 left = piles[i]+dp[i+1][j][1] right = piles[j]+dp[i][j-1][1] #先手选择最左边或最右边的分数 if left>right: dp[i][j][0] = left dp[i][j][1] = dp[i+1][j][0] else: dp[i][j][0] = right dp[i][j][1] = dp[i][j-1][0] res = dp[0][col-1] return res[0]-res[1]
877. 石子游戏
每轮拿一顿,问先手是否获胜。
与上相同,结尾换成比较即可。
return res[0]>res[1]
1140. 石子游戏 II
亚历克斯和李继续他们的石子游戏。许多堆石子 排成一行,每堆都有正整数颗石子 piles[i]。游戏以谁手中的石子最多来决出胜负。
亚历克斯和李轮流进行,亚历克斯先开始。最初,M = 1。
在每个玩家的回合中,该玩家可以拿走剩下的 前 X 堆的所有石子,其中 1 <= X <= 2M。然后,令 M = max(M, X)。
游戏一直持续到所有石子都被拿走。
假设亚历克斯和李都发挥出最佳水平,返回亚历克斯可以得到的最大数量的石头。
加备忘录的动态规划
dp(i,M)表示从i堆开始拿,允许拿 M <= x <= 2 * M 时,在剩余石子中所能拿到的最大值。
最终返回dp(0,1)
搜索状态时,我们要遵循以下几个原则:
- 如果 i >= n,那么说明石子都已经拿完,直接返回 0;
- 如果 i + M * 2 >= n,那么说明可以把剩余石子一起拿到,就可以直接返回剩余石子的数目 sum(piles[i:]);
- 如果不属于以上两种情况,那么我们需要遍历 1 <= x <= 2 * M,求剩余的最小 dp(i + x, max(x, M)),也就是自己拿多少的时候,对手拿的石子最少(由于剩余石子数固定,那么最小化对手石子数,就是最大化自己的石子数)。
为了防止重复搜索,可以采用记忆化的方法。为了快速求剩余石子数目,可以提前处理后缀和。
如图所示, dfs(i, M) 表示,当从第 i 堆石子开始拿,允许拿 M <= x <= 2 * M 时,在剩余石子中所能拿到的最大值。蓝色块代表先手拿的状态,黄色块代表后手拿的状态。边上的权值代表拿了几堆石子(也就是 x),红色边代表当前层最优解,连续的红色路径就是答案。
class Solution: def stoneGame(self,piles): memo = dict() n = len(piles) def dp(i,m): if (i,m) in memo: return memo[(i,m)] if i>=n: # 溢出拿不到任何石子 return 0 if i+2*m>=n: # 如果剩余堆数小于等于 2M, 那么可以全拿走 return sum(piles[i:]) best = 0 for x in range(1,m*2+1): # 剩余石子减去对方最优策略 best = max(best,sum(piles[i:])-dp(i+x,max(x,m))) memo[(i,m)] = best return best return dp(0,1)
股票
第一题是只进行一次交易,相当于 k = 1;第二题是不限交易次数,相当于 k = +infinity(正无穷);第三题是只进行 2 次交易,相当于 k = 2;剩下两道也是不限交易次数,但是加了交易「冷冻期」和「手续费」的额外条件,其实就是第二题的变种,都很容易处理。
每天都有三种「选择」:买入、卖出、无操作。
但问题是,并不是每天都可以任意选择这三种选择的,因为 sell 必须在 buy 之后,buy 必须在 sell 之后(第一次除外)。那么 rest 操作还应该分两种状态,一种是 buy 之后的 rest(持有了股票),一种是 sell 之后的 rest(没有持有股票)。而且别忘了,我们还有交易次数 k 的限制,就是说你 buy 还只能在 k > 0 的前提下操作。
这个问题的「状态」有三个,第一个是天数,第二个是当天允许交易的最大次数,第三个是当前的持有状态(即之前说的 rest 的状态,我们不妨用 1 表示持有,0 表示没有持有)。
用一个三维数组 dp 就可以装下这几种状态的全部组合,用 for 循环就能完成穷举:
dp[3][2][1] 的含义就是:今天是第三天,我现在手上持有着股票,至今最多进行 2 次交易。再比如 dp[2][3][0] 的含义:今天是第二天,我现在手上没有持有股票,至今最多进行 3 次交易。
我们想求的最终答案是 dp[n - 1][K][0],即最后一天,最多允许 K 次交易,所能获取的最大利润(此时股票全部卖出)。
这个解释应该很清楚了,如果 buy,就要从利润中减去 prices[i],如果 sell,就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 k 的限制,我们在选择 buy 的时候,把最大交易数 k 减小了 1,很好理解吧,当然你也可以在 sell 的时候减 1,一样的。
定义 base case,即最简单的情况。
把上面的状态转移方程总结一下:
121. 买卖股票的最佳时机
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
class Solution: def maxProfit(self, prices: List[int]) -> int: if len(prices)<=1: return 0 profit = 0 min_price = prices[0] for i in prices: min_price = min(i, min_price) profit = max(profit,i-min_price) return profit
122. 买卖股票的最佳时机 II
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
class Solution: def maxProfit(self, prices: List[int], fee: int) -> int: yes = -prices[0] #如果持有股票的利润 no = 0 #如果不持有的利润 for i in range(1,len(prices)): yes = max(yes,no-prices[i]) no = max(no,yes+prices[i]) return no
123. 买卖股票的最佳时机 III
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
class Solution: def maxProfit(self, prices: List[int]) -> int: n = len(prices) if n==1 or n==0: return 0 k=2 dp = [[[0,0] for j in range(k+1)] for i in range(n)] # n第n天,k,2(0 不持有股票,1 持有股票) for i in range(0,n): for j in range(k,-1,-1): if i==0: dp[i][j][0]=0 dp[i][j][1]=-prices[0] elif j==0: dp[i][j][1]=-float('inf') dp[i][j][0]=0 else: dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]+prices[i]) dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i]) #注意j的变化,买股票需要减1,卖不需要 return dp[n-1][k][0]
188. 买卖股票的最佳时机 IV
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
这题和 k = 2 没啥区别,可以直接套上一题的第一个解法。但是提交之后会出现一个超内存的错误,原来是传入的 k 值可以任意大,导致 dp 数组太大了。现在想想,交易次数 k 最多能有多大呢?一次交易由买入和卖出构成,至少需要两天。所以说有效的限制次数 k 应该不超过 n/2,如果超过,就没有约束作用了,相当于 k = +infinity,等同于122题
class Solution: def maxProfit(self, k: int, prices: List[int]) -> int: n = len(prices) if n<=1: return 0 if k>n/2: profit = 0 for i in range(1,n): tmp = prices[i]-prices[i-1] if tmp>0: profit += tmp return profit dp = [[[0,0] for j in range(k+1)] for i in range(n)] for i in range(n): for j in range(k,-1,-1): if i==0: dp[i][j][0] = 0 dp[i][j][1] = -prices[0] elif j==0: dp[i][j][0] = 0 dp[i][j][1] = -float('inf') else: dp[i][j][0] = max(dp[i-1][j][0],dp[i-1][j][1]+prices[i]) dp[i][j][1] = max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i]) return dp[n-1][k][0]
714. 买卖股票的最佳时机含手续费
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每次交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
class Solution: def maxProfit(self, prices: List[int], fee: int) -> int: yes = -prices[0] #持有 no = 0 #不持有 for i in range(1,len(prices)): yes = max(yes,no-prices[i]) no = max(no,yes+prices[i]-fee) return no
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
class Solution: def maxProfit(self, prices: List[int]) -> int: if len(prices)<=1: return 0 yes = -prices[0] no = 0 pre = 0 for i in range(1,len(prices)): temp = no #第i天选择买的时候,需要从上上次no开始状态转移 no = max(no,yes+prices[i]) yes = max(yes,pre-prices[i]) pre = temp return no
扔鸡蛋
887. 鸡蛋掉落
* 有 K 个鸡蛋,有 N 层楼,用最少的操作次数 F 检查出鸡蛋的质量。 * * 思路: * 本题应该逆向思维,若你有 K 个鸡蛋,你最多操作 F 次,求 N 最大值。 * * dp[k][f] = dp[k][f-1] + dp[k-1][f-1] + 1; * 解释: * 0.dp[k][f]:如果你还剩 k 个蛋,且只能操作 f 次了,所能确定的楼层。 * 1.dp[k][f-1]:蛋没碎,因此该部分决定了所操作楼层的上面所能容纳的楼层最大值 * 2.dp[k-1][f-1]:蛋碎了,因此该部分决定了所操作楼层的下面所能容纳的楼层最大值;
class Solution: def superEggDrop(self, K: int, N: int) -> int: dpTable = [[0] * (N+1) for _ in range(K+1)] m = 0 #m为次数 while(dpTable[K][m] < N): #线性遍历,最多N次 m += 1 for k in range(1, K+1): dpTable[k][m] = dpTable[k-1][m-1] + dpTable[k][m-1] +1 return m
斐波那契数列
10.斐波那契数列
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
示例 1:
输入:n = 2
输出:1
示例 2:
输入:n = 5
输出:5
动态规划
原理: 以斐波那契数列性质 f(n+1)=f(n)+f(n−1)为转移方程。从计算效率、空间复杂度上看,动态规划是本题的最佳解法。
状态定义: 设 dp 为一维数组,其中 dp[i]的值代表 斐波那契数列第 $i$ 个数字 。
转移方程: dp[i+1]=dp[i]+dp[i−1],即对应数列定义 f(n+1)=f(n)+f(n−1)
初始状态: dp[0]=0 dp[1]=1即初始化前两个数字;
返回值: dp[n],即斐波那契数列的第 n 个数字。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
时间复杂度 O(N) 计算 f(n) 需循环 n次,每轮循环内计算操作使用 O(1) 。
空间复杂度 O(1): 几个标志变量使用常数大小的额外空间。
由于 Python 中整形数字的大小限制 取决计算机的内存 (可理解为无限大),因此可不考虑大数越界问题。
class Solution: def fib(self,n): a = 0 b = 1 for _ in range(n): a,b = b, a+b return a % 1000000007
8 青蛙跳台阶问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入:n = 2
输出:2
示例 2:
输入:n = 7
输出:21
此类求 多少种可能性 的题目一般都有 递推性质 ,即 f(n)和 f(n−1)…f(1) 之间是有联系的。
设跳上n 级台阶有 f(n)种跳法。在所有跳法中,青蛙的最后一步只有两种情况: 跳上 1 级或 2 级台阶。
当为 1 级台阶: 剩 n−1 个台阶,此情况共有 f(n−1) 种跳法;
当为 2 级台阶: 剩 n−2个台阶,此情况共有 f(n−2)种跳法。
f(n) 为以上两种情况之和,即 f(n)=f(n−1)+f(n−2) ,以上递推性质为 斐波那契数列 。本题可转化为求斐波那契数列第 n项的值,唯一的不同在于起始数字不同。
青蛙跳台阶问题: f(0)=1 f(1)=1, f(2)=2
斐波那契数列问题: f(0)=0 f(1)=1, f(2)=1
class Solution: def numWays(self, n: int) -> int: a = 1 b = 1 for _ in range(n): a,b = b, a+b return a%1000000007
91&面试题46. 把数字翻译成字符串
一条包含字母 A-Z 的消息通过以下方式进行了编码:
'A' -> 1
'B' -> 2
...
'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。
class Solution: def numDecodings(self, s: str) -> int: n = len(s) if n==0: return 0 dp = [1,0] dp[1] = 1 if s[0]!='0' else 0 for i in range(1,n): dp.append(0) if s[i]!='0': dp[i+1] += dp[i] if s[i-1:i+1]>='10' and s[i-1:i+1]<='26': dp[i+1] += dp[i-1] return dp[n]
母牛生产
题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
第 i 年成熟的牛的数量为:
dp[i]=dp[i-1]+dp[i-3]
信件错排
题目描述:有 N 个 信 和 信封,它们被打乱,求错误装信的方式数量。
定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况:
① i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)*dp[i-2] 种错误装信方式。
② i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)*dp[i-1] 种错误装信方式。
综上所述,错误装信数量方式数量为:
dp[i] = (i-1)*dp[i-2]+(i-1)*dp[i-1]
dp[N] 即为所求。
和上楼梯问题一样,dp[i] 只与 dp[i-1] 和 dp[i-2] 有关,因此也可以只用两个变量来存储 dp[i-1] 和 dp[i-2]。