动态规划算法专题

动态规划算法专题

爬楼梯:

#进阶版的爬楼梯,每次可以爬1阶或两阶楼梯,但是不能连续爬两阶
def climbStairs(n):
    if n < 3:
        return n
    dp = [[0 for _ in range(2)] for i in range(n+1)]
    dp[1][0] = dp[1][1] = 1
    dp[2][0] = 1
    dp[2][1] = 1
    for i in range(3, n + 1):
        dp[i][0] = dp[i - 1][0] + dp[i - 1][1]
        dp[i][1] = dp[i - 2][0]
    return dp[-1][0] + dp[-1][1]

#基础版的爬楼梯,每次爬一阶或两阶楼梯
def climb(n):
    if n < 3:
        return n
    dp = [0] * (n + 1)
    dp[1] = 1
    dp[2] = 2
    for i in range(3, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[-1]
print(climbStairs(5))
print(climb(5))

单词拆分

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true

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

最大正方形:

在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。
示例:
输入:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
输出: 4

class Solution:
    def maximalSquare(self, matrix: List[List[str]]) -> int:
        maxarea = 0
        if not matrix:
            return 0
        n = len(matrix)
        m = len(matrix[0])
        dp = [[0]*m for _ in range(n)]
        for i in range(n):
            for j in range(m):
                if matrix[i][j] == '0':continue
                width = dp[i][j] = dp[i][j-1] + 1 if j else  1
                #这里计算的从i向上每一行的宽度
                for k in range(i,-1,-1):
                	#第K行的宽度
                    width = min(width,dp[k][j])
                    #从i--k的高度
                    length = min(width,(i-k+1))
                    maxarea = max(maxarea,length**2)
        return maxarea

完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.

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

最长重复子数组

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
示例:
输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1] 。

class Solution:
    def findLength(self, A: List[int], B: List[int]) -> int:
        n = len(A)
        m = len(B)
        dp = [[0]*(m+1) for _ in range(n+1)]
        
        for i in range(1,n+1):
            for j in range(1,m+1):
                if A[i-1] == B[j-1]:
                    dp[i][j] = dp[i-1][j-1] +1
        return max(max(i) for i in dp)

最低票价

输入:days = [1,4,6,7,8,20], costs = [2,7,15]
输出:11
解释:
例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划:
在第 1 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 1 天生效。
在第 3 天,你花了 costs[1] = $7 买了一张为期 7 天的通行证,它将在第 3, 4, …, 9 天生效。
在第 20 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 20 天生效。
你总共花了 $11,并完成了你计划的每一天旅行。

class Solution:
    def mincostTickets(self, days: List[int], costs: List[int]) -> int:
        dp = [0 for _ in range(days[-1]+1)]
        idx = 0
        for i in range(1,len(dp)):
            if i !=days[idx]:
                dp[i] = dp[i-1]
            else:
                #这里的max是为了防止数组下标越界
                dp[i] = min(dp[max(0,i-1)]+costs[0], dp[max(0,i-7)+costs[1], dp[max(0,i-30)]+costs[2])
                idx +=1
        return dp[-1]

买卖股票系列

这是一类特殊的一维数组动态规划问题。换一种思路。综合一下一共有三种状态dp[i][k][j],dp[i][k][j]
代表当前所拥有的金钱或股票。i用来遍历数组元素,k表示交易次数,j代表当前拥有的是金钱还是股票。然后后续可能还有冷冻期,手续费等。冷冻期的话每次循环遍历前需要提前存储一下casetemp,循环结束前再把tempprecase然后再状态转移方程里面用precase操作即可。手续费的话随便在动态转移方程里面减去即可。状态转移方程如下:dp[i][j][0] = max(dp[i-1][j][0],dp[i-1][j[1]+prices[i]) dp[i][j][1] = max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i])

'''
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。

注意:你不能在买入股票前卖出股票。

 

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

'''
#基础动态规划写法
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        if not prices:
            return 0
        dp = [[0]*2 for _ in range(n)]
        dp[0][0] = 0
        dp[0][1] = -prices[0]
        for i in range(1,n):
            dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i])
            #主要是这里,因为只可以进行一次交易,所以只能是最初的dp[0][0]-prices[i],当写成dp[i-1][0]-prices[i]时就是无限次交易了。
            dp[i][1] = max(dp[i-1][1],dp[0][0]-prices[i])
        return dp[-1][0]

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        min_price = inf
        max_price = 0
        for price in prices:
            max_price = max(max_price,price-min_price)
            min_price = min(min_price,price)
        return max_price
'''
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

 

示例 1:

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

'''
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        case = 0
        stock = -prices[0]
        for i in range(1,len(prices)):
            case,stock = max(case,stock+prices[i]),max(stock,case-prices[i])
        return case
#贪心
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        res= 0
        for i in range(1,len(prices)):
            if prices[i] - prices[i-1]>0:
                res+=prices[i] - prices[i-1]
        return res

'''
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

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

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。   
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。   
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

'''
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices or len(prices)<2:
            return 0
        n = len(prices)
        dp = [[[j for j in range(2)] for k in range(3)] for i in range(n)]
        for k in range(3):
            dp[0][k][0] = 0
            dp[0][k][1] = -prices[0]
        for i in range(1,n):
            for j in range(1,3):
                dp[i][j][0] = max(dp[i-1][j][0],dp[i-1][j][1]+prices[i])
                dp[i][j][1] = max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i])
        return dp[n-1][2][0]
