• 一文解决动态规划~详解


    动态规划

    详细总结所有动态规划面试题
    跟着刷完你就会!

    动态规划思路:

    采用动态五部曲
    img

    1. 一维动态规划–简单dp:

    斐波那契数

    img

    1. dp数组的含义,dp[i]=F(i)的值
    2. 递推方程 dp[i]=dp[i-1]+dp[i-2]
    3. dp的初始化,dp[0]=0,dp[1]=1
    4. 从前到后遍历
    def fib(self, n):
        dp=[0,1]
        if n<2:
            return dp[n]
        for i in range(2,n+1):
            dp.append(dp[i-1]+dp[i-2])
        return dp[-1] 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们发现只用到前两个状态的数值,可以只用两个变量存储, 空间复杂度为O(1)

    def fib(self, n):
        dp=[0,1]
        if n<2:
            return dp[n]
        for i in range(2,n+1):
            tmp=dp[0]+dp[1]
            dp[0]=dp[1]
            dp[1]=tmp
        return dp[-1] 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    爬楼梯

    img

    1. dp[i]表示到i阶梯有多少种爬楼梯方法
    2. dp[i-1]再跨一步或者dp[i-2]再跨两步就可以到dp[i]=dp[i-1]+dp[i-2]
    3. 初始化,dp[0]没有意义,dp[1]=1,dp[2]=2
    4. 从前到后遍历
    def climbStairs(self, n):
        dp=[0,1,2]
        if n<3:
            return dp[n]
        for i in range(3,n+1):
            dp.append(dp[i-1]+dp[i-2])
        return dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    同样可以用两个变量降低一下空间复杂度

    使用最小花费爬楼梯

    img

    1. 到达第下标为i的台阶花费的最少体力dp[i]
    2. dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
    3. 初始化,dp[0]=cost[0],dp[1]=cost[1]
    4. 从前往后遍历,原数组:[1,100,1,1,1,100,1,1,100,1] dp数组:[1, 100, 2, 3, 3, 103, 4, 5, 104, 6]
    5. 因此最后要取dp[-1]和dp[-2]的最小值,因为这两个最后一步可以直达并且不花费
    def minCostClimbingStairs(self, cost):
        dp=[cost[0],cost[1]]
        n=len(cost)
        for i in range(2,n):
            dp.append(min(dp[i-1],dp[i-2])+cost[i])
        return min(dp[-1],dp[-2])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    粉刷房子

    img

    def minCost(self, costs: List[List[int]]) -> int:
        #dp[i][j],j取0-2,表示第i间房子粉刷成三种颜色的最小花费
        #初始化,dp[0][0-2]=costs[0][0-2]
        #dp[i][0]=min(dp[i-1][1],dp[i-1][2])+costs[i][0]
        #滚动数组覆盖
        dp=costs[0].copy()
        for i in range(1,len(costs)):
            tmp=dp.copy()
            dp[0]=min(tmp[1],tmp[2])+costs[i][0]
            dp[1]=min(tmp[0],tmp[2])+costs[i][1]
            dp[2]=min(tmp[0],tmp[1])+costs[i][2]
        return min(dp)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    字符串反转

    img

    dp[i][0]=dp[i-1][0]+(s[i]==0? 0:1)

    dp[i][1]=min(dp[i-1][0]+dp[i-1][1])+(s[i]==1?0:1)

    dp[0]=[(s[0]==0?0:1),(s[1]==1?0:1)]

    def minFlipsMonoIncr(self, s: str) -> int:
        dp=[0 if s[0]=="0" else 1,0 if s[0]=="1" else 1]
        for i in range(1,len(s)):
            tmp=dp.copy()
            dp[0]=tmp[0]+(0 if s[i]=="0" else 1)
            dp[1]=min(tmp[0],tmp[1])+(0 if s[i]=="1" else 1)
        return min(dp)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    不同路径

    img

    机器⼈从(0 , 0) 位置触发,到(m - 1, n - 1)终点

    1. dp数组的含义是表示走到dp[i][j]位置的路径数量
    2. dp[i][j] = dp[i - 1][j] + dp[i][j - 1],只能从这两个方向过来
    3. dp[i][0]和dp[0][j]都是1,初始化,因为到这的路径只有1条
    4. 遍历从左到右,从上到下
    def uniquePaths(self, m, n):
        dp=[[0]*n for _ in range(m)]
        for i in range(m):
            dp[i][0]=1
        for i in range(n):
            dp[0][i]=1
        for i in range(1,m):
            for j in range(1,n):
                dp[i][j]=dp[i-1][j]+dp[i][j-1]
        return dp[-1][-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    不同路径-有障碍物

    img

    1. dp数组的含义依然是表示走到当前位置的路径dp[i][j]
    2. 但是如果位置不是障碍,才去推导dp[i][j]=dp[i][j-1]+dp[i-1][j], else=0
    3. 初始化和上面一样,但是要排除有障碍的情况,有障碍的后面就不初始化,因为无法到达,需要上行
    4. 遍历依然从上到下,从左到右
    def uniquePathsWithObstacles(self, obstacleGrid):
        m=len(obstacleGrid)
        n=len(obstacleGrid[0])
        dp=[[0]*n for _ in range(m)]
        for i in range(m):
            if obstacleGrid[i][0]==1:
                break
            dp[i][0]=1
        for i in range(n):
            if obstacleGrid[0][i]==1:
                break
            dp[0][i]=1
        for i in range(1,m):
            for j in range(1,n):
                if obstacleGrid[i][j]==0:
                    dp[i][j]=dp[i-1][j]+dp[i][j-1]
                else:
                    dp[i][j]=0
        return dp[-1][-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    整数拆分

    img

    1. dp表示的的含义:拆分i,dp[i]表示拆分出的最大值
    2. dp[i]=max(dp[i],j*(i-j),j*dp[i-j]),j从1开始遍历,到j-1,要比较dp[i]的原因是dp[i]是循环保存的之前最大值
    3. 初始化dp[2]=1,dp[1]=0
    4. 遍历,i从3开始,j从1开始,正好用dp[2]的值,也用到了dp[1]的值但是并没有用,因为是0
    def integerBreak(self, n):
        dp=[0]*(n+1)
        dp[2]=1
        for i in range(3,n+1):
            for j in range(1,i):
                dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]))
        return dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    不同的二叉搜索树

    img

    img

    1. 分析dp[i]表示的是i个节点组成的二叉搜索树的种类
    2. dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量,j从1开始遍历,遍历到i
    3. 如上图的递推关系,我们发现以j为头节点,j-1左子树,i-j个右子树
    4. dp[0]=1,即左子树为空
    5. 遍历,i从1-n,j从1-i
    def numTrees(self, n):
        dp=[0]*(n+1)
        dp[0]=1
        for i in range(1,n+1):
            for j in range(1,i+1):
                dp[i]+=dp[j-1]*dp[i-j]
        return dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2. 复杂动态规划–0-1背包问题:

    img

    0-1背包问题:

    有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

    1. 确定dp数组以及下标的含义

    对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

    1. 确定递推公式

    再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

    那么可以有两个方向推出来dp[i][j],

    **不放物品i:**由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)

    **放物品i:**由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

    所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

    1. 初始化:即dp[i][0]=0,没有疑问,因为背包容量为0的时候就没有任何价值

    在看其他情况。

    状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

    dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

    那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。

    当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。

    #初始化
    weight=[1,3,4]
    value=[15,20,30]
    w=len(weight)
    n=4
    dp=[[0]*(n+1) for _ in range(w)]
    for j in range(n+1):
        if j<weight[0]:
            dp[0][j]=0
        else:
            dp[0][j]=value[0]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. 遍历顺序:

    先遍历物品,后遍历背包重量

    物品i从1- >w,j->bagweight

    for i in range(1,w):
        for j in range(0,n+1):
            if j<weight[i]:
                dp[i][j]=dp[i-1][j]
            else:
                dp[i][j]=max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
    print(dp)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    压缩0-1背包问题:

    dp[i][j]只与dp[i-1]这一层的j有关,因此可以节省空间

    1. 确定dp数组的定义

    在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。用循环表示拿到哪件商品了

    1. 递推,等于把i层的东西去掉了,递推方程为
    dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
    
    • 1
    1. 初始化,因为dp[j]表示容量为j的背包,因此dp[0]=0,其余位置也初始化为0
    2. 二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

    为什么呢?

    倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么前面的物品0就会被重复加入多次!在max的前提下

    weight=[1,3,4]
    value=[15,20,30]
    w=len(weight)
    n=4
    dp=[0]*(n+1)
    for i in range(0,w):
        for j in range(n,weight[i]-1,-1): #必须倒序,才能用到上一层的前面的值,不然会反复加
           dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
        print(dp)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    分割等和子集:

    img

    1. 这里首先将其套到背包问题,背包容量为sum/2,weight=nums,value=nums
    2. 即dp[j]表示容量为j的背包,当前放入i件物品之后容纳的最大价值
    3. 递推方程:dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]),这里的i表示拿到了第几个数
    4. 初始化,dp[0]=0,一个数字不放
    5. 遍历顺序:i从0->len(nums),j从sum/2->0
    def canPartition(self, nums):
        bag_sum=sum(nums)
        if bag_sum%2==1:
            return False
        bag_sum=bag_sum/2
        dp=[0]*(bag_sum+1)
        for i in range(len(nums)):
            for j in range(bag_sum,-1,-1):
                if j>=nums[i]:
                    dp[j]=max(dp[j],dp[j-nums[i]]+nums[i])
        return True if dp[bag_sum]==bag_sum else False
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    最后一块石头重量:

    img

    1. 抽象到01背包问题,可以理解成,分出重量最多的两堆石头,即分出最高为一半重量的石头堆
    2. dp[j]表示重量为j的背包,在当前i之前放入后所能容纳的最大价值
    3. 递推方程:dp[j]=max(dp[j],dp[j-stones[i]]+stones[i])
    4. 初始化dp[0]=0,长度为weight/2
    5. 遍历顺序i从0->len(stones),j从weight/2-0
    6. 注意,这里我们先向下取整,得到左边这堆的最大重量dp[-1],右边的是sum-dp[-1]
    def lastStoneWeightII(self, stones):
        bag_sum=sum(stones)
        target=bag_sum//2
        dp=[0]*(target+1)
        for i in range(len(stones)):
            for j in range(target,-1,-1):
                if j>=stones[i]:
                    dp[j]=max(dp[j],dp[j-stones[i]]+stones[i])
        return bag_sum-2*dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    目标和:

    img

    1. dp[j]表示当前i层所凑够背包j的解法数量,设➕号的和为x,剩下的数字放-号,为sum-x
    2. 注意,因为变成了求解法数量了,不是求当前背包j的最大价值了
    3. dp[j]+=dp[j-nums[i]]
    4. 初始化dp[0]=1,背包为0的解法是1种,即什么也不拿
    5. x-(sum-x)=target,x=(target+sum)/2,如果向下取整,奇数无解,排除,abs(S)>sum也无解,因此背包容量为x=(target+sum)/2
    def findTargetSumWays(self, nums, target):
        num_sum=sum(nums)
        if (target+num_sum)%2==1 or abs(target)>num_sum:
            return 0
        max_bag=(target+num_sum)//2
        dp=[0]*(max_bag+1)
        dp[0]=1
        for i in range(len(nums)):
            for j in range(max_bag,nums[i] - 1,-1):#这里必须要-1,到0,因为dp[0]!=0
                dp[j]+=dp[j-nums[i]]
        return dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    一和零:

    img

    1. 这题本质上是一个两个维度的0-1背包问题,mn表示01的容量限制,子集个数是价值
    2. dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]
    3. dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
    4. 初始化,背包容量为m,n,dp[0][0]=0
    5. 外层物品(字符串),内层容量(分别遍历mn,倒序)遍历。
    def findMaxForm(self, strs, m, n):
        dp=[[0]*(n+1) for _ in range(m+1)]
        for one_str in strs:
            oneNum = 0
            zeroNum = 0
            for s in one_str:
                if s=='0':
                    zeroNum+=1
                else:
                    oneNum+=1
            for i in range(m,zeroNum-1,-1):
                for j in range(n,oneNum-1,-1):
                    dp[i][j]=max(dp[i][j],dp[i - zeroNum][j - oneNum] + 1)
        return dp[-1][-1]         
                
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3. 复杂动态规划–完全背包问题:

    完全背包问题:

    有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

    完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

    唯一要改的就是遍历的顺序的一个设置,不用倒序遍历,而是要正序!

    weight=[1,3,4]
    value=[15,20,30]
    w=len(weight)
    n=4#最大容量
    dp=[0]*(n+1)
    for i in range(0,w):
        for j in range(weight[i],n+1): #正序
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
        print(dp)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    最少的硬币数目:

    img

    1. 经典的完全背包问题,每个硬币可以用无限次,背包容量为amount,价值为次数,求最少,dp[j]表示组成j金额的最少硬币数量
    2. 递推公式,dp[j]=min(dp[j],dp[j-coins[i]]+1])
    3. 初始化,dp[0]=0,dp=[amount+1]*(amount+1)
    4. 遍历外圈物品,内圈价值,正序从coins遍历。(完全背包正向遍历)
    def coinChange(self, coins, amount):
        dp=[amount+1]*(amount+1)
        dp[0]=0
        for i in range(len(coins)):
            for j in range(coins[i],amount+1):
                dp[j]=min(dp[j],dp[j-coins[i]]+1)
        if dp[-1]==amount+1:#如果依然是初始化的值,说明没法凑成        
            return -1
        else:
            return dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    硬币组合数目:

    img

    组合排列的数目。

    img

    组合和排列并不相同。

    1. dp的含义,dp[j]表示面值为j的组合数目
    2. 递推公式 dp[j] += dp[j - coins[i]];
    3. 初始化,dp[0]=1,其余的位置初始化位0就好
    4. 这里的遍历方式直接影响最后的结果。因为是无限背包问题,所以需要正序

    如果是组合问题:应该先物品,后背包

    for i in range(len(coins)):
        for j in range(coins[i],amount+1):
            dp[j]+=dp[j-coins[i]]
    
    • 1
    • 2
    • 3

    如果是排列问题,应该先背包后物品。

    for j in range(1,amount+1):
        for i in range(len(coins)):
            if j >=coins[i]:
                dp[j]+=dp[j-coins[i]]
    
    • 1
    • 2
    • 3
    • 4

    因此本题代码:

    def change(self, amount, coins):
        dp=[0]*(amount+1)
        dp[0]=1
        for i in range(len(coins)):
            for j in range(coins[i],amount+1):
                dp[j]+=dp[j-coins[i]]
        return dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    组合总和:

    img

    1. dp[j]: 凑成目标正整数为j的排列个数为dp[j]
    2. dp[j] += dp[j - nums[i]];
    3. 初始化,dp[0]=1,其余初始化0
    4. 排列问题,所以外层遍历背包,内层遍历数字
    def combinationSum4(self, nums, target):
        dp=[0]*(target+1)
        dp[0]=1
        for j in range(1,target+1):
            for i in range(len(nums)):
                if j>=nums[i]:
                    dp[j]+=dp[j-nums[i]]
        return dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    爬楼梯:

    img

    img

    跳的阶数就是物品,楼顶就是amount,跳法就是组合数目,这里我们设置nums为跳的台阶层数

    1. dp[j]表示跳的阶梯是j的跳法
    2. dp[j]+=dp[j-nums[i]]
    3. 初始化,dp[0]=1,其余为0
    4. 遍历顺序,因为是排列,先上1和先上2是不一样的,所以先背包放外面,物品放内层
    def climbStairs(self, n):
        dp=[0]*(n+1)
        dp[0]=1
        nums=[1,2]
        for j in range(1,n+1):
            for i in range(len(nums)):
                if j>=nums[i]:
                    dp[j]+=dp[j-nums[i]]
        return dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    完全平方数:

    img

    首先分析,很浓的完全背包问题,因此需要正向遍历,然后是求最小价值,因此递推用min并且不用区分组合还是排列。即先背包还是先物品都可以

    背包容量为n,物品为1,4,9.。。。一直到小于n

    1. dp[j]表示组成j的最少数量
    2. dp[j]=min(dp[j],dp[j-i*i]+1)
    3. dp初始化,dp[0]=0,其余的要初始化为n,即为n个1
    4. 遍历,先背包还是先物品都可以,然后完全背包问题需要正向
    5. i从0->iii到n
    def numSquares(self, n):
        dp=[n]*(n+1)
        dp[0]=0
        thinglen=int(pow(n,0.5))
        for i in range(thinglen+1):
            for j in range(i*i,n+1):
                dp[j]=min(dp[j],dp[j-i*i]+1)
        return dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    单词组合:

    img

    抽象成背包问题,首先物品是字典中的字符串,背包是s,是否可以装下。很浓的完全背包问题,因为字典中的词可以重复使用

    1. dp[j]表示0-j部分的字符串是否可以被字典组成,背包同样要len(s)+1,因为【0:len(s)】能拿到全部字符串
    2. dp[j]=dp[j-i]&[i:j] in wordict
    3. 初始化,dp[0]=1,单纯为了递推
    4. 遍历,先物品后背包都可以,物品0-len(s),背包i-len(s)
    def wordBreak(self, s, wordDict):
        dp=[False]*(len(s)+1)
        dp[0]=True
        for i in range(len(s)):
            for j in range(i+1,len(s)+1):
                if dp[i] and s[i:j] in wordDict:
                    dp[j]=True
        return dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    def wordBreak(self, s, wordDict):
        dp=[False]*(len(s)+1)
        dp[0]=True
        for j in range(1,len(s)+1):
            for i in range(0,j):
                if dp[i] and s[i:j] in wordDict:
                    dp[j]=True
        return dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4. 普通动态规划-股票问题

    打家劫舍:

    img

    1. dp表示偷完i以内的房间赚到的最大价值
    2. dp[i]=max(dp[i-2]+nums[i],dp[i-1])
    3. 初始化,dp[0]=nums[0],dp[1]=max(nums[0],nums[1])
    4. 遍历,从0-n即可
    def rob(self, nums):
        if len(nums)==0:
            return 0
        if len(nums)==1:
            return nums[0]
        dp=[0]*len(nums)
        dp[0]=nums[0]
        dp[1]=max(nums[0],nums[1])
        for i in range(2,len(nums)):
            dp[i]=max(dp[i-2]+nums[i],dp[i-1])
        return dp[-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    打家劫舍-成环

    img

    唯一的区别就是最后一个房子和第一个房子相连,不能同时偷

    将偷的代码封装成一个函数

    分别考虑头到尾-1,和头+1到尾的两种情况,取最大。

    def rob(self, nums):
        def robv1(nums,l,r):
            if r==l:
                return nums[l]
            dp=[0]*(r-l+1)
            dp[0]=nums[l]
            dp[1]=max(nums[l],nums[l+1])
            for i in range(2,r-l+1):
                dp[i]=max(dp[i-2]+nums[l+i],dp[i-1])
            return dp[-1]
        if len(nums)==0:
            return 0
        if len(nums)==1:
            return nums[0]
        result1=robv1(nums,0,len(nums)-2)
        result2=robv1(nums,1,len(nums)-1)
        return max(result1,result2)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    打家劫舍-二叉树

    img

    首先本题要用后序遍历

    在树上构建dp数组,记录每个节点偷或者不偷的val

    然后最终取val在根节点的val

    dp递推

    不偷当前节点left=max(result1[0],result1[1])+max(result2[0],result2[1])

    偷当前节点right=node.val+result1[0]+result2[0]

    [left,right]

    def rob(self, root):
        def robtree(node):
            if not node:
                return [0,0]
            result1=robtree(node.left)
            result2=robtree(node.right)
            left=max(result1[0],result1[1])+max(result2[0],result2[1])
            right=node.val+result1[0]+result2[0]
            return [left,right]
        result=robtree(root)
        return max(result[0],result[1])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    买股票的最佳时机

    img

    贪心解法:

    if not prices:
        return 0
    profit=0
    cost=prices[0]
    for i in range(1,len(prices)):
        cost=min(cost,prices[i])
        profit=max(profit,prices[i]-cost)
    return profit
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. 抽象dp -dp[i][0] 表示第i天持有股票所得最多现⾦,dp[i][1] 表示第i天不持有股票所得最多现⾦
    2. img
    3. 这里我们要搞清楚,对现金来说,我们要持有股票,但现金要更多,不持有股票的话,卖出现金要更多。
    4. 对于持有股票来说,因为只能买一次,所以是现在买还是之前就买,是-prices[i],和dp[i-1][0]的最大值,不持有的话,是早卖dp[i-1][1]和现在卖prices[i]+dp[i-1][0]的最大值。

    初始化,dp[0][0]=-prices,dp[0][1]=0,刚买就卖

    def maxProfit(self, prices):
        if not prices:
            return 0
        dp=[[0,0] for _ in range (len(prices))]
        dp[0][0]=-prices[0]
        dp[0][1]=0
        for i in range(1,len(prices)):
            dp[i][0]=max(dp[i-1][0],-prices[i])
            dp[i][1]=max(dp[i-1][1],prices[i]+dp[i-1][0]) 
        return dp[-1][1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    买卖股票的最佳时机-多次购买

    贪心解法:

    def maxProfit(self, prices):
        #等价于每天都在买卖
        profit = 0
        for i in range(1, len(prices)):
            tmp = prices[i] - prices[i - 1]
            if tmp > 0: 
                profit += tmp
        return profit
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    和上面的题的区别在于,

    现金,要么不买,是dp[i-1][0],

    要么用dp[i-1][1]-prices[i],用已有不持股的利润去买股票

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

    不吃股票得到的最多利润不变

    def maxProfit(self, prices):
        if not prices:
            return 0
        dp=[[0,0] for _ in range (len(prices))]
        dp[0][0]=-prices[0]
        dp[0][1]=0
        for i in range(1,len(prices)):
            dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i])
            dp[i][1]=max(dp[i-1][1],prices[i]+dp[i-1][0]) 
        return dp[-1][1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    买卖股票的最佳时机-两次购买

    img

    img

    img

    初始化:dp[0][1]=-prices[0],dp[0][2]=0 img

    遍历顺序和刚才相同:

    def maxProfit(self, prices):
        if not prices:
            return 0
        dp=[[0,0,0,0,0] for _ in range (len(prices))]
        dp[0][1]=-prices[0]
        dp[0][3]=-prices[0]
        for i in range(1,len(prices)):
            dp[i][0]=dp[i-1][0]
            dp[i][1]=max(dp[i-1][1],-prices[i]+dp[i-1][0]) 
            dp[i][2]=max(dp[i-1][2],prices[i]+dp[i-1][1]) 
            dp[i][3]=max(dp[i-1][3],-prices[i]+dp[i-1][2]) 
            dp[i][4]=max(dp[i-1][4],prices[i]+dp[i-1][3])   
        return dp[-1][4]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    买卖股票的最佳时机-k次购买

    img

    本题将上题扩展了一下,用二维数组表示dp

    1. dp[i][j],j从0-2*k,j奇数表示买入,偶数表示卖出
    2. dp递推,奇数dp[i][j]=dp[i-1][j-1]-prices[i]
    3. 偶数,dp[i][j]=dp[i-1][j-1]+prices[i]
    4. 初始化,dp[0][奇数]=-prices[0]
    5. 遍历顺序,从前到后
    def maxProfit(self, k, prices):
        if not prices:
            return 0
        dp=[[0]*(2*k+1) for _ in range (len(prices))]
        for i in range(2*k+1):
            if i%2==1:
                dp[0][i]=-prices[0]
        for i in range(1,len(prices)):
            dp[i][0]=dp[i-1][0]
            for j in range(1,2*k+1):
                if j%2==1:
                    dp[i][j]=max(dp[i-1][j],-prices[i]+dp[i-1][j-1]) 
                else:
                    dp[i][j]=max(dp[i-1][j],prices[i]+dp[i-1][j-1])       
        return dp[-1][-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    买卖股票的最佳时机-手续费

    img

    贪心算法

    def maxProfit(self, prices, fee):
        #贪心
        #等价于每天都在买卖
        profit = 0
        buy=prices[0]+fee
        for i in range(1, len(prices)):
            if prices[i]+fee<buy:
                buy=prices[i]+fee
            elif prices[i]-buy>0: 
                profit += prices[i]-buy
                buy=prices[i]#这里是因为并不知道现在卖了会是最大收益,因此保存buy,之后循环如果prices直接大于当前buy,那么等于使用后面的价格卖的,pmax-pmin+profit=用最高价卖的
        return profit
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们还是来分析dp,可以多次卖,那么使用dp[i][0-1]来表示持有和持有不持有股票的最大收益

    1. dp[i][0]表示不持有股票的状态,dp[i][1]表示持有股票的状态
    2. 第i天持有股票:

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

    第i天卖出股票

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

    1. 初始化,dp[0][0]=-prices[0],dp[0][1]=0
    def maxProfit(self, prices):
        if not prices:
            return 0
        dp=[[0,0] for _ in range (len(prices))]
        dp[0][0]=-prices[0]
        dp[0][1]=0
        for i in range(1,len(prices)):
            dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i])
            dp[i][1]=max(dp[i-1][1],prices[i]+dp[i-1][0]-fee) 
        return dp[-1][1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    买入股票的最佳时机-冷冻期

    img

    img

    img

    img

    dp[i][0]=max(dp[i-1][0],max(dp[i-1][1],dp[i-1][3])-prices[i])
    dp[i][1]=max(dp[i-1][1],dp[i-1][3])
    dp[i][2]=dp[i-1][0]+prices[i]
    dp[i][3]=dp[i-1][2]
    
    • 1
    • 2
    • 3
    • 4

    img

    综上

    def maxProfit(self, prices):
        if not prices:
            return 0
        dp=[[0,0,0,0] for _ in range (len(prices))]
        dp[0][0]=-prices[0]
        for i in range(1,len(prices)):
            dp[i][0]=max(dp[i-1][0],max(dp[i-1][1],dp[i-1][3])-prices[i])
            dp[i][1]=max(dp[i-1][1],dp[i-1][3])
            dp[i][2]=dp[i-1][0]+prices[i]
            dp[i][3]=dp[i-1][2]
        return max(dp[-1])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    5. 复杂动态规划-序列问题

    img

    最长上升子序列(不连续)

    img

    1. dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度
    2. 位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值
    3. if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1)
    4. 每一个i,对应的dp[i](即最长上升子序列)起始大小至少都是1
    5. 因此j要从0-i-1,i要从1-n
    def lengthOfLIS(self, nums):
        dp=[1]*(len(nums))
        result=1
        for i in range(1,len(nums)):
            for j in range(0,i):
                if nums[i]>nums[j]:
                    dp[i]=max(dp[i],dp[j]+1)
            result=max(result,dp[i])
        return result
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    最长连续递增子序列

    img

    1. dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]
    2. 如果 nums[i + 1] > nums[i],dp[i]=dp[i-1]+1
    3. 初始化,都为1
    4. 遍历顺序,一层循环
    5. 取dp最大值
    def findLengthOfLCIS(self, nums):
        dp=[1]*(len(nums))
        for i in range(1,len(nums)):
            if nums[i]>nums[i-1]:
                dp[i]=dp[i-1]+1
        return max(dp)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    最长重复子数组(连续子序列)

    img

    1. dp[i][j]表示两个数组以i-1和j-1结尾的数组中公共子数组最长的长度
    2. if nums1[i-1]==nums2[j-1]: dp[i][j]=dp[i-1][j-1]+1
    3. 初始化,都为0就好,长度要为n+1
    4. 遍历顺序,从1-n,1-m
    def findLength(self, nums1, nums2):
        dp=[[0]*(len(nums2)+1) for _ in range(len(nums1)+1)]
        result=0
        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
                result=max(dp[i][j],result)
        return result
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    最长公共子串(不连续)

    img

    1. dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
    2. if text1[i-1]==text2[j-1]: dp[i][j]=dp[i-1][j-1]+1,else:dp[i][j]=max(dp[i][j-1],dp[i-1][j])
    3. 初始化都为0
    def longestCommonSubsequence(self, text1, text2):
        dp=[[0]*(len(text2)+1) for _ in range(len(text1)+1)]
        for i in range(1,len(text1)+1):
            for j in range(1,len(text2)+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[-1][-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    要求找到子串原串:

    dp=[ [""]*(len(text2)+1) for _ in range(len(text1)+1)]
    for i in range(1,len(text1)+1):
        for j in range(1,len(text2)+1):
            if text1[i-1]==text2[j-1]:
                dp[i][j]=dp[i-1][j-1]+text2[j-1]
            else:
                if len(dp[i-1][j])>len(dp[i][j-1]):
                    dp[i][j]=dp[i-1][j]
                else:
                    dp[i][j]=dp[i][j-1]
    return dp[-1][-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    不相交的线

    img

    其实就是求两个字符串的最长公共子序列的长度!不要求连续,但是必须保证相对顺序

    def maxUncrossedLines(self, nums1, nums2):
        dp=[[0]*(len(nums2)+1) for _ in range(len(nums1)+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[-1][-1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    img

    最大子序和

    img

    dp[i]表示以i结尾的的最大连续子序列最大和

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

    nums[i]从当前从头计算,因为要求连续

    初始化,dp[0]=nums[0],长度等同nums

    def maxSubArray(self, nums):
        dp=[0]*len(nums)
        dp[0]=nums[0]
        res=nums[0]
        for i in range(1,len(nums)):
            dp[i]=max(nums[i],nums[i]+dp[i-1])
            res=max(dp[i],res)
        return res
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    判断子序列:

    img

    双指针做法:

    def isSubsequence(self, s, t):
        l=0
        r=0
        while l<len(s) and r<len(t):
            if s[l]==t[r]:
                l+=1
            r+=1
        if l==len(s):
            return True
        else:
            return False
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    dp做法:

    1. 确定dp数组(dp table)以及下标的含义

    dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]

    1. 状态转移方程:

    img

    img

    初始化就都初始化为0就好了。

    def isSubsequence(self, s, t):
        dp=[[0]*(len(t)+1) for _ in range(len(s)+1)]
        for i in range(1,len(s)+1):
            for j in range(1,len(t)+1):
                if s[i-1]==t[j-1]:
                    dp[i][j]=dp[i-1][j-1]+1
                else:
                    dp[i][j]=dp[i][j-1]
                
        return dp[-1][-1]==len(s)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    量子计算 笔记1 —— 量子比特
    Vue中如何进行分布式任务调度与定时任务管理
    Mysql - 字符串截取、拆分
    2022 Apache IoTDB 物联网生态大会 | 早鸟超值获票福利第一弹!(限时 3 天)
    图片格式转换软件哪个好?建议收藏这三个方法
    关于阅读《重构的时机和方法》这本书所带来的启发
    蓝桥杯每日一题2023.11.22
    GBase 8c V3.0.0数据类型——访问权限查询函数
    九种分布式ID解决方案
    python使用openvc库进行图像数据增强
  • 原文地址:https://blog.csdn.net/fs1341825137/article/details/125875918