【Leetcode】 top 100 动态规划

70 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

分析:dp[i] 代表爬到第 i 阶台阶的方法数

递推规律:dp[i] = dp[i-1] + dp[i-2]

初始条件:dp[1] = 1, dp[2] = 2 (1+1/2)

class Solution(object):
    def climbStairs(self, n):
        """
        :type n: int
        :rtype: int
        """
        dp = []
        for i in range(n):
            if i == 0:dp.append(1)
            elif i == 1:dp.append(2)
            else:dp.append(dp[i-1]+dp[i-2])
        return dp[-1]

#由于只与dp[i-1]和dp[i-2]相关,用两个变量交替更新即可
118 杨辉三角

给定一个非负整数 numRows生成「杨辉三角」的前 numRows 行。

分析:dp[i,j] 代表第 i 行第 j 列的元素(从0开始)

递推规律:dp[i,j] = dp[i-1,j-1] + dp[i-1,j]

初始条件:dp[0]=[1]  dp[1]=[1,1]   j = 0或i时,dp[i,j] = 0

class Solution(object):
    def generate(self, numRows):
        """
        :type numRows: int
        :rtype: List[List[int]]
        """
        dp = [[1]*i for i in range(1,numRows+1)]
        for i in range(numRows):
            for j in range(i+1):
                if j == 0 or j == i:continue
                else: dp[i][j] = dp[i-1][j-1]+dp[i-1][j]
        
        return dp

#不考虑返回值的占用空间时,可以用滚动数组来保存dp[i-1],但需要倒序更新!!!因为要用到j-1的值
 198 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

分析:dp[i] 代表偷到第 i 个房屋时获得的最高金额

递推规律:dp[i] = max(dp[i-1], dp[i-2]+nums[i])

初始条件:dp[0] = nums[0]  dp[1] = max(nums[0], nums[1])