#k==任意值
class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        if not prices or len(prices)<2:
            return 0
        n = len(prices)
        if k >n//2:
            return self.max_inf(prices)
        dp = [[[i for i in range(2)] for j in range(k+1)] for i in range(n)]
        for i in range(k+1):
            dp[0][i][0] = 0
            dp[0][i][1] = -prices[0]
        for i in range(1,n):
            for j in range(1,k+1):
                dp[i][j][0] = max(dp[i-1][j][0],dp[i-1][j][1]+prices[i])
                dp[i][j][1] = max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i])
        return dp[n-1][k][0]
    def max_inf(self,prices):
        if not prices or len(prices)<2:
            return 0
        n = len(prices)
        dp = [[i for i in range(2)]for i in range(n)]
        dp[0][0] = 0
        dp[0][1] = -prices[0]
        for i in range(1,n):
            dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i])
            dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i])
        return dp[n-1][0]
 #冷冻期
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        if not prices or n<2:
            return 0
        case = 0
        stock = -prices[0]
        precase = 0
        for price in prices:
            temp = case
            case,stock = max(case,stock+price),max(stock,precase-price)
            precase = temp
        return case
 #手续费
class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n = len(prices)
        if not prices or n<2:
            return 0
        case = 0
        stock = -prices[0]
        for price in prices:
            case,stock = max(case,stock+price-fee),max(stock,case-price)
        return case


打家劫舍系列

打家劫舍:这是一种基础的一维数组问题。(1)因为不能偷邻家,所以dp[i] = max(dp[i-2]+nums[i],dp[i-1]),这里需要掌握以下滚动数组的知识:pre,cur = cur,max(pre+i,cur)#同时赋值,左边的cur相当于新的cur,右边的cur是旧的cur,右边的pre是旧的cur前面的最优值(2)因为是一个环,所以left = nums[:-1],right = nums[1:]然后分别递归两个数组求最大即可。(3)与树的结合。需要考虑两个状态,res=[0,0],res[0]表示不偷,res[1]表示偷。

'''
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,
如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:

输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:

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

'''
class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:
            return 0
        n = len(nums)
        if n==1:
            return nums[0]
        if n==2:
            return max(nums[0],nums[1])
        dp = [0 for i in range(n)]
        dp[0] = nums[0]
        dp[1] = max(nums[0],nums[1])
        res = 0
        for i in range(2,n):
            dp[i] = max(dp[i-2]+nums[i],dp[i-1])
            res = max(res,dp[i])
        return res
#简单点的程序
class Solution:
    def rob(self, nums: List[int]) -> int:
        pre,cur = 0,0
        for i in nums:
            pre,cur = cur,max(pre+i,cur)#同时赋值,左边的cur相当于新的cur,右边的cur是旧的cur,右边的pre是旧的cur前面的最优值
        return cur
'''
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。
同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
'''
#分成两个数组,一个不包含第一个,一个不包含最后一个,然后求最大值
class Solution:
    def rob(self, nums: List[int]) -> int:
        if len(nums) == 1:
            return nums[0]
        left = nums[:-1]
        right = nums[1:]
        res_l = self.find(left)
        res_r = self.find(right)

        return max(res_l, res_r)

    def find(self, x):
        pre, cur = 0, 0
        for num in x:
            pre, cur = cur, max(pre + num, cur)
        return cur
'''
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,
我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,
聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 
如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

示例 1:

输入: [3,2,3,null,3,null,1]

     3
    / \
   2   3
    \   \ 
     3   1

输出: 7 
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
'''
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def rob(self, root: TreeNode) -> int:
        #
        def robinteger(root):
            # 如果节点本身就是空的,那无论偷不偷,都拿不到钱
            res = [0, 0]
            if not root: return res
            # res[1]表示节点被偷能拿到的最多的钱
                # 节点被偷的话,其子节点不能被偷
            left = robinteger(root.left)
            right = robinteger(root.right)
            res[1] += root.val + left[0] + right[0]
            # res[0]表示不偷该节点能拿到的最多的钱
                # 不被偷的话,无论其子节点是否被偷都可以
            res[0] += max(left[0], left[1]) + max(right[0], right[1])
            return res
        res = robinteger(root)
        return max(res[0], res[1])


leetcode 5 最长回文子串(遍历子串中心点,然后分别从奇偶向两端寻找最长回文子串)

#
# @lc app=leetcode.cn id=5 lang=python3
#
# [5] 最长回文子串
#

# @lc code=start
class Solution:
    def longestPalindrome(self, s: str) -> str:
        size = len(s)
        if size < 2:
            return s
        
        # 至少是 1
        max_len = 1
        res = s[0]

        for i in range(size):
            palindrome_odd, odd_len = self.__center_spread(s, size, i, i)
            palindrome_even, even_len = self.__center_spread(s, size, i, i + 1)

            # 当前找到的最长回文子串
            cur_max_sub = palindrome_odd if odd_len >= even_len else palindrome_even
            if len(cur_max_sub) > max_len:
                max_len = len(cur_max_sub)
                res = cur_max_sub

        return res

    def __center_spread(self, s, size, left, right):
        """
        left = right 的时候,此时回文中心是一个字符,回文串的长度是奇数
        right = left + 1 的时候,此时回文中心是一个空隙,回文串的长度是偶数
        """
        i = left
        j = right

        while i >= 0 and j < size and s[i] == s[j]:
            i -= 1
            j += 1
        return s[i + 1:j], j - i - 1
        
