HOT100(八)动态规划

1、爬楼梯

①动态规划

(1)时间复杂度 O(n) ,空间复杂度 O(n)的做法

开辟一个长度为 n+1 的状态数组f,f[i]表示走到第i个台阶的方案数。初始化f[0]=1(在台阶底部,不需要移动也视为一种方法),f[1]=1(走到台阶1的方案只有一种,就是爬一步)。

爬楼梯的状态转移公式是f[i]=f[i-1]+f[i-2],因为走到第i个台阶,必然是从第i-1个台阶或者第i-2个台阶上爬上来的,因此走到第i个台阶的方案数等于走到第i-1个台阶的方案数与走到第i-2个台阶的方案数之和。

最后返回f(n)就是爬到n级阶梯的方案总数。

class Solution:
    def climbStairs(self, n: int) -> int:
        if n<=1:
            return 1
        f=[0]*(n+1)
        f[0],f[1]=1,1
        for i in range(2,n+1):
            f[i]=f[i-1]+f[i-2]
        return f[n]

(2)时间复杂度 O(n) ,空间复杂度 O(1)的做法

采用滚动数组思想,将空间复杂度优化到O(1)。

ab 分别存储了到达当前台阶前的两个状态的爬法数量。循环每次迭代时,ab 依次滚动更新,使得 a 总是 b 的前一个状态,而 b 总是当前状态

class Solution:
    def climbStairs(self, n: int) -> int:
        if n<=1:
            return n
        a,b=1,1
        for i in range(2,n+1):
            a,b=b,a+b
        return b

②爬楼梯进阶

题目描述:给定n阶台阶,一次可以跳1到n阶,计算有多少种不同的方法可以从地面跳到第n阶台阶。

(1)时间复杂度 O(n²)的做法

dp[i]=dp[i-1]+dp[i-2]+...+dp[1]+dp[0]

class Solution:
    def climbStairs(self, n: int) -> int:
        if n<=1:
            return 1
        dp=[0]*(n+1)
        dp[0]=1
        for i in range(1,n+1):
            for j in range(i):
                dp[i]+=dp[j]
        return dp[n]

(2)优化后时间复杂度O(n)的做法

dp[i]=dp[i-1]+dp[i-2]+...+dp[1]+dp[0],而dp[i-1]=dp[i-2]+...+dp[1]+dp[0],所以可以得到dp[i]=dp[i-1]*2。

def climbStairs(n: int) -> int:
    dp = [1] * (n + 1)
    for i in range(2, n + 1):
        dp[i] = 2 * dp[i - 1]
    return dp[n]

也可以维护一个total_sum变量记录到目前为止的累积和。

def climbStairs(n: int) -> int:
        dp=[0]*(n+1);dp[0]=1;dp[1]=1
        total_sum=dp[0]+dp[1]
        for i in range(2,n+1):
            dp[i]=total_sum
            total_sum+=dp[i]
        return dp[n]

2、杨辉三角

左上角和右上角同时有元素的元素状态转移式:c[i][j]=c[i−1][j−1]+c[i−1][j] 。

class Solution:
    def generate(self, numRows: int) -> List[List[int]]:
        c=[[1]*(i+1) for i in range(numRows)]
        for i in range(2,numRows):
            for j in range(1,i):
                c[i][j]=c[i-1][j-1]+c[i-1][j]
        return c

3、打家劫舍

①数组存储

维护一个状态数组dp,dp[i]表示打劫前i个房子所能获得的最大收益数。

由于打劫了当前房子就不能打劫邻近的房子,因此状态转移方程如下所示:

dp[i]=max(dp[i-1],dp[i-2]+nums[i])

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

②滚动数组

用dp[i-1]更新r,用dp[i]更新p,滚动下去p=dp[n]就是最后答案。

class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:
            return 0
        n=len(nums)
        if n==1:
            return nums[0]
        r=nums[0]
        p=max(nums[0],nums[1])
        for i in range(2,n):
            r,p=p,max(p,r+nums[i])
        return p