class Solution(object):
    def rob(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        dp = [0]*len(nums)
        for i in range(len(nums)):
            if i == 0:dp[0] = nums[0]
            elif i == 1:dp[1] = max(nums[0], nums[1])
            else: dp[i] = max(dp[i-1], dp[i-2]+nums[i])
        return dp[-1]

#同样地,可以仅用dpi和dpj两个变量来动态维护dp[i-2]和dp[i-1]
class Solution(object):
    def rob(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        if len(nums) == 1:return nums[0]
        
        dpi, dpj = nums[0],  max(nums[0], nums[1])
        for i in range(2,len(nums)):
            out = max(dpj, dpi+nums[i])
            dpi, dpj = dpj, out
        return dpj
279 完全平方数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。

分析:dp[i] 表示和为 i 的完全平方数的最少数量;

假设 i 之前有一个数m,能通过加上一个完全平方数等于 i,即有dp[i] = dp[m]+1 = dp[i-k*k]+1,因为存在多个m,所以需要遍历全部的dp[m]找到最小值来得到dp[i];

递推规律:dp[i] = min(dp[i-k*k])+1   k=1...int(i**0.5)

初始条件:dp[0] = 0 (dp[1]可以通过dp[0+1*1]找到)

class Solution(object):
    def numSquares(self, n):
        """
        :type n: int
        :rtype: int
        """
        dp = [0]*(n+1)
        for i in range(1,n+1):
            minn = i
            for k in range(1,int(i**0.5)+1):   #range的右边取不到
                minn = min(dp[i-k*k],minn)     #遍历所有m
            dp[i] = minn+1
        return dp[n]

322 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 

分析:dp[i] 表示总金额为 i 的最少硬币个数;

递推规律:dp[i] = min(dp[i-coins[k]])+1

初始条件:dp[0] = 0   dp[coins[k]] = 1

找不到dp[i-coins[k]]将其赋为-1,视作无效;

class Solution(object):
    def coinChange(self, coins, amount):
        """
        :type coins: List[int]
        :type amount: int
        :rtype: int
        """
        
        dp = [0]*(amount+1)
        for i in range(len(coins)):
            if coins[i] > amount:continue          #需要跳过大于总金额的硬币
            else: dp[coins[i]] = 1
        for i in range(1, amount+1):
            minn = i
            for k in range(len(coins)):
                if i-coins[k]<0:continue           #负数也有对应索引,但金额不能为负
                elif dp[i-coins[k]]<0:continue     #出现无效的金额
                else:minn = min(dp[i-coins[k]], minn)
            dp[i] = minn + 1 if minn != i else -1
        return dp[amount]
139 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

分析:dp[i] 表示s[:i] 能否用worddict拼接的情况;

递推规律:dp[i] = dp[j] + s[j:i]   j<i

(实际是 and 运算,要求[0:j] 能被 worddict 表示且s[j:i]也能被 worddict 表示)

初始条件:dp[0] = True

(假设s = ‘s’,worddict = ['s'],此时dp[1] = dp[0] and s[0] in worddict,很明显此时dp[0]为True)

class Solution(object):
    def wordBreak(self, s, wordDict):
        """
        :type s: str
        :type wordDict: List[str]
        :rtype: bool
        """
        wordDict = set(wordDict)
        dp = [0] * (len(s)+1)
        
        for i in range(len(s)+1):
            if i == 0:dp[0] = True

            for j in range(i):
                dp[i] = dp[j] and (s[j:i] in wordDict)
                if dp[i]:break
        return dp[-1]
300 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

分析:dp[i] 表示nums[:i]中最长严格递增子序列的长度

递推规律:在nums[i]>nums[j]的情况下:dp[i] = dp[j]+ 1  否则 dp[i] = dp[j]

(dp[i]也是个明显的递增序列)  j要满足dp[j]=dp[i-1]

初始条件:dp[0] = 1

---------------------------------------------------------------------------------------------------------------------

案例:[4,10,4,3,8,9]  按上述思路分析:dp = [1,2,2,2,3,4],dp[3]=2对应[4,10],dp[4]是因为8>3而将最长严格递增子序列长度增加,但3实际上没参与最长严格递增子序列的构成;

错误原因:dp[i] 应该表示nums[:i]中以nums[i]结尾的最长严格递增子序列的长度,否则无法通过判断nums[i]和nums[j]的大小关系来决定dp[i];

class Solution(object):
    def lengthOfLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        dp = [1]*len(nums)
        for i in range(1, len(nums)):
            for j in range(i):
                if nums[i]>nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)

官方思路:贪心+二分查找

d[i] 表示 长度为i的最长上升子序列 末尾元素 的最小值

length 表示 当前的最长上升子序列长度

在遍历nums时,若nums[i]>d[length],就可以生成新的最长上升子序列,length = length+1,d[length] = nums[i];否则判断能否利用nums[i]去更新d,因为需要d增长的足够慢,所以用nums去替代第一个大于nums[i]的值(这里虽然d和nums的顺序不同,但对应的最长上升子序列的长度相同)

from bisect import bisect_left    

class Solution(object):
    def lengthOfLIS(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        d = []
        d.append(nums[0])
        length = 0
        for i in range(1, len(nums)):
            if nums[i] > d[length]:
                length = length + 1
                d.append(nums[i])
            else:
                idx = bisect_left(d, nums[i])
                d[idx] = nums[i]
        return length+1

贪心在于构建增长最慢的上升序列,二分查找在于查找第一个大于nums[i]的值(因为d递增)

# 在nums中进行二分查找val
left, right = 0, len(nums)-1
while left<=right:
    mid = (left+right)//2
    if nums[mid] >= val:
        right = mid - 1
    else: 
        left = mid + 1
152 乘积最大子数组

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

分析:由于存在负数,除了需要维护乘积最大值,还需要维护乘积最小值(负值越大);

mindp[i] 表示以 nums[i] 结尾的乘积最小值,maxdp[i] 表示以 nums[i] 结尾的乘积最大值;

递推规律:mindp[i] =  min(mindp[i-1]*nums[i], nums[i], maxdp[i-1]*nums[i])

                  maxdp[i] = max(mindp[i-1]*nums[i], nums[i], maxdp[i-1]*nums[i])

初始条件:mindp[0] = maxdp[0] = nums[0]

class Solution(object):
    def maxProduct(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        length = len(nums)
        if length == 1:return nums[0]

        mindp, maxdp = [0]*length, [0]*length
        mindp[0], maxdp[0] = nums[0], nums[0]
        maxproduct = maxdp[0]
        for i in range(1, length):
            mindp[i] = min(mindp[i-1]*nums[i], nums[i], maxdp[i-1]*nums[i])
            maxdp[i] = max(mindp[i-1]*nums[i], nums[i], maxdp[i-1]*nums[i])
            maxproduct = max(maxproduct, maxdp[i])
        return maxproduct

#同样地,可以仅用dpm和dpn两个变量来动态维护mindp[i-1]和maxdp[i-1]
416 分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

分析:dp[i] 表示元素和为 i 的情况,i <= sum(nums)//2;

递推规律:dp[i] = dp[i] or dp [i-nums[j]]    当前元素为nums[j]

初始条件:dp[0] = true 

class Solution(object):
    def canPartition(self, nums):
        """
        :type nums: List[int]
        :rtype: bool
        """
        summ = sum(nums)
        if summ % 2:return False
        else:summ = summ // 2

        dp = [False]*(summ+1)
        dp[0] = True

        for j in range(len(nums)):
            for i in range(summ, nums[j]-1, -1):   #判断新加入的nums[j]会使哪些可能的和出现
                dp[i] = dp[i] or dp[i-nums[j]]
                #从后往前遍历是因为当前的nums[j]只能使用一次
                #假设 i = 2*nums[j],从前往后遍历i会先出现 dp[nums[j]]=true, 然后dp[i]=true 
        return dp[-1]

需要从后往前更新来维护dp

416. 分割等和子集 - 力扣(LeetCode)  这份题解详细描述了怎么从普通二维dp优化到滚动数组的一维dp到倒序遍历的一维dp

 32 最长有效括号

给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

分析:dp[i] 表示以nums[i]结尾的最长有效括号子串的长度

递推规律:当nums[i] = '('    dp[i] = 0

                  当nums[i] = ')'    需要去倒序找到第一个还没匹配的'('

                                            若nums[i-1]='('   dp[i] = dp[i-2]+2,否则得去i-dp[i-1]-1的位置寻找,若能找到,dp[i] = dp[i-1]+2+dp[i-dp[i-1]-2];若不能找到,dp[i]=0

初始条件:dp[0] = 0

class Solution(object):
    def longestValidParentheses(self, s):
        """
        :type s: str
        :rtype: int
        """
        dp = [0]*len(s)
        maxlen = 0
        for i in range(1, len(s)):
            if s[i] == '(':continue
            elif s[i-1] == '(':
                dp[i] = dp[i-2] + 2 if i-2>=0 else 2
            else:
                if i-dp[i-1]-1>=0 and s[i - dp[i - 1] - 1] == '(':
                    dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2] if i-dp[i-1]-2>=0 else dp[i-1] + 2
                else: continue
            maxlen = max(maxlen, dp[i])
        return maxlen

  • 11
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值