# @lc code=end

最长回文子序列

由于是回文,所以需要用到二维dp数组,dp[i][j]表示从nums[i]到nums[j]之间的最长回文序列长度。所以若nums[i]==nums[j]则dp[i][j]==dp[i+1][j-1]+2,否则dp[i][j]==max(dp[i+1][j],dp[i][j-1]),然后还需要一个小技巧。就是画出dp矩阵后我们发现dp[i][i]==1,所以需要后序遍历或者斜着遍历。
后序:for (int i = n - 1; i >= 0; i–) 斜着:for (int l = 2; l <= n; l++) {
{for (int j = i + 1; j < n; j++)} for (int i = 0; i <= n - l; i++) {int j = l + i - 1;}

'''
给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000。

示例 1:
输入:

"bbbab"
输出:

4
一个可能的最长回文子序列为 "bbbb"。

示例 2:
输入:

"cbbd"
输出:

2
一个可能的最长回文子序列为 "bb"。

'''
class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n = len(s)
        if not s:
            return 0
        #画出状态转移矩阵就可以知道下面的base 和循环的意思了。
        dp = [[0]*(n) for _ in range(n)]
        for i in range(n):
            dp[i][i] = 1
        for i in range(n-1,-1,-1):
            for j in range(i+1,n):
                if s[i] == s[j]:
                #这里包含了两种情况,一种是重复的,一种是正常的回文
                    dp[i][j] = dp[i+1][j-1]+2
                else:
                    dp[i][j] = max(dp[i+1][j],dp[i][j-1])
        return dp[0][-1]

LCS最长公共子序列

这里是引用
字符相等dp[i][j] = dp[i-1][j-1],字符不相等dp[i][j]==max(dp[i-1][j],dp[i][j-1])

'''
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。

'''
def longestCommonSubsequence(str1, str2) -> int:
    m, n = len(str1), len(str2)
    # 构建 DP table 和 base case
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    # 进行状态转移
    #这里之所以取n+1,m+1的原因就是dp矩阵是一个(n+1,m+1)的矩阵,其中dp[0][:] == dp[:][0]==0
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if str1[i - 1] == str2[j - 1]:
                # 找到一个 lcs 中的字符
                dp[i][j] = 1 + dp[i - 1][j - 1]
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

    return dp[-1][-1]

LIS 最长上升子序列

因为dp[i]不是由dp[i-1]决定的,而dp[i]与当前元素的值有关,且有nums[i] > nums[j]: dp[i] = max(dp[i], dp[j] + 1) 这样就把dp[i]和前面求出来的dp[j]求出来了。

'''
给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

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

'''
#贪心+二分查找,考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
#基于上面的贪心思路,我们维护一个数组tail,表示长度为 ii 的最长上升子序列的末尾元素的最小值。tail并不代表最后的最小上升序列!

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        d = []
        for n in nums:
            if not d or n > d[-1]:
                d.append(n)
            else:
                l, r = 0, len(d) - 1
                loc = r
                while l <= r:
                    mid = (l + r) // 2
                    if d[mid] >= n:
                        loc = mid
                        r = mid - 1
                    else:
                        l = mid + 1
                #找到一个合适的位置把n插进去
                d[loc] = n
        return len(d)

#动态规划,dp[i]为考虑前i个元素,以第i个数字结尾的最长上升序列的长度。
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        dp = []
        for i in range(len(nums)):
            dp.append(1)
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)


最大子串和(dp[i]==max(dp[i-1]+nums[i],nums[i])因为只有这两种情况,所以直接取最大就好。)

'''
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

'''
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        if not nums:
            return 0
        n = len(nums)
        dp = [i for i in nums]
        res = dp[0]#初始化最大值
        for i in range(1,n):
            #因为是连续序列最大和,所以有下面的状态条件
            if dp[i-1] > 0:
                dp[i] = dp[i-1]+nums[i]
            else:
                dp[i] = nums[i]
            res = max(res,dp[i])#保留最大值
        return res

矩阵中从左上角到右下角的最小价值距离。

T = int(input())
for i in range(T):
    n,m = list(map(int,input().strip().split()))
    x,y = list(map(int,input().strip().split()))
    a,b,c,d = list(map(int,input().strip().split()))
    matrix = []
    for j in range(n):
        tmp = list(map(str,input().strip().split()))
        temp = []
        # print(tmp[0])
        for k in tmp[0]:
            temp.append(tmp)
        matrix.append(temp)
    dirs = [(1, 0, a), (-1, 0, b), (1, 0, c), (0, 1, d)]
    result = []
    max_index = 1000000006
    dp = [[max_index]*m for i in range(n)]
    dp[0][0] = 0
    for r in range(n):
        for c in range(m):
            for x,y,cost in dirs:
                r0 = r+x
                c0 = c+y
                if 0<=r0<n and 0<=c0<m and matrix[r0][c0] != 'x':
                    if dp[r][c]+cost < dp[r0][c0]:
                        dp[r0][c0] = dp[r][c]+cost
    print(dp[-1][-1])

乘积最大子数组(连续子串子数组的问题,我们在取状态的时候要不就是累积状态,否则就是当前状态)

由于有负数的存在,所以考虑历史的最大值和最小值。若当前值小于零,交换最大最小值(最小变最大)。也就是在上面的基础上多判断一下正负。

