Leetcode

    技术2022-07-11  87

    70. 爬楼梯


    有n级台阶,每次有两种方法上楼(一次上一节/一次上两节)。问:有多少种方案?

    https://leetcode-cn.com/problems/climbing-stairs/

    输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶

    解题思路

    动态规划: 一个问题的最优解,可以从其子集的最优解组合而得。

    第i级台阶的方法总数=(i-1)级台阶方法总数 + (i-2)级台阶方法总数例外:n<=2 时,排除掉即可。

    时间复杂度:O(n)

    空间复杂度:O(n),需要长度为n的数组。

    代码

    class Solution(object): def climbStairs(self, n): if n == 1: return 1 if n == 2: return 2 first = 1 second = 2 res = 0 for i in range(2,n): res = first + second first = second second = res return res

    198. 打家劫舍


    链接:https://leetcode-cn.com/problems/house-robber/

    有k间房,只能隔一间偷一个。问你能偷到的最大金额。

    example:

    输入: [2,7,9,3,1] 输出: 12 解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。

    解题思路

    动态规划:

    定义子问题写出子问题的递推关系确定 DP 数组的计算顺序空间优化(可选)

    定义子问题:

    有k间房,用f(k)表示k与k之前偷窃到的最高金额。隔一间偷一间:如果偷了k,则不能偷k-1。

    子问题的递推关系:

    只有一间房时(k=1),f(1)=A(1)k = 2 时,对比A(1)与A(2)的最大值。f(2)=max(A(1),A(2))k = 3 时,对比f(3) = max(f(1)+A(3),f(2))以此类推,k间房,每间房的金额为nums[0],nums[1]…nums[k-1],f(k) = max(f(k-2)+nums[k-1],f(k-1))因为列表是从0开始索引,而房间号是从1开始索引,为了便于理解,在开头添加一个dp[0]=0,表示一间没偷时,金额总数为0。

    确定顺序:

    从下到上

    代码

    class Solution(object): def rob(self, nums): N = len(nums) if N == 0: return 0 dp = [0]*(N+1) dp[0] = 0 dp[1] = nums[0] for i in range(2,N+1): dp[i] = max(dp[i-2]+nums[i-1],dp[i-1]) return dp[N]

    因为dp[n]仅与dp[n-2]&dp[n-1]有关,可以使用old,new更新

    空间复杂度为:O(1)new,old = max(old+num,new),new 这里old更新的为new的输入值而非输出值。 class Solution: def rob(self, nums: List[int]) -> int: old,new = 0,0 for num in nums: new,old = max(old+num,new),new return new

    213. 打家劫舍 II


    链接:https://leetcode-cn.com/problems/house-robber-ii/

    有k间房,最后一间和第一间相连,只能隔一间偷一个。问你能偷到的最大金额。

    解题思路

    同198题,难点在于此题将收尾相连成为环状排列。题目要求:第一个房子和最后一个房子不能一起偷。即首尾不能同时存在。解决方法:拆环,将环装排列拆成两个单排,最后比较两个单排的大小。 偷第一个房子,列表变更为num[:-1]偷最后一个房子,列表变更为num[1:] 其他同T198 时间复杂度:O(n)空间复杂度:O(n)

    代码

    class Solution(object): def rob(self, num): if len(num) == 1: return num[0] def result(nums): N = len(nums) if N == 0: return 0 dp = [0]*(N+1) dp[0] = 0 dp[1] = nums[0] for i in range(2,N+1): dp[i] = max(dp[i-2]+nums[i-1],dp[i-1]) return dp[N] return(max(result(num[1:]),result(num[:-1])))

    因为dp[n]仅与dp[n-2]&dp[n-1]有关,可以使用old,new更新

    空间复杂度为:O(1)

    代码

    class Solution: def rob(self, nums: List[int]) -> int: if len(nums) == 1: return nums[0] def result(num): old,new = 0,0 for nums in num: new,old = max(old+nums,new),new return(new) return(max(result(nums[1:]),result(nums[:-1])))

    64. 最小路径和


    链接:https://leetcode-cn.com/problems/minimum-path-sum/

    给定非负整数的m x n网络,找到从(0,0)到(m-1,n-1)的最小路径和(从左上走到右下经过的所有数字和)

    example:

    输入: [ [1,3,1], [1,5,1], [4,2,1] ] 输出: 7 解释: 因为路径 1→3→1→1→1 的总和最小。

    解题思路

    这是一道二维的动态规划题,化二维为一维就思考每条路的走法:

    构造一个与grid尺寸相同的dp二维列表。自左上向右下,用dp[i][j]代表到达grip[i][j]的最小和,有两条路线可以到达(i,j),从左边(i-1,j)和上边(i,j-1)用min()函数比较两条线路的最小即可,注意排除边界情况。时间复杂度:O(mn)空间复杂度:O(mn)

    代码

    class Solution(object): def minPathSum(self, grid): #利用dp构造一个新的网络用于储存 m = len(grid) n = len(grid[0]) dp = [[0]*n for i in range(m)] dp[0][0] = grid[0][0] for i in range(m): for j in range(n): if i == j == 0: continue elif i == 0: dp[0][j] = grid[0][j] + dp[0][j-1] elif j == 0: dp[i][0] = grid[i][0] + dp[i-1][0] else: dp[i][j] = grid[i][j] + min(dp[i-1][j],dp[i][j-1]) return dp[m-1][n-1]

    因只与(i-1,j),(i,j-1)有关,所以可以不用构造dp,直接在原列表中更新

    时间复杂度:O(mn)空间复杂度:O(1) class Solution: def minPathSum(self, grid: List[List[int]]) -> int: m = len(grid) n = len(grid[0]) for i in range(m): for j in range(n): if i==j==0: continue elif i==0: grid[i][j] = grid[0][j-1] + grid[i][j] elif j==0: grid[i][j] = grid[i-1][0] + grid[i][j] else: grid[i][j] = min(grid[i-1][j],grid[i][j-1]) + grid[i][j] return(grid[-1][-1])

    62. 不同路径


    链接:https://leetcode-cn.com/problems/unique-paths/

    m * n大小的方格,从左上角走到右下角的路径总数。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y9CqQzln-1593597823054)(attachment:image.png)]

    解题思路

    动态规划 题目要求总路径,那就设dp[i][j]为到达[i][j]位置的总路径数,那么dp[i][j]=dp[i-1][j]+dp[i][j-1]

    时间复杂度:O(m*n)空间复杂度:O(m*n)

    代码

    class Solution(object): def uniquePaths(self, m, n): dp = [[0]*m for _ in range(n)] for i in range(n): for j in range(m): if i == 0 or j == 0: dp[i][j] = 1 else: dp[i][j] = dp[i-1][j] + dp[i][j-1] return(dp[n-1][m-1])

    303. 区域和检索 - 数组不可变


    链接:https://leetcode-cn.com/problems/range-sum-query-immutable

    给定一个整数数组nums,求出数组从索引i到j(i≤j)范围内元素的总和,包含i,j两点。

    example:

    给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange() sumRange(0, 2) -> 1 sumRange(2, 5) -> -1 sumRange(0, 5) -> -3

    解题思路

    动态规划 要求总和,就令dp[i]代表i和i以前的数据和。

    计算i~j的和就是dp[j]-dp[i-1]。为了避免i-1超出范围,改为,dp[j]-dp[i]+nums[i]。要调用nums和dp。需要使用self.nums&self.dp时间复杂度:O(n)空间复杂度:O(n)

    代码

    python class NumArray(object): def __init__(self, nums): self.nums = nums n = len(nums) dp = [0]*n sum = 0 for i in range(n): sum += nums[i] dp[i] = sum self.dp = dp def sumRange(self, i, j): return(self.dp[j]-self.dp[i]+self.nums[i])

    413. 等差数列划分


    链接:https://leetcode-cn.com/problems/arithmetic-slices/

    如果一个数列大于等于3个数,且两两差值相等,则称为等差数列。问一个任意数列有多少等差数列。

    example:

    A = [1, 2, 3, 4] 返回: 3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]

    解题思路

    动态规划:题目要求所有等差数列个数,设dp[i]表示以i位置结尾的等差数列数量(官方解答其实就是这个意思)那么整个数组A的等差数组个数就为sum[dp]

    代码

    class Solution(object): def numberOfArithmeticSlices(self, A): #以dp[i]表示到达i位置时的等差数列总和 n = len(A) if n<3: return 0 dp = [0]*n for i in range(2,n): if A[i-2]+A[i] == A[i-1]*2: dp[i] = dp[i-1]+1 return sum(dp)

    343. 整数拆分


    链接:https://leetcode-cn.com/problems/integer-break/

    将整数n拆分成多个整数,求拆分后的最大乘积。

    example:

    输入: 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

    解题思路

    动态规划:求整数n时的最大乘积,则设dp[i]为i时的最大乘积。最后输出dp[n]为什么能用动态规划? n=1,dp[1]=1n=2,dp[2]=1n=3,dp[3]=max(1x(3-1),2*(3-2))=max(1x(dp[2],(3-1)),2x(dp[1],(3-2)))n=k,dp[k]=max(1x(k-1),2x(k-2),(k-1)x(k-(k-1))))=max(1x(dp[k-1],(k-1)),2x(dp[k-2],(k-2)),(k-1)x(dp[1],1)) 由递推可得,dp[i]=max(jdp[i-j],j(i-j),dp[i])。为什么有j*(i-j)? 因为dp[i-j]的结果中无(i-j),只有1*(i-j-1)

    代码

    class Solution(object): def integerBreak(self, n): dp = [0]*(n+1) dp[1] = 1 for i in range(2,n+1): for j in range(1,i): dp[i] = max(j*dp[i-j],j*(i-j),dp[i]) return dp[n]

    279. 完全平方数


    链接:https://leetcode-cn.com/problems/perfect-squares/

    给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。找到最小的完全平方数组合,可以重复使用。

    example:

    输入: n = 13 输出: 2 解释: 13 = 4 + 9

    解题思路

    动态规划:因为要求完全平方数个数最少,设i的最少完全平方数为dp[i],用j表示完全平方数。那么dp[i]=dp[i-j]+1,因此可以使用动态规划。问题的关键是n是由哪几个完全平方数组成最小,比如12:可以是dp[8]+dp[4],也可以是dp[3]+dp[9],用square构建完全平方数列表,用j在里面循环输出dp[i]最小值。

    代码

    class Solution(object): def numSquares(self, n): dp = [float('inf')]*(n+1) dp[0] = 0 square = [i**2 for i in range(int(math.sqrt(n))+1)] for i in range(1,n+1): for j in square: if i < j: break else: dp[i] = min(dp[i],dp[i-j]+1) return dp[n]

    300. 最长上升子序列


    链接:https://leetcode-cn.com/problems/longest-increasing-subsequence/

    给定一个无序的整数数组,求其中最长递增子序列的长度。可以跳序组合子序列

    example:

    输入: [10,9,2,5,3,7,101,18] 输出: 4 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

    解题思路

    本题难点:子序列可以跳序组合的,用dp[i]=dp[j]+1解决,具体如下:时间复杂度:O(n^2),i,j两个循环空间复杂度:O(n),需要长度为n的dp 动态规划:用dp[i]表示s[i]为结尾的最长子序列长度找到[:i]中s[j]小于s[i]的最长子序列dp[j],长度+1即可。用dp[i]来缓存最长结果,直到小循环j结束。最后输出整个序列中的最大值即可 max(dp[i] for i in range(n))

    代码

    class Solution(object): def lengthOfLIS(self, nums): #因要求最长递增子序列的长度,那么dp[i]就为以i结尾的最长递增序列长度 n = len(nums) if n == 0: return 0 dp=[1]*n #对于s[i],因为可以跳序组合,所以需要j重新遍历s[:i+1]来找到dp[i],只要s[i]>s[j],dp[i]=dp[j]+1 for i in range(1,n): for j in range(i): if nums[i]>nums[j]: dp[i] = max(dp[i],dp[j]+1) return max(dp[i] for i in range(n))

    646. 最长数对链


    链接:https://leetcode-cn.com/problems/maximum-length-of-pair-chain/

    列表pairs由n个数对构成。数对由两个数组成,且第一位小于第二位。对于[[a,b],[c,d]],若b<c,则可组成数对链。求最长数对链长度

    example:

    输入: [[1,2], [2,3], [3,4]] 输出: 2 解释: 最长的数对链是 [1,2],[3,4]

    解题思路

    本题难点:数对是无序的需要排序,其他同第300题时间复杂度:O(n^2),有i,j两个最长为n的循环空间复杂度:O(n),长度为n的dp 动态规划:用dp[i]表示第i位时的最长对数链。j在[:i]中小循环,只要dp[i]的第一位大于dp[j]的第二位,对链长度就可以增加,用dp[i]来做缓存。最后输出max dp[i]即可

    代码

    class Solution(object): def findLongestChain(self, pairs): n = len(pairs) dp = [1]*n #先将数对以0位进行排序 pairs.sort(key=lambda x:x[0]) #dp[i]表示第i对的最长数对链 for i in range(1,n): for j in range(i): if pairs[i][0]>pairs[j][1]: dp[i]=max(dp[i],dp[j]+1) return max(dp[i] for i in range(n))

    背包问题

    背包问题实际上也只是动态规划中的一类

    题目求什么,dp含义就是什么背包问题中dp[i][j]是由dp[i-1][?]决定的?代表如何使用nums[i]可以达到dp[i][j]。状态转移方程通过思考这一步得出。

    1143. 最长公共子序列


    链接:https://leetcode-cn.com/problems/longest-common-subsequence/

    两个字符串text1&text2。求最长公共子序列长度(LCS)

    example:

    输入:text1 = "abcde", text2 = "ace" 输出:3 解释:最长公共子序列是 "ace",它的长度为 3。

    解题思路

    最长公共子序列(lcs)是一道二维动态规划问题,难点在于有两个变量i,j在同时变化,不好理解与思考状态转移方程。解决方法,思考时,把二维想成一维,即固定text1的i,让j去text1[:i]&text2中找最长公共子序列。text1[i]=text2[j]时,dp[i][j]=dp[i-1][j-1]+1text1[i]!=text2[j]时,就要比较最大值是在text1[:i]中,还是text[:i-1]中,dp[i][j]=max(dp[i-1][j],dp[i][j-1])状态转移方程: 时间复杂度:O(m*n),两个循环空间复杂度:O(m*n),dp是(m,n)大小

    代码

    class Solution(object): def longestCommonSubsequence(self, text1, text2): n,m = len(text1),len(text2) dp = [[0]*(m+1) for _ in range(n+1)] for i in range(1,n+1): for j in range(1,m+1): if text1[i-1]==text2[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[n][m])

    416. 分割等和子集


    链接:https://leetcode-cn.com/problems/partition-equal-subset-sum/

    能否整分正数组

    example:

    输入: [1, 5, 11, 5] 输出: true 解释: 数组可以分割成 [1, 5, 5] 和 [11]

    解题思路

    动态规划 + 01背包问题

    dp[i][j]表示:[0:i]中的数,是否存在和为j,最终dp[-1][-1]为即表示是否存在和为sum/201背包问题理解时把所有情况的图画出来即可。借鉴这个大哥的图

    思路看这个大哥

    代码

    class Solution(object): def canPartition(self, nums): sums,n = sum(nums),len(nums) if sums % 2 == 1: return False target = sums/2 dp = [[False]*(target+1) for _ in range(n)] nums.sort() for i in range(n): for j in range(nums[i],target+1): if nums[i]==j: dp[i][j]=True else: dp[i][j]=dp[i-1][j] or dp[i-1][j-nums[i]] return dp[-1][-1]

    494.目标和


    链接:https://leetcode-cn.com/problems/target-sum/

    数组nums为非负整数组,每个使用一次,通过在数前添加+/-号使结果为S,求方法数量

    example:

    输入: 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。

    解题思路

    思路参考来源于解答动态规划:题意求方法数,所以dp[i][j]表示从0-i数组中可以通过加减得到结果j的方法数量状态转移方程: 对于j如果nums[i]使用+号,则dp[i][j]=dp[i-1][j-nums[i]]=r的方法数量对于j如果nums[i]使用-号,则dp[i][j]=dp[i-1][j+nums[i]]=l的方法数量所以综上,dp[i][j]方法总数为l+r,即:dp[i][j] = dp[i-1][j-nums[i]]+dp[i-1][j+nums[i]] 目标位置:dp[n-1][sums+S]

    代码

    class Solution(object): def findTargetSumWays(self, nums, S): n = len(nums) sums = sum(nums) if sums<S: return 0 dp = [[0]*(2*sums+1) for _ in range(n)] #第一行初始化,需要注意的是+,-有可能指向同一个数,所以使用叠加+=1而不是赋值=1 dp[0][sums-nums[0]]+=1 dp[0][sums+nums[0]]+=1 for i in range(1,n): for j in range(2*sums+1): l = dp[i-1][j-nums[i]] if 0<=j-nums[i]<(2*sums+1) else 0 r = dp[i-1][j+nums[i]] if 0<=j+nums[i]<(2*sums+1) else 0 dp[i][j] = l + r return dp[n-1][sums+S]

    474.一和零


    链接:https://leetcode-cn.com/problems/ones-and-zeroes/

    有m个0,n个1,和k个字符串,求最多能拼出多少个字符串。

    example:

    输入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3 输出: 4 解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。

    解题思路

    两个背包,0背包和1背包,字符串会占用两个背包的空间,受益是1。

    动态规划:求能拼出字符串最大数量,所以dp[i][j]就为在当前字符串,用i个0,j个1能拼出字符串的最大个数状态转移方程:dp[k][i][j]=max(dp[k-1][i][j],1+dp[k-1][i-count_0[k]][j-count_1[k]]),k表示第k个字符串,count_0[k]为k字符串需要的0数量。状态转移方程优化: 因dp[k][i][j]仅与dp[k-1]有关,故优化为2维如下: dp[i][j]=max(dp[i][j],1+dp[i-count_0][j-count_1])i,j遍历需逆序,以i为例子:for i in range(m,count_0 -1,-1): 逆序原因:如若正序,dp[i][j]迭代的是dp[k][i][j]而非dp[k-1][i][j],反向不存在这个问题。在count_0处截止原因:小于count_0时,i-count_0<0,out of range且没意义。 时间复杂度:O(mnk)空间复杂度:O(m*n)

    代码

    class Solution(object): def findMaxForm(self, strs, m, n): dp=[[0]*(n+1) for _ in range(m+1)] for item in strs: count_0 = item.count("0") count_1 = item.count("1") for i in range(m,count_0 -1,-1): for j in range(n,count_1 -1,-1): dp[i][j] = max(dp[i][j],1+dp[i-count_0][j-count_1]) return dp[m][n]

    322.零钱兑换


    链接:https://leetcode-cn.com/problems/coin-change/

    多种面额硬币coins,每种不限数量,组成总金额amount。求所需最小硬币数。

    example:

    输入: coins = [1, 2, 5], amount = 11 输出: 3 解释: 11 = 5 + 5 + 1

    解题思路

    1. 动态规划:

    求用coins使用数量最小值,那就设dp[i][j]代表使用[0,i]个币种组成j的最小数量。

    2. 状态转移方程:

    dp[i][j]=min(dp[i-1][j],1+dp[i-1][j-coins[i],1+dp[i][j-coins[i]]])

    dp[i-1][j]:不使用coins[i]。1+dp[i-1][j-coins[i]]:使用coins[i],且j-coins[i]在i-1处取得最小1+dp[i][j-coins[i]]: 使用coins[i],且j-coins[i]在i处取得最小

    3. 优化:

    对于2.2,2.3如果对coins从小到大排序,则j-coins[i]只会出现在i处,即:

    dp[i][j]=min(dp[i-1][j],1+dp[i][j-coins[i]])

    在(0,coins[i])区间,因为j-coins[i]<0,所以最小值dp[i][j]就是dp[i-1][j]的值.因为dp[i][j]只与dp[i-1]&dp[i]有关,且这里的dp[i-1]行与dp[i]行值相等。所以j采用顺序遍历时,dp[i][j]仅与dp[i]有关,即可以降维为一维数组:(如果j采用逆序如474题,则dp是从i-1行遍历过来,注意区分)

    dp[j]=min(dp[j],1+dp[j-coins])

    4. 时间复杂度:O(N*amount),N为coins种类

    5. 空间复杂度:O(amount)

    代码

    class Solution(object): def coinChange(self, coins, amount): n = len(coins) coins.sort() dp=[float("inf")]*(amount+1) dp[0]=0 for cost in coins: for j in range(cost,amount+1): dp[j]=min(dp[j],dp[j-cost]+1) return dp[amount] if dp[amount]!=float("inf") else -1

    518.零钱兑换二


    链接:https://leetcode-cn.com/problems/coin-change-2

    不同面额钱币coins,组成金额amount的组合数有多少

    example:

    输入: amount = 5, coins = [1, 2, 5] 输出: 4 解释: 有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1

    解题思路

    动态规划,同T322,采用顺序优化方法降维。

    动态规划:题目求组合数,设dp[i][j]表示使用前i种硬币组成j金额的组合数。画图: 在(0,coins[i]),dp[i,j]=dp[i-1][j];在(coins[i],amount+1),dp[i][j]=dp[i-1][j]+dp[i][j-coins[i]]。状态转移方程 dp[i][j] = dp[i-1][j] + dp[i][j-coins[i]]优化 dp[i][j-coins[i]]实际上是由dp[i-1][j]得来的,也就是说dp[i][j]只与dp[i-1]有关。故可降维为1维。因为dp[i][j-coins[i]]是由更新后的dp[i][j]的计算的,所以j使用顺序dp[j] = dp[j] + dp[j-coins] 时间复杂度:O(n*amount),n为coins的种类数空间复杂度:O(amount)

    代码

    class Solution(object): def change(self, amount, coins): dp = [0]*(amount+1) dp[0] = 1 for cost in coins: for j in range(cost,amount+1): dp[j] = dp[j] + dp[j-cost] return dp[amount]

    139.单词拆分


    链接:https://leetcode-cn.com/problems/word-break/

    非空字符串s是否可以被空格拆分成字典wordDict中的值。

    example:

    输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。

    解题思路

    动态规划:dp[i]表示s前i位是否可以被空格拆分成wordDict中的元素。状态转移方程:dp[i]=True表示前i位可满足题意,s[i:j]也在字典中则dp[j]=True时间复杂度:O(n^2)空间复杂度:O(n)

    代码

    class Solution(object): def wordBreak(self, s, wordDict): 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]

    377. 组合总和 Ⅳ


    链接:https://leetcode-cn.com/problems/combination-sum-iv/

    nums为不重复的正整数数组,target为目标和,nums有多少种组合方式(不同排序算作不同组合)

    example:

    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) 请注意,顺序不同的序列被视作不同的组合。 因此输出为 7。

    解题思路

    动态规划:这题我认为就是蚂蚁上楼梯问题,只不过又多了几种上楼的方式罢了。难点:乍一看,难点在于存在排列组合问题。 如4=2+2的组合中,虽然是2对已有排序dp[2]的插空: {1,1,2},{1,2,1},{2,1,1},{2,2} 但实际上: {1,2,1},{2,1,1}属于dp[3]类。状态转移方程:以nums=[1,2,3],target=4为例,dp[4]=dp[1]+dp[2]+dp[3]时间复杂度:O(n*m),每一个i都要遍历n个数组,一共要遍历n次。n为数组个数,m为target+1空间复杂度:O(m),dp占用一个长为target+1的空间

    代码

    class Solution(object): def combinationSum4(self, nums, target): dp = [0]*(target+1) dp[0]=1 for i in range(1,target+1): for j in nums: if i>=j: dp[i] += dp[i-j] return dp[target]

    股票交易问题

    股票交易共六种题型,设dp[i][k][0 or 1]表示第i天股票是持有(用1表示),还是不持有(用0表示),第i天还有k次交易次数 初始化:

    dp[-1][k][0] = dp[i][0][0] = 0 ,第0天不持有or不允许交易当然利润为0dp[-1][k][1] = dp[i][0][1] = float(’-inf’) 第0天就持有or无交易次数就持有当然不存在,设为负无穷

    状态转移方程:

    dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

    309. 最佳买卖股票时机(含冷冻期)


    链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/

    股票每天有买入buy,卖出sell,不操作rest三种操作,求第i天最大利润。(卖出后需要间隔一天才能买入,冷冻期)

    example:

    输入: [1,2,3,0,2] 输出: 3 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

    解题思路

    动态规划+股票交易+冷冻:股票交易共六种题型,设dp[i][0 or 1]表示第i天是持有(1)股票,还是不持有(0)。状态转移方程: dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i]) 第i天不持有:(1)i-1天就不持有,(2)i-1天持有,第i天卖出获得prices[i]利润dp[i][1] = max(dp[i-1][1],dp[i-2][0]-prices[i]) 第i天持有:(1)i-1天就持有(2)i-2天没持有(有一天冷冻期),第i天买入,扣除prices[i]成本 初始化:状态转移方程有i-2&i-1,要把这两天设为特殊情况 dp[0][0]=dp[1][0]=0, 第0天,第1天都没持有利润当然是0。dp[0][1]=dp[1][1]= -prices[1],第0天不可能持有,可以设为float("-inf"),当然设成-prices[1]刚好不影响dp[1][1]也是可以的。注意:因为dp多了第0天但prices没有,所以i循环时,dp[i]对应的是prices[i-1]当天价格 时间复杂度:O(n) ,只用到一个循环,n是交易天数空间复杂度:O(n) ,dp占用了2*n的空间,所以是O(n)

    代码

    class Solution(object): def maxProfit(self, prices): n = len(prices) if n<2: return 0 dp = [[0]*2 for _ in range(n+1)] dp[0][0] = dp[1][0] = 0 dp[0][1] = dp[1][1] = -prices[0] for i in range(2,n+1): dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i-1]) dp[i][1]=max(dp[i-1][1],dp[i-2][0]-prices[i-1]) return dp[n][0]

    714.买卖股票的最佳时机(含手续费)


    链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee

    股票每天有买入buy,卖出sell,不操作rest三种操作,求第i天最大利润。在卖出时收取一次手续费fee,卖出后才可买入。

    example:

    输入: prices = [1, 3, 2, 8, 4, 9], fee = 2 输出: 8 解释: 能够达到的最大利润: 在此处买入 prices[0] = 1 在此处卖出 prices[3] = 8 在此处买入 prices[4] = 4 在此处卖出 prices[5] = 9 总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

    解题思路

    动态规划+股票交易+手续费:dp[i][0 or 1]表示第i天是否持有股票时的最大利润状态转移方程: dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i-1]-fee) ,第i天未持有,(1)i-1天就未持有,(2)i-1天持有,第i天卖出,需要交手续费dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i-1]) ,第i天持有,(1)i-1天就持有,(2)i-1天未持有,第i天买入 初始化 状态转移方程涉及dp[i-1],故当i=0天时,dp[0][0]利润肯定未0,dp[0][1]还未买入就有股票,情况不存在设为负无穷float(’-inf’) 时间复杂度:O(n) ,循环n次,n为天数。空间复杂度:O(n) ,dp占用2n的空间

    代码

    class Solution(object): def maxProfit(self, prices, fee): n = len(prices) dp = [[0]*2 for _ in range(n+1)] dp[0][0],dp[0][1]=0,float('-inf') for i in range(1,n+1): dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i-1]-fee) dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i-1]) return dp[n][0] 优化 因dp[i]仅与dp[i-1]有关,故可降维。dp[0] = max(dp[0],dp[1]+prices[i-1]-fee)dp[1] = max(dp[1],dp[0]-prices[i-1])时间复杂度:O(n),空间复杂度:O(1),只占用了常数位2的空间

    代码

    class Solution(object): def maxProfit(self, prices, fee): n = len(prices) dp = [0,float('-inf')] for i in range(1,n+1): dp[0] = max(dp[0],dp[1]+prices[i-1]-fee) dp[1] = max(dp[1],dp[0]-prices[i-1]) return dp[0]

    123.买卖股票的最佳时机三(最多完成k=2笔交易)


    链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/

    股票每天有买入buy,卖出sell,不操作rest三种操作,求第i天最大利润。最多完成两笔交易,(买入卖出一起算一笔)

    example:

    输入: [3,3,5,0,0,3,1,4] 输出: 6 解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。 在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。

    解题思路

    动态规划+股票交易+k=2:dp[i][k][0 or 1]表示第i天,最多可完成k笔交易时,是(1)否(0)持有股票时,的最大利润。状态转移方程: dp[i][k][0]=max(dp[i-1][k][0],dp[i-1][k][1]+prices[i]) ,第i天未持有:(1)第i-1天未持有,(2)第i-1天持有,第i天卖出,赚钱第i天利润dp[i][k][1]=max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i]) ,第i天持有:(1)第i-1天持有,(2)第i-1天未持有,第i天买入,每次买入算做一次交易,成本为prices[i]k从1开始,因为dp[i][0][0 or 1]表示最多可进行0笔交易,不论什么时候结果都是0 初始化 dp[0][k][1]=dp[1][k][1]第0天不可能有一次交易,设为负无穷float("-inf") 时间复杂度:O(2n) ,每一天都要判断k=2次,一共n天空间复杂度:O(2kn) ,dp一共要占用这么2kn个空间

    代码

    class Solution(object): def maxProfit(self, prices): n = len(prices) if n<1: return 0 dp = [[[0]*2 for _ in range(3)] for _ in range(n+1)] for k in range(3): dp[0][k][1] = float('-inf') for i in range(1,n+1): for k in range(1,3): dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k][1]+prices[i-1]) dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i-1]) return dp[n][2][0]

    6. 卖出时算做交易

    上面是在买入时减少k,如果直接修改成dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k-1][1]+prices[i-1]),结果不对,原因在于若买入不消耗交易次数,则可以在k=0时买入,需要对k=0多做一步初始化。其他不变,循环内部修改如下即可。 for i in range(1,n+1): for k in range(3): dp[i][0][0] = 0 dp[i][k][0] = max(dp[i-1][k][0],dp[i-1][k-1][1]+prices[i-1]) dp[i][k][1] = max(dp[i-1][k][1],dp[i-1][k][0]-prices[i-1]) return dp[n][2][0]

    7. 优化

    * dp[i]仅与dp[i-1]有关,可降维 * dp[k][0] = max(dp[k][0],dp[k][1]+prices[i-1]) * dp[k][1] = max(dp[k][1],dp[k-1][0]-prices[i-1]) * 时间复杂度:O(2n),空间复杂度:O(1),只需要6的常数空间。

    代码

    class Solution(object): def maxProfit(self, prices): n = len(prices) dp = [[0]*2 for _ in range(3)] for k in range(3): dp[k][1] = float('-inf') for i in range(1,n+1): for k in range(1,3): dp[k][0] = max(dp[k][0],dp[k][1]+prices[i-1]) dp[k][1] = max(dp[k][1],dp[k-1][0]-prices[i-1]) return dp[2][0]

    188.买卖股票的最佳时机 IV(指定k)


    链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/

    股票每天有买入buy,卖出sell,不操作rest三种操作,求第i天最大利润。最多完成k笔交易,(买入卖出一起算一笔)

    example:

    输入: [2,4,1], k = 2 输出: 2 解释: 在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

    解题思路

    动态规划+股票交易+指定k:本题结构同T123,只是k由2变为任意值。 本题还存在k=infinite无穷的情况,同T714,因为一笔交易包含一买一卖两天操作,所以k<=n/2;当k>=n/2时,可看做k=float(‘inf’)状态转移方程 dp[i][k1][0] = max(dp[i-1][k1][0],dp[i-1][k1][1]+prices[i-1]) dp[i][k1][1] = max(dp[i-1][k1][1],dp[i-1][k1-1][0]-prices[i-1]) 初始化 dp[0][k][1]=float(’-inf’) ,第0天,不论k取和值,都不存在持有的情况时间复杂度:O(kn) ,每天都要遍历所有的k,每次有o or 1 两种情况,一共2*k*n空间复杂度:O(kn) ,dp占用2*k*n的三位空间

    代码

    class Solution(object): def maxProfit(self, k, prices): n = len(prices) #k>2/n时,相当于k=float('inf') def maxProfit_infk(prices): dp = [[0]*2 for _ in range(n+1)] dp[0][1]=float('-inf') for i in range(1,n+1): dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i-1]) dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i-1]) return dp[n][0] if k>n/2: return maxProfit_infk(prices) dp = [[[0]*2 for _ in range(k+1)] for _ in range(n+1)] for k1 in range(k+1): dp[0][k1][1] = float('-inf') for i in range(1,n+1): for k1 in range(1,k+1): dp[i][k1][0] = max(dp[i-1][k1][0],dp[i-1][k1][1]+prices[i-1]) dp[i][k1][1] = max(dp[i-1][k1][1],dp[i-1][k1-1][0]-prices[i-1]) return dp[n][k][0]

    6.优化

    * dp[i]仅与dp[i-1]有关,3维降2维 * dp[k1][0] = max(dp[k1][0],dp[k1][1]+prices[i-1]) * dp[k1][1] = max(dp[k1][1],dp[k1-1][0]-prices[i-1]) * 时间复杂度:O(nk),空间复杂度(2k)

    代码

    class Solution: def maxProfit(self, k, prices): n = len(prices) def maxProfit_infk(prices): dp = [0]*2 dp[1]=float('-inf') for i in range(1,n+1): dp[0]=max(dp[0],dp[1]+prices[i-1]) dp[1]=max(dp[1],dp[0]-prices[i-1]) return dp[0] if k>n/2: return maxProfit_infk(prices) dp = [[0]*2 for _ in range(k+1)] for k1 in range(k+1): dp[k1][1] = float('-inf') for i in range(1,n+1): for k1 in range(1,k+1): dp[k1][0] = max(dp[k1][0],dp[k1][1]+prices[i-1]) dp[k1][1] = max(dp[k1][1],dp[k1-1][0]-prices[i-1]) return dp[k][0]

    583. 两个字符串的删除操作


    链接:https://leetcode-cn.com/problems/delete-operation-for-two-strings/

    给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

    example

    输入: "sea", "eat" 输出: 2 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"

    解题思路

    动态规划:dp[i][j]表示word1前i位&word2前j位找到最长公共子串的最小步数。状态转移方程: 如果word1[i-1]=word2[j-1],则不会增加步数,dp[i][j]=dp[i-1][j-1]如果word1[i-1]!=word2[j-1],则要步数需+1,删word1还是word2取决于min(dp[i-1][j],dp[i][j-1]) 时间复杂度:O(m*n),每次遍历word2的m个字符,一共循环word1的长度n次。空间复杂度:O(m*n),dp占用m*n个空间大小,确切来说是(m+1)*(n+1)

    代码

    class Solution(object): def minDistance(self, word1, word2): n,m = len(word1),len(word2) dp = [[0]*(m+1) for _ in range(n+1)] for i in range(n+1): for j in range(m+1): if i==0 or j==0: dp[i][j]=i+j elif word1[i-1]==word2[j-1]: dp[i][j]=dp[i-1][j-1] else: dp[i][j]=min(dp[i-1][j],dp[i][j-1])+1 return dp[n][m]

    72. 编辑距离


    链接:https://leetcode-cn.com/problems/edit-distance/

    两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。操作有:删除,替换,增加。

    解题思路

    动态规划:题目求什么设什么,dp[i][j]表示word1[:i]替换成word2[:j]所需最小步数。状态转移方程: word1[i]=word2[j],不需执行操作,dp[i][j]=dp[i-1][j-1]word1[i]!=word2[j],执行删除时为dp[i-1][j]+1,替换时为dp[i-1][j-1]+1,增加时为dp[i][j-1]+1,比小就完了 时间复杂度O(n*m),两个循环空间复杂度O(n*m),dp大小为(n+1)*(m+1)

    代码

    class Solution(object): def minDistance(self, word1, word2): n,m = len(word1),len(word2) dp = [[0]*(m+1) for _ in range(n+1)] for i in range(n+1): for j in range(m+1): if i==0 or j==0: dp[i][j]=i+j elif word1[i-1]==word2[j-1]: dp[i][j]=dp[i-1][j-1] else: dp[i][j]=min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1 return(dp[n][m])

    650. 只有两个键的键盘


    链接:https://leetcode-cn.com/problems/2-keys-keyboard/

    最开始只有一个字符 A,问需要多少次操作能够得到 n 个字符 A,每次操作可以复制当前所有的字符,或者粘贴。

    example:

    输入: 3 输出: 3 解释: 最初, 我们只有一个字符 'A'。 第 1 步, 我们使用 Copy All 操作。 第 2 步, 我们使用 Paste 操作来获得 'AA'。 第 3 步, 我们使用 Paste 操作来获得 'AAA'

    解题思路

    动态规划:dp[i]表示打印i个‘A’所需最小步数,另用j表示当前粘贴板中的数。状态转移方程:i有可能全部是1组成称为素数,也可能是合数可进行分解,如8=2*4=4*2=1*8,则dp[8]=min(dp[2]+4,dp[4]+2,8) 如果i是素数,则dp[i]=i如果i是合数,则dp[i]=min(dp[j]+ i/j,minCount),这里的minCount用来迭代找到dp[i]最小时的minCount 空间复杂度O(n),时间复杂度:O((1+n)*n/2)=O(n^2)

    代码

    class Solution(object): def minSteps(self, n): dp=[0]*(n+1) for i in range(2,n+1): minCount=i for j in range(1,i): if i % j == 0: minCount = min(dp[j] + i/j , minCount) dp[i]=minCount return dp[n]

    5454. 统计全 1 子矩形_第196场周赛第3题


    链接:https://leetcode-cn.com/problems/count-submatrices-with-all-ones/

    给你一个只包含 0 和 1 的 rows * columns 矩阵 mat ,请你返回有多少个 子矩形 的元素全部都是 1

    example:

    输入:mat = [[1,0,1], [1,1,0], [1,1,0]] 输出:13 解释: 有 6 个 1x1 的矩形。 有 2 个 1x2 的矩形。 有 3 个 2x1 的矩形。 有 1 个 2x2 的矩形。 有 1 个 3x1 的矩形。 矩形数目总共 = 6 + 2 + 3 + 1 + 1 = 13 。

    解题思路

    前言:做这题思路一直绕在了动态规划里面,想着dp[i][j]表示到mat[i][j]时的最多子矩形个数。结果推出了状态转移方程为: dp[i][j]=dp[i][j-1]+dp[i-1][j]-dp[i-1][j-1]+tmp

    tmp表示以mat[i][j]为右下顶点的子矩形个数。试过各种方法永远绕不开tmp的求法,显然本题的难点就是: 如何求得以mat[i][j]为右下顶点的子矩形个数

    思路转变:如果将每个mat[i][j]为右下顶点的矩形个数进行求和,在mat[-1][-1]时,即可输出结果。(反正tmp这个东西必求,何必再用个dp)

    求以mat[i][j]为右下顶点的子矩形个数难点解答,代码中相应注释:

    在第i行时,子矩形个数与该行以[i][j]结尾的连续1个数相等。在由i-1遍历至0行时,子矩形个数与之前行的最短连续1个数相等,用一个##left来更新最短边界##。

    时间复杂度:算不清,空间复杂度:O(n*m)

    这个解答汇总很棒

    代码

    class Solution(object): def numSubmat(self, mat): n,m = len(mat),len(mat[0]) ans = 0 #保存子矩形个数 for i in range(n): for j in range(m): if mat[i][j]==1: left,top = -1,-1 y = i while y > top: #从mat[i][j]向0行迭代 x = j while x > left: #从mat[i][j]向0列迭代 if mat[y][x]==0: left = x #更新左边界 else: ans += 1 x -= 1 y -= 1 return ans
    Processed: 0.014, SQL: 9