0
点赞
收藏
分享

微信扫一扫

数据结构算法总结-动态规划

绪风 2022-03-11 阅读 60

动态规划

解题步骤

如果按照DP个标准型表述,可以写成如下形式:

1)状态定义:一般题目要什么,就把状态定义成什么
2)状态转移方程
3)初始状态
4)返回值

示例汇总

1.面试题42. 连续子数组的最大和

状态转移方程:

如果dp[i-1]<0,则dp[i]=nums[i]
否则dp[i]=dp[i-1]+nums[i]

2.面试题 08.11. 零钱兑换

相同题目( .518. 零钱兑换 II)

状态转移方程:(注意:顺序无关,1+5和5+1是同一种方案)

先遍历零钱,保证在考虑一枚零钱的情况时,没有较大的零钱影响,这样,我们最终每种组合情况,都是以零钱的面额大小非递减组合。保证了同样的情况,调换顺序后重复计算的情况。DP状态的变化通过多轮计算完成。

        coins=[1,5,10,25]
        dp= [0 for i in range(n+1)]
        dp[0]=1
        for coin in coins:
            for i in range(coin,n+1):
                dp[i]=dp[i]+dp[i-coin]

3.337. 打家劫舍 III:三种解法,递归、递归+记忆化、后序遍历+动态规划

状态转移方程思路:

  1. 当前节点是否打劫,受两个叶子节点是否打劫的影响,所以,应该先访问叶子结点,再确定当前节点是否打劫。所以,需要后序遍历
  2. 当前节点是否打劫,与左右叶子结点是否打劫所累计的金额有关。所以,需要用一个数组存储累计金额。
        def dfs(root):
            """
            """
            if not root:#root 节点不存在,即,偷不偷该点,最终返回值都为0
                return [0,0]
            
            leftArr=dfs(root.left)
            rightArr=dfs(root.right)

            #分别指代当前节点偷与不偷,累积到当前节点的最大偷取金额
            dp=[0,0]
            #当前节点不偷,左右节点可偷、可不偷,所以,去各节点可能性的最大值计算
            dp[0]=max(leftArr[0],leftArr[1]) + max(rightArr[0],rightArr[1])
            #当前节点偷,左右节点只能不偷
            dp[1]=leftArr[0]+rightArr[0]+root.val

            return dp

4.279. 完全平方数:动态规划

从小到大开始访问,状态转移方程:dp[i]=min(dp[i], dp[i-square[j]]+1)

    def numSquares(self, n):
        dp=[n]*(n+1)
        dp[0]=0

        square=[i**2 for i in range(int(n**0.5)+1)]
        for i in range(1,n+1):
            for j in range(len(square)):
                if square[j]<=i:
                    dp[i]=min(dp[i], dp[i-square[j]]+1)
                else:
                    break
        return dp[n]

5.152. 乘积最大子数组:动态规划

关键点:

  1. 需要分情况讨论为正、为负的情况,而且要从小到大的顺序访问index并记录正负累计乘积,因此,需要一个二维数组,消除后效性
  2. 最终取值需要存在不同维度上,最后需要对他们再做比较。总结的规律:如果全部状态都以[0]做起点,使用最后状态取值就可以了,e.g.爬楼梯问题、198. 打家劫舍、213. 打家劫舍 II;如果从中间开始作为起点,需要再做对比,e.g.本题(乘积最大子数组)、连续子数组最大和。
  3. 状态方程:判断 “上轮状态与当前值乘积” VS “当前值” 两者的大小关系
        if len(nums)==1:
            return nums[0]

        # 初始化
        dp=[[0,0] for i in range(len(nums))]
        #使用第二维记录max和min状态,便于分类讨论
        dp[0][0]=nums[0] #min
        dp[0][1]=nums[0] #max

        for i in range(1,len(nums)):

            #分类讨论
            if nums[i]>0: #正值
                dp[i][0] = dp[i - 1][0] * nums[i]
                dp[i][1] = max(dp[i - 1][1] * nums[i],nums[i])
            elif nums[i]<=0: #负值
                dp[i][0] = min(dp[i - 1][1] * nums[i],nums[i])
                dp[i][1] = max(dp[i - 1][0] * nums[i],nums[i])

        m=nums[0]
        for i in range(len(dp)):
            m=m if m>dp[i][1] else dp[i][1]

        return m

6.221. 最大正方形:动态规划+存储优化