'''
给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字)。
示例 1:

输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:

输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
'''
class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if not nums:
            return 0
        n = len(nums)
        if n==1:
            return nums[0]
        max_cur = 1
        min_cur = 1
        res = 0
        for i in range(n):
            if nums[i]<0:
                temp = max_cur
                max_cur = min_cur
                min_cur = temp
            max_cur = max(max_cur*nums[i],nums[i])
            min_cur = min(min_cur*nums[i],nums[i])
            res = max(res,max_cur)
        return res

编辑距离

画出状态矩阵,易知若当前字符相等dp[i][j] = dp[i-1][j-1],若当前字符不相等dp[i][j]==min(替换,删除,添加)画出矩阵后就知道dp[i][j]的值由他旁边的三个位置决定,所以直接取最小值即可,管他每个是啥意思呢。

'''
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符
 

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')


'''
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        n = len(word1)
        m = len(word2)
        if n*m==0:
            return n+m
        dp = [[0]*(m+1) for _ in range(n+1)]
        for k in range(n+1):
            dp[k][0] = k
        for k in range(m+1):
            dp[0][k] = k
        for i in range(1,n+1):
            for j in range(1,m+1):
                delt = dp[i-1][j]
                ins = dp[i][j-1]
                exc = dp[i-1][j-1]
                if word1[i-1] == word2[j-1]:
                    dp[i][j] =  dp[i-1][j-1]
                else:
                    dp[i][j] = min(delt,ins,exc)+1
        return dp[n][m]

通配符匹配

跟上面一样的思路,若当前值相等或p[j-1]’?'则dp[i][j] = dp[i-1][j-1],若p[j-1]’*’,则dp[i-1][j] or dp[i][j-1] or dp[i-1][j-1],具体为什么我不管。

'''
给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。

'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。

'''
class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        n = len(s)
        m = len(p)
        if p == s or p == '*':
            return True
        if p == '' or s == '':
            return False
        dp = [[False] * (m + 1) for _ in range(n + 1)]
        dp[0][0] = True
        # 初始化第一列:对于s为"",p只有1个或者多个*才可以匹配才可以匹配
        for i in range(1, m + 1):
            dp[0][i] = dp[0][i - 1] and p[i - 1] == '*'

        for i in range(1, n + 1):
            for j in range(1, m + 1):
                if s[i - 1] == p[j - 1] or p[j - 1] == '?':
                    dp[i][j] = dp[i - 1][j - 1]
                elif p[j - 1] == '*':
                    dp[i][j] = dp[i - 1][j] or dp[i][j - 1]
        return dp[-1][-1]

正则表达式匹配

也一样,不过由于正则表达式’’ 匹配零个或多个前面的那一个元素,跟前一个有关,所以若p[j - 1] == ''时,if p[j - 2] == s[i - 1] or p[j - 2] == ‘.’:dp[i][j] = dp[i - 1][j] or dp[i][j - 2] else: dp[i][j] = dp[i][j - 2]。也就是将上面的j-1换成了j-2而已。

'''
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。

'''
class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        n = len(s)
        m = len(p)
        if p == s:
            return True

        dp = [[False] * (m + 1) for _ in range(n + 1)]
        dp[0][0] = True
        for k in range(2, m + 1):
            if p[k - 1] == '*':
                dp[0][k] = dp[0][k - 2]

        for i in range(1, n + 1):
            for j in range(1, m + 1):
                if s[i - 1] == p[j - 1] or p[j - 1] == '.':
                    dp[i][j] = dp[i - 1][j - 1]
                elif p[j - 1] == '*':
                    if p[j - 2] == s[i - 1] or p[j - 2] == '.':
                        dp[i][j] = dp[i - 1][j] or dp[i][j - 2]
                    else:
                        dp[i][j] = dp[i][j - 2]
        return dp[-1][-1]

鸡蛋掉落