4、完全平方数

  • 状态转移数组定义:维护一个数组dp,dp[i]表示和为i的最少平方数数量。

  • 状态转移方程:dp[i]=min(dp[i],dp[i−j²]+1),这里的 dp[i-j^2] 表示去掉一个完全平方数j²后,剩下的数的最小完全平方数数量,加上 1 是因为用了一个j²。

  • 初始化:因为找到是最少数量,所以初始化dp中元素初值为float('inf'),而根据题意可知,0不参与完全平方数的构建,因此dp[0]=0。

  • 遍历数组:外层循环遍历1到n,内层循环遍历1到n的开方。

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

5、零钱兑换

  • 状态数组:维护一个状态数组dp,dp[i]记录了组成金额i所需的最少金额数。
  • 状态转移方程:dp[i]=min(dp[i],dp[i-coin]+1)。
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if amount==0:
            return 0
        dp=[float('inf')]*(amount+1)
        dp[0]=0
        for i in range(1,amount+1):
            for coin in coins:
                if i-coin>=0:
                    dp[i]=min(dp[i],dp[i-coin]+1)
        return dp[amount] if dp[amount]!=float('inf') else -1

6、单词拆分

首先将wordDict转换成集合,因为在集合中查找的效率更高些。

  • 定义状态: 我们定义一个布尔数组 dp,其中 dp[i] 表示前 i 个字符的子字符串 s[0:i] 是否可以由 wordDict 中的单词拆分。

  • 状态转移方程: 对于每个 i,需要检查在 s[0:i] 之前的每一个分割点 j,如果 dp[j]True,且 s[j:i] 在wordSet 中,那么 dp[i] 就可以被置为 True,表示可以拆分成合法的单词组合:dp[i]=dp[j]∧(s[j:i]∈wordSet)。

  • 初始化:dp[0] = True,表示空字符串可以被成功拆分。

  • 结果: 最终 dp[n] 就表示整个字符串是否可以被成功拆分。

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        n=len(s)
        wordSet=set(wordDict)
        dp=[False]*(n+1)
        dp[0]=True
        for i in range(1,n+1):
            for j in range(i):
                if dp[j] and s[j:i] in wordSet:
                    dp[i]=True
                    break
        return dp[n]

7、最长递增子序列

①动态规划

  • 定义状态: 我们使用一个数组 dp,其中 dp[i] 表示以第 i 个元素结尾的最长递增子序列的长度。

  • 状态转移方程: 对于每个元素 nums[i],我们遍历它之前的所有元素 nums[j]j < i),如果 nums[i] > nums[j],则表示 nums[i] 可以接在 nums[j] 后面构成递增子序列,因此:dp[i]=max(dp[i],dp[j]+1)。其中 dp[j] 是以 nums[j] 结尾的最长递增子序列的长度,加上 1 表示再加上当前元素 nums[i]

  • 初始化: 每个元素都至少可以作为一个长度为 1 的子序列,因此 dp 数组初始化为全 1。

  • 结果: 最终答案是 dp 数组中的最大值,即最长递增子序列的长度。

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        n=len(nums)
        dp=[1]*n
        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)

②贪心+二分查找

维护一个序列d来存储当前得到的最大递增子序列。

让序列 d 尽可能保持递增,并且在可以替换的情况下,优先用较小的值来替换 d 中的某个元素,这样就有更多机会在未来找到更长的递增子序列。

具体如下:

每次遍历数组时,考虑当前数字 nums[i]

  • 如果 nums[i]比序列d中最后一个元素 d [-1]还大,就把 nums[i]加入 d,增长序列d。
  • 否则,我们在序列 d 中找到第一个大于等于 n 的元素n 替换它这个操作是为了尽可能地保持较小的值,从而增加后续的递增潜力

比如说,nums=[1,4,2,3,5]。初始d=[],d=[1],d=[1,4],d=[1,2],d=[1,2,3],d=[1,2,3,5]。

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        n=len(nums)
        ans=[nums[0]]
        def bin_search(num,ans):
            l,r=0,len(ans)-1
            while l<r:
                mid=(l+r)//2
                if num<=ans[mid]:
                    r=mid
                else:
                    l=mid+1
            return l
        for i in range(1,n):
            if nums[i]>ans[-1]:
                ans.append(nums[i])
            else:
                pos=bin_search(nums[i],ans)
                ans[pos]=nums[i]
        return len(ans)

8、乘积最大子数组