关键点:

  1. 要点1:使用两个数组替代二维数组,用于存储DP状态
  2. 要点2:dp存储以(r,c)为右下角的最大正方形边长,它的大小与数组取值、附近3个dp位置的大小
  3. 状态转移方程:dp_cur[c] = min(dp_cur[c - 1], dp_pre[c - 1], dp_pre[c]) + 1 if(matrix[r][c]=="1") else  0
    def maximalSquare(self, matrix):
        """
        :type matrix: List[List[str]]
        :rtype: int
        """
        rows = len(matrix)
        if(rows==0):return 0
        cols = len(matrix[0])
        if(cols==0):return 0

        dp_pre = [0] * rows
        dp_cur = [0] * cols

        #首行的首列初始化
        if (matrix[0][0] == "1"): dp_cur[0] = 1

        #首行初始化
        for c in range(1, cols):
                dp_cur[c] = 1 if(matrix[0][c]=="1") else 0

        max_val=max(dp_cur)

        #动态规划状态转移
        for r in range(1, rows):
            dp_pre = dp_cur
            dp_cur = [0] * cols
            dp_cur[0] = 1 if(matrix[r][0] =="1") else 0
            for c in range(1, cols):
                #DP状态转移方程
                dp_cur[c] = min(dp_cur[c - 1], dp_pre[c - 1], dp_pre[c]) + 1 if(matrix[r][c]=="1") else  0
            max_val=max(max_val,max(dp_cur))

        return max_val**2

7.264. 丑数 II:指针+自变量与变量同体 

参考2

  1. 丑数乘以乘数「2、3、5」都为丑数,所以,可以用三个指针指向乘2\3\5的位置
  2. 关键在于要按照大小排列,因此,要有一个min(nums[i2]*2,nums[i3]*3,nums[i5]*5)的判断过程,丑数列表中每次只增加一个元素
  3. 之后,如果新增加的元素和之前丑数及对应乘数的乘积相等,则,丑数指针增加+1
        #指针
        i2=i3=i5=0

        for i in range(1,1690+1):
            if(i==n):
                return nums[i-1]
            nums.append(min(nums[i2]*2,nums[i3]*3,nums[i5]*5))
            
            #注意,由于存在nums[i2]*2,nums[i3]*3,nums[i5]*5三者相同的情况,
            #为了避免丑数重复出现,使用if,不要使用elif,只要符合条件都向前移动一次
            if(nums[i]==nums[i2]*2):
                i2=i2+1
            if(nums[i]==nums[i3]*3):
                i3=i3+1
            if(nums[i]==nums[i5]*5):
                i5=i5+1
        return None

8.322. 零钱兑换

  • 使用dp存储总金额为i时的需要的硬币数
  • 针对不同面额的coin,对比dp[i], dp[i-coins[j]]+1的大小
for i in range(amount+1):
    for j in range(n):
        if coins[j] <= i:
            dp[i] = min(dp[i], dp[i-coins[j]]+1)

9.474. 一和零

背包问题,采用动态规划经典做法,从小处展开。具体见代码。

空间复杂度为 O(lmn),总时间复杂度是 O(lmn + L)。

class Solution(object):
    def findMaxForm(self, strs, m, n):
        """
        :type strs: List[str]
        :type m: int
        :type n: int
        :rtype: int

        状态定义:dp[i][j][k],表示只考虑前i个字符串时,使用j个0和k个1,能够装进”背包“的最大字符串数量
        状态转移方程:
        dp[i][j][k] =
        1)不考虑当前字符串,dp[i-1][j][k]
        2)考虑当前字符串,假设当前字符串包含的0和1分别是j0和k0,dp[i-1][j-j0][k-k0]+1
        """
        
        def getinfo(str_val):
            c=[0,0]
            for s in str_val:
                if(s=='0'):
                    c[0]=c[0]+1
                else:
                    c[1]=c[1]+1
            return c[0],c[1]
        
        l =len(strs)

        dp=[[[0 for k in range(n+1)] for j in  range(m+1)]  for i in range(l+1) ]

        for i in range(1,l+1):
            j0,k0=getinfo(strs[i-1])
            for j in  range(0,m+1):
                for k in range(0,n+1):
                    if(j>=j0 and k>=k0):
                        dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-j0][k-k0]+1)
                    else:
                        dp[i][j][k]=dp[i-1][j][k]
        return dp[l][m][n]

举报

相关推荐

0 条评论