状态转移很好写,因为只有两种状态,碎和没碎对应的就是楼上遍历和楼下遍历。所以这里写出两种基于不同搜索方式的程序。a、for 1 <= i <= N: res = min(res,max(dp(K - 1, i - 1), # 碎dp(K, N - i) # 没碎) + 1 # 在第 i 楼扔了⼀次)
b、二分搜索略微复杂ans = 1 + min(max(dp(k-1, x-1), dp(k, n-x)for x in (lo, hi)) 其中lo是递归中找到的楼层下界,hi是递归中找到的楼层上界,然后再存储到字典中。

'''
你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N  共有 N 层楼的建筑。

每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。

你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。

每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。

你的目标是确切地知道 F 的值是多少。

无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?

 

示例 1:

输入:K = 1, N = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,我们肯定知道 F = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,我们肯定知道 F = 1 。
如果它没碎,那么我们肯定知道 F = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 F 是多少。
示例 2:

输入:K = 2, N = 6
输出:3
示例 3:

输入:K = 3, N = 14
输出:4

'''
class Solution:
    def superEggDrop(self, K: int, N: int) -> int:
        memo = {}#保存(k,n)中间值的状态,也就是dp的真意
        def dp(k, n):
            if (k, n) not in memo:
                if n == 0:
                    ans = 0
                elif k == 1:
                    ans = n
                else:
                    lo, hi = 1, n
                    # keep a gap of 2 X values to manually check later
                    while lo + 1 < hi:
                        x = (lo + hi) // 2
                        t1 = dp(k-1, x-1)#蛋碎
                        t2 = dp(k, n-x)#蛋没碎

                        if t1 < t2:
                            lo = x
                        elif t1 > t2:
                            hi = x
                        else:
                            lo = hi = x
                    #求出最坏情况之下的最少次数
                    ans = 1 + min(max(dp(k-1, x-1), dp(k, n-x))
                                  for x in (lo, hi))

                memo[k, n] = ans
            return memo[k, n]

        return dp(K, N)

俄罗斯套娃

二维LIS算法找最长递增序列长度,将序列按照第一维升序,第二位降序排列,然后对第二维数据进行LIS, 找最长递增子串长度。巧妙的把同样宽度, 高度递增的不合法序列滤掉。envelopes.sort(key=lambda x: (x[0], -x[1]))

'''
给定一些标记了宽度和高度的信封,宽度和高度以整数对形式 (w, h) 出现。
当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。

请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。

说明:
不允许旋转信封。

示例:

输入: envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出: 3
解释: 最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。

'''

from typing import List

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        envelopes.sort(key=lambda x: (x[0], -x[1]))
        def lis(nums):
            print(nums)
            res = 0
            n = len(nums)
            dp = [1 for i in range(n)]
            for i in range(len(nums)):
                for j in range(i):
                    if nums[i] > nums[j]:
                        dp[i] = max(dp[i], dp[j] + 1)
                res = max(res, dp[i])

            return res

        return lis([k[1] for k in envelopes])
nums = [[4,5],[4,6],[6,7],[2,3],[1,1]]
A = Solution()
print(A.maxEnvelopes(nums))

三角形的最小路径

'''
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

例如,给定三角形:

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

'''

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        dp = triangle
        n = len(triangle)
        '''
        dp = triangle     #这边的初始化真的是牛逼,不仅不需要把把上面两行特殊的情况考虑进去,
		而且为下面也做了铺垫。因为是做累加,所以不需要初始化为0       
        m = len(dp) 
        #dp是一个list列表,同样也可以看成是一个二维数组,如图所示
        # [             [
        #     2,          [2],
        #     3,4        [3,4],
        #     6,5,7     [6,5,7],
        #     4,1,8,3  [4,1,8,3]
        # ]             ]
        # 可以知道3的下一行中相邻的结点为6和5,6的下一行中相邻的结点为4和1,5的下一行中相邻的结点为1和8
        '''
        for i in range(1,n):
            for j in range(len(triangle[i])):
                if j==0:
                    dp[i][j] += dp[i-1][j]#第一列的元素来源路径只可能是由上一行对应列的元素移动而来
                elif j==len(triangle[i])-1:
                    dp[i][j] += dp[i-1][j-1]
                    # 末尾的元素 如:4的来源路径只有可能是从2来
                    # PS:虽然在上面左边图中,4和6好像是相邻的,但是在三角形中,4和6不相邻
                    # 我的理解就是根据题目要求得到上面左边的图,移动方式只有向下和像45度方向移动两种情况
                else:
                    dp[i][j] += min(dp[i-1][j],dp[i-1][j-1])#中间的元素
        return min(dp[-1])#最后一行的最小值,因为我们求出来的是最后一行每个元素的最小路径

N天后的牢房

'''
8 间牢房排成一排,每间牢房不是有人住就是空着。

每天,无论牢房是被占用或空置,都会根据以下规则进行更改:

如果一间牢房的两个相邻的房间都被占用或都是空的,那么该牢房就会被占用。
否则,它就会被空置。
(请注意,由于监狱中的牢房排成一行,所以行中的第一个和最后一个房间无法有两个相邻的房间。)

我们用以下方式描述监狱的当前状态:如果第 i 间牢房被占用,则 cell[i]==1,否则 cell[i]==0。

根据监狱的初始状态,在 N 天后返回监狱的状况(和上述 N 种变化)。

 

示例 1:

输入:cells = [0,1,0,1,1,0,0,1], N = 7
输出:[0,0,1,1,0,0,0,0]
解释:
下表概述了监狱每天的状况:
Day 0: [0, 1, 0, 1, 1, 0, 0, 1]
Day 1: [0, 1, 1, 0, 0, 0, 0, 0]
Day 2: [0, 0, 0, 0, 1, 1, 1, 0]
Day 3: [0, 1, 1, 0, 0, 1, 0, 0]
Day 4: [0, 0, 0, 0, 0, 1, 0, 0]
Day 5: [0, 1, 1, 1, 0, 1, 0, 0]
Day 6: [0, 0, 1, 0, 1, 1, 0, 0]
Day 7: [0, 0, 1, 1, 0, 0, 0, 0]

'''
class Solution:
    def prisonAfterNDays(self, cells: List[int], N: int) -> List[int]:
        jk = [cells]
        N = N % 14
        for m in range(14):
            k = [None] * 8
            k[0] = 0
            k[-1] = 0
            for i in range(1, 7):
                if (jk[m][i - 1] == 0 and jk[m][i + 1] == 0) or (jk[m][i - 1] == 1 and jk[m][i + 1] == 1):
                    k[i] = 1
                else:
                    k[i] = 0
            jk.append(k)
        if N != 0:
            return jk[N]
        else:
            return jk[14]

不同路径(用到了完全背包优化方案数)

'''
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不同的路径?

'''
#dp[i][j] = dp[i-1][j]+dp[i][j-1]优化
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:

        dp = [1]*n
        for i in range(1,m):
            for j in range(1,n):
                dp[j] = dp[j] + dp[j-1]
        return dp[-1]


不同路径2:

'''
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

示例 1:

输入:
[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

'''


class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        m = len(obstacleGrid)
        n = len(obstacleGrid[0])
        if obstacleGrid[0][0] == 1:
            return 0
        obstacleGrid[0][0] = 1
        for i in range(1, m):
            if obstacleGrid[i][0] == 0:
                obstacleGrid[i][0] = obstacleGrid[i - 1][0]
            else:
                obstacleGrid[i][0] = 0
        for j in range(1, n):
            if obstacleGrid[0][j] == 0:
                obstacleGrid[0][j] = obstacleGrid[0][j - 1]
            else:
                obstacleGrid[0][j] = 0

        for i in range(1, m):
            for j in range(1, n):
                if obstacleGrid[i][j] == 0:
                    obstacleGrid[i][j] = obstacleGrid[i - 1][j] + obstacleGrid[i][j - 1]
                else:
                    obstacleGrid[i][j] = 0

        return obstacleGrid[-1][-1]


class Solution(object):
    def uniquePathsWithObstacles(self, obstacleGrid):
        """
        :type obstacleGrid: List[List[int]]
        :rtype: int
        """
        # 定义状态:即数据元素的含义:dp表示当前位置的路径条数
        # 建立状态转移方程:dp[i] = dp[i]+dp[i-1]
        # 设定初始值:增加初始值1,即dp = [1] + [0]*n
        # 状态压缩:即优化数组空间,将二维数组压缩到一维数组,逐行计算当前最新路径条数,并覆盖上一行对应的路径条数
        # 选取dp[-2]表示到达finish位置路径总条数,因为一开始新增加的1,因此最终值要往前推一个

        m = len(obstacleGrid)
        n = len(obstacleGrid[0])
        dp = [1] + [0] * n
        for i in range(0, m):
            for j in range(0, n):
                dp[j] = 0 if obstacleGrid[i][j] else dp[j] + dp[j - 1]
        return dp[-2]

最大矩阵和:

#动态规划
row,col = list(map(int,input().strip().split()))
grid = []
for i in range(row):
    tmp = list(map(int,input().strip().split()))
    grid.append(tmp)
for i in range(row):
    for j in range(col):
        if i==j==0:continue
        elif i==0: grid[i][j] = grid[i][j-1] + grid[i][j]
        elif j==0: grid[i][j] = grid[i-1][j] + grid[i][j]
        else:grid[i][j] = max(grid[i-1][j],grid[i][j-1])+grid[i][j]
print(grid[-1][-1])

#BFS搜索
def helper():
    row, col = list(map(int, input().strip().split()))
    grid = []
    for i in range(row):
        tmp = list(map(int,input().strip().split()))
        grid.append(tmp)
    result = []
    def bfs(i,j,res):
        if 0<=i<row and 0<=j<col:
            res += grid[i][j]
            for x,y in [(0,1),(1,0)]:
                bfs(i+x,j+y,res)
        if i==row-1 and j==col-1:
            result.append(res)
    bfs(0,0,0)
    return max(result)
print(helper())

出界的路径数:

'''
给定一个 m × n 的网格和一个球。球的起始坐标为 (i,j) ,你可以将球移到相邻的单元格内,
或者往上、下、左、右四个方向上移动使球穿过网格边界。
但是,你最多可以移动 N 次。找出可以将球移出边界的路径数量。答案可能非常大,返回 结果 mod 109 + 7 的值。

示例 1:

输入: m = 2, n = 2, N = 2, i = 0, j = 0
输出: 6

'''
from functools import lru_cache
import itertools
class Solution:
    def findPaths(self, m: int, n: int, N: int, i: int, j: int) -> int:
        dp = [[0]*n for _ in range(m)]
        dp[i][j] = 1
        res = 0
        dirs = [[0,1],[1,0],[-1,0],[0,-1]]
        divider = 10**9+7
        for k in range(N):
            dp2 = [[0]*n for _ in range(m)]
            for r in range(m):
                for c in range(n):
                    for item in dirs:
                        r0 = r + item[0]
                        c0 = c + item[1]
                        if r0>=0 and r0 < m and c0>=0 and c0 < n:
                        #这里之所以要加上dp2[r0][c0]的原因就是因为到达(r0,c0)这个点的路径不只有一条,很可能之前就到达了,所以得加上之前的路径数
                            dp2[r0][c0] = (dp[r][c] + dp2[r0][c0]) % divider
                        else:
                        #这里要记得加上到这个节点之前的所有路径
                            res = (res + dp[r][c]) % divider
 			#路径是递增的,所以dp也要加上这个节点的所有路径也就是dp2
            dp = dp2[:][:]

        return res

矩阵中的最长递增路径

'''
给定一个整数矩阵,找出最长递增路径的长度。

对于每个单元格,你可以往上,下,左,右四个方向移动。 你不能在对角线方向上移动或移动到边界外(即不允许环绕)。

示例 1:

输入: nums =
[
  [9,9,4],
  [6,6,8],
  [2,1,1]
]
输出: 4
解释: 最长递增路径为 [1, 2, 6, 9]。

'''
from functools import lru_cache
import itertools
class Solution:
    def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
        if not matrix:
            return 0
        r, c, d = len(matrix), len(matrix[0]), ((0, 1), (0, -1), (1, 0), (-1, 0))
        @lru_cache(None)
        def f(i, j):
            t = 0
            for di, dj in d:
                x, y = i + di, j + dj
                if 0 <= x < r and 0 <= y < c and matrix[x][y] > matrix[i][j]:
                    t = max(t, f(x, y))
            return t + 1
        return max(f(i, j) for i, j in itertools.product(range(r), range(c)))

戳气球

'''
有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。每当你戳破一个气球 i 时,你可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。

求所能获得硬币的最大数量。

说明:

你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。
0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100
示例:

输入: [3,1,5,8]
输出: 167
解释: nums = [3,1,5,8] --> [3,5,8] -->   [3,8]   -->  [8]  --> []
     coins =  3*1*5      +  3*5*8    +  1*3*8      + 1*8*1   = 167
'''
class Solution:
    def maxCoins(self, nums: List[int]) -> int:

        # reframe problem as before
        nums = [1] + nums + [1]
        n = len(nums)

        # dp will store the results of our calls
        dp = [[0] * n for _ in range(n)]

        # iterate over dp and incrementally build up to dp[0][n-1]
        for left in range(n-2, -1, -1):
            for right in range(left+2, n):
                # same formula to get the best score from (left, right) as before
                dp[left][right] = max(nums[left] * nums[i] * nums[right] + dp[left][i] + dp[i][right] for i in range(left+1, right))

        return dp[0][n-1]

二叉树的直径(深度+1)

'''
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

 

示例 :
给定二叉树

          1
         / \
        2   3
       / \
      4   5
返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。

'''

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None
#最大直径就是所有路径中的max(left+right)
class Solution:
    def diameterOfBinaryTree(self, root: TreeNode) -> int:
        self.max_tree = 0
        self.find(root)
        return self.max_tree
    def find(self,node):
        if not node:
            return 0
        left = self.find(node.left)
        right = self.find(node.right)
        self.max_tree = max(self.max_tree,(left+right))

        return max(left,right)+1

二叉树中的最大路径和

'''
给定一个非空二叉树,返回其最大路径和。

本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。

示例 1:

输入: [1,2,3]

       1
      / \
     2   3

输出: 6

'''
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

'''
    a
   / \
  b   c
b+ a + c。
b + a + a的父结点。
a + c + a的父结点。
其中情况 1,表示如果不联络父结点的情况,或本身是根结点的情况。
这种情况是没法递归的,但是结果有可能是全局最大路径和。
情况 2 和 3,递归时计算 a+b 和 a+c,选择一个更优的方案返回,也就是上面说的递归后的最优解啦。

另外结点有可能是负值,最大和肯定就要想办法舍弃负值(max(0, x))(max(0,x))。
但是上面 3 种情况,无论哪种,a 作为联络点,都不能够舍弃。

'''
class Solution:
    def maxPathSum(self, root: TreeNode) -> int:
        self.max_num = -inf
        self.find(root)
        return self.max_num
    def find(self,node):
        if not node:
            return 0
        left = max(0,self.find(node.left))
        right = max(0,self.find(node.right))
        #这里由于node.val+left+right一定大于node.val+max(left,right)这种情况,所以省略。
        self.max_num = max(self.max_num,node.val+left+right)
        return node.val+max(left,right)#向上面的父节点递归


背包DP

首先放一下01背包和完全背包的模板:

01背包:

题目
有N件物品和一个容量为v的背包。第i件物品的费用是w[i],价值是v[i],求将哪些物品装入背包可使价值总和最大。

基本思路
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即f[i][j]表示前iii件物品恰放入一个容量为j的背包可以获得的最大价值。则其状态转移方程便是:

def bag(n, c, w, v):
    """
    测试数据:
    n = 6  物品的数量,
    c = 10 书包能承受的重量,
    w = [2, 2, 3, 1, 5, 2] 每个物品的重量,
    v = [2, 3, 1, 5, 4, 3] 每个物品的价值
    """
    # 置零,表示初始状态
    res = 0
    value = [[0 for j in range(c + 1)] for i in range(n + 1)]
    for i in range(1, n + 1):
        for j in range(1, c + 1):
            value[i][j] = value[i - 1][j]
            # 背包总容量够放当前物体,遍历前一个状态考虑是否置换
            if j >= w[i - 1] and value[i][j] < value[i - 1][j - w[i - 1]] + v[i - 1]:
                value[i][j] = value[i - 1][j - w[i - 1]] + v[i - 1]
            res = max(res,value[i][j])
    return res

优化空间后的代码:

def bag1(n, c, w, v):
    values = [0 for i in range(c+1)]
    for i in range(1, n + 1):
        for j in range(c, 0, -1):
            # 背包总容量够放当前物体,遍历前一个状态考虑是否置换,这里的value[j]即为上一次最佳结果
            if j >= w[i-1]:
                values[j] = max(values[j-w[i-1]]+v[i-1], values[j])
    return values

这里之所以倒序就可以省略前面的一维变量的原因就是,从后往前计算的时候对于f来说就是从i-1到i的计算。简单点说就是,f[j]依据的是从来没有计算过的值得出来的,假如正序的话,f[j-w[i]]一定是已经算出来的结果,那么久相当于重复选取了。

完全背包问题:

题目
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

基本思路
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][j]表示前i种物品恰放入一个容量为V的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:

n, v = map(int, input().split())
goods = []
for i in range(n):
    goods.append([int(i) for i in input().split()])
dp = [0 for i in range(v+1)]
for i in range(n):
    for j in range(v,-1,-1): # 从后往前
        k = j//goods[i][0]  # 能选多少次
        # 从这些次里面取最大
        dp[j] = max([dp[j- x* goods[i][0]] + x * goods[i][1] for x in range(k+1)])

print(dp[-1])

优化后的代码:

n, v = map(int, input().split())
goods = []
for i in range(n):
    goods.append([int(i) for i in input().split()])
dp = [0 for i in range(v+1)]
for i in range(n):
    for j in range(v+1):
        if j >= goods[i][0]:
            dp[j] = max(dp[j], dp[j-goods[i][0]] + goods[i][1])

print(dp[-1])

这个代码与01背包的代码只有j的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么01背包中要按照j=V…0j=V…0j=V…0的逆序来循环。这是因为要保证第iii次循环中的状态f[i][j]是由状态f[i−1][j−w[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第iii件物品”这件策略时,依据的是一个绝无已经选入第iii件物品的子结果f[i−1][j−w[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第iii种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][j−w[i]],所以就可以并且必须采用j=0…Vj=0…Vj=0…V的顺序循环。这就是这个简单的程序为何成立的道理。值得一提的是,上面的伪代码中两层for循环的次序可以颠倒。这个结论有可能会带来算法时间常数上的优化。

背包问题中的方案数:dp[j] +=dp[j-nums[i]]

分割等和子集

其实就是一个01背包问题。sum//2等价于01背包中的重量。注意当nums[i]==j的时候,意味着满足了sum//2,所以dp[j]==True

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

注意:

每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].
 

示例 2:

输入: [1, 2, 3, 5]

输出: false

解释: 数组不能分割成两个元素和相等的子集.

'''
class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        if not nums:
            return False
        n = len(nums)
        sums = sum(nums)
        if sums%2==1:
            return False

        mid_sum = sums//2

        dp = [False]*(mid_sum+1)

        # dp[0] = True   相当于下面的if else条件
        for i in range(n):
            for j in range(mid_sum,nums[i]-1,-1):
                if nums[i] == j:
                    dp[j]=True
                else:
                    dp[j] = dp[j] or dp[j-nums[i]]
        return dp[-1]

零钱兑换(完全背包)

'''
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

 

示例 1:

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

输入: coins = [2], amount = 3
输出: -1

'''

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        n = len(coins)
        dp = [inf]*(amount+1)
        dp[0] = 0
        for i in range(n):
            for j in range(coins[i],amount+1):
                dp[j] = min(dp[j],dp[j-coins[i]]+1)
        return dp[-1] if dp[-1] != inf else -1

零钱兑换方案数

完全背包 dp[j] = dp[j-coins[i]] + dp[j]

'''
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 

 

示例 1:

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

'''
class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        n = len(coins)
        dp = [0]*(amount+1)
        dp[0] = 1
        for i in range(n):
            for j in range(coins[i],amount+1):
                dp[j] += dp[j-coins[i]]
        return dp[-1]


目标和

'''
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例 1:

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

'''

#方法解析:
'''
我们假设P是正子集,N是负子集。让我们看看如何将其转换为子集求和问题:

                  sum(P) - sum(N) = target
                  (两边同时加上sum(P)+sum(N))
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
            (因为 sum(P) + sum(N) = sum(nums))
                       2 * sum(P) = target + sum(nums)
因此,原来的问题已转化为一个求子集的和问题: 找到nums的一个子集 P,使得

sum(P) = (target + sum(nums))//2

状态转移方程:
遍历nums,遍历的数记作num
再逆序遍历从P到num,遍历的数记作j
更新dp[j] = dp[j - num] + dp[j]	这里的含义是,dp[j]的方法数==选择num时候的方法数再加上不选择num时候的方法数!!!
dp[0]==1  这里的含义是,方法数位0的方案就是什么都不选。dp[0]==dp[0][0]
'''
class Solution:
    def findTargetSumWays(self, nums: List[int], S: int) -> int:
        n = len(nums)
        sums = sum(nums)
        if sums<S or (sums+S)%2==1:
            return 0

        res = (sums+S)//2

        dp = [0]*(res+1)
        dp[0] = 1
        for i in range(n):
            for j in range(res,nums[i]-1,-1):
                dp[j] += dp[j-nums[i]]
        return dp[-1]

一和零 二维背包

二维01背包问题。dp[j][k] = max(dp[j][k], dp[j - count_0][k - count_1] + 1)

'''
在计算机界中,我们总是追求用有限的资源获取最大的收益。

现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。

你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。

注意:

给定 0 和 1 的数量都不会超过 100。
给定字符串数组的长度不会超过 600。
示例 1:

输入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
输出: 4

解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。

'''
class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        L = len(strs)
        dp = [[0] * (n + 1) for _ in range(m + 1)]

        for i in range(L):
            count_0 = self.find_0(strs[i])
            count_1 = self.find_1(strs[i])
            for j in range(m, count_0 - 1, -1):
                for k in range(n, count_1 - 1, -1):
                    dp[j][k] = max(dp[j][k], dp[j - count_0][k - count_1] + 1)
        return dp[-1][-1]

    def find_0(self, s):
        res = 0
        for i in s:
            if i == '0':
                res += 1
        return res

    def find_1(self, s):
        res = 0
        for i in s:
            if i == '1':
                res += 1
        return res
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值