由于负数乘积可能使得结果反转为正数,因此在处理乘积问题时,除了维护当前的最大值,还需要同时维护当前的最小值(因为负数乘以负数可能会变成正数)。

维护三个变量max_product、min_product、max_global,分别记录当前以 i 结尾的子数组的最大乘积、当前以 i 结尾的子数组的最小乘积和全局最大乘积。

 遍历数组中每个元素:

  • 当前元素为负数:max_product和min_product交换一下,因为负数乘上负数是正值。
  • 当前元素不为负:比较、更新max_production、min_production和max_product。
class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        max_res=max_product=min_product=nums[0]
        n=len(nums)
        for i in range(1,n):
            if nums[i]<0:
                max_product,min_product=min_product,max_product
            max_product=max(nums[i],nums[i]*max_product)
            min_product=min(nums[i],nums[i]*min_product)
            max_res=max(max_res,max_product)
        return max_res

9、分割等和子集

整体思路如下:

  • 当列表中元素个数小于2,分割不了等和子集,直接返回 False
  • 首先,如果数组的总和是奇数,那么肯定无法将其分成两个和相等的子集,直接返回 False
  • 如果总和是偶数,目标就是找出是否可以从数组中挑选出一些数字,它们的和等于数组总和的一半(即 sum(nums)// 2)。

这样问题就转换成了一个0-1背包问题,可以把这个问题看作一个容量为half_sum的背包,数组中的每个数字就是物品,问是否能够恰好填满这个背包。

  • 使用一个布尔数组 dp,其中 dp[i] 表示是否存在子集和等于 i
  • 状态转移:对于每个数字 num,我们更新 dp 数组的状态。如果 dp[j-num]True,则 dp[j] 也应为 True,即表示我们可以通过加入当前的 num 形成和为 j 的子集。【dp[j]=dp[j] or dp[j−num]】
  • 最终,检查 dp[half_num] 是否为 True,如果是,则说明可以找到一个子集和等于目标值。
class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        if len(nums)<2:
            return False
        total_sum=sum(nums)
        if total_sum%2==1:
            return False
        half_sum=total_sum//2
        dp=[False]*(half_sum+1)
        dp[0]=True
        for num in nums:
            for j in range(half_sum,num-1,-1):
                dp[j]=dp[j] or dp[j-num]
        return dp[half_sum]

10、最长有效括号

主要思路如下:

状态数组dp:开辟一个长度为n的状态数组dp,dp[i] 表示 以位置 i 结尾的最长有效括号子串的长度

状态转移:只有遇到 的时候才能开始判断括号闭合得到有效括号,遇到 ) 分两种情况进行处理,已知当前遍历到了第i个元素,s[i]=')'。

  • s[i-1]='(':此时两个括号可以形成有效闭合。dp[i]=dp[i-2]+2(当i-2>=0的时候),s[i-2]表示前i-2位最长有效括号的长度,而加上的2正是当前处理的有效闭合括号个数()【也就是s[i-1]和s[i]两个括号)。
  • s[i-1]=')':此时两个括号不能形成有效闭合,因此需要找到当前 i 指向的右括号所匹配的左括号位置。已知dp[i-1]是前 i-1 位中最长有效括号的长度,因此可知,i-dp[i-1]-1是距离i最近的可能是未匹配左括号的位置。若s[i-dp[i-1]-1]='(',则又找到了一个括号匹配对,因此dp[i]=dp[i-1]+dp[i-dp[i-1]-2]+2(当i-dp[i-1]-2>=0时)。【dp[i-dp[i-1]-2]表示在匹配的左括号之前,是否还有其他有效的括号子串】。比如()()()()在以上括号子串中,dp[i-dp[i-1]-2]=2(紫色部分),dp[i-1]=6(红色部分),2(绿色部分)。
class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n=len(s)
        if n==0:
            return 0
        dp=[0]*n
        res=0
        for i in range(1,n):
            if s[i]==')':
                if s[i-1]=='(':
                    dp[i]=dp[i-2]+2 if i-2>=0 else 2
                elif i-dp[i-1]-1>=0 and s[i-dp[i-1]-1]=='(':
                    dp[i]=dp[i-1]+dp[i-dp[i-1]-2]+2 if i-dp[i-1]-2>=0 else dp[i-1]+2
            res=max(res,dp[i])
        return res

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值