Leetcode每日好多题+动态规划

文章目录

  • 第一章 每日一题
    • 1、Leetcode 115:不同的子序列
    • 2、Leetcode 92:反转链表II
    • 3、Leetcode 1603 :设计停车系统
    • 4、Leetcode 290:单词规律
    • 5、Leetcode 6:Z字形变换
    • 6、Leetcode 914:卡牌分组
    • 7、面试题 01.08 零矩阵:
    • 8、Leetcode 150:逆波兰表达式求值
    • 9、Leetcode 232:用栈实现队列
    • 10、Leetcode 503:下一个更大元素II
    • 11、Leetcode 131:分割回文串
  • 第二章 动态规划
    • 1、Leetcode 62:不同路径
    • 2、Leetcode 63:不同路径II
    • 3、Leetcode 1143:最长公共子序列
    • 4、Leetcode 70:爬楼梯
    • 5、Leetcode 120:三角形的最小路径和
    • 6、Leetcode 55: 最大子序和
    • 7、Leetcode 152:乘积最大子树和
    • 8、Leetcode 332:零钱兑换问题
    • 9、Leetcode 198:打家劫舍
    • 10、Leetcode 213:打家劫舍
    • 10、Leetcode 121:买卖股票的最佳时机
    • 11、Leetcode 122:买入股票的最佳时机II
    • 12、Leetcode 123:买卖股票的最佳时机III
    • 13、Leetcode 309:最佳买卖股票时机含冷冻期
    • 14、Leetcode 188:买卖股票的最佳时机IV
    • 15、Leetcode 714:买卖股票的最佳时机含手续费
    • 16、Leetcode 32:最长有效括号
    • 17、Leetcode 64:最小路径和
    • 18、Leetcode 72 :编辑距离
    • 19、Leetcode 91:解码方法
    • 20、Leetcode 221:最大正方形
    • 21、Leetcode 403:青蛙过河
    • 22、Leetcode 410:分割数组的最大值
    • 23、Leetcode 552:学生出勤记录II
    • 24、Leetcode 647:回文子串
    • 25、Leetcode 76:最小覆盖子串
    • 26、Leetcode 312:戳气球
    • 总结

第一章 每日一题

1、Leetcode 115:不同的子序列

题目描述
在这里插入图片描述

解题思路
转移方程的话已经在代码中写到了,解题思路感觉这个大佬写的最清晰,即如果当前对比的位置字符相同,我可以选择进行匹配或者不进行匹配,因为从右往左的顺序进行匹配的话,即使相同前面可能也会有相同的,所以有两种选择。
在这里插入图片描述

代码实现

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        #统计两个字符串的长度
        m,n=len(s),len(t)
        #以t为子串,如果t比s长,不符合题意
        if m<n:
            return 0
        #建立储存的状态矩阵
        dp=[[0]*(n+1) for _ in range(m+1)]
        #初始化,如果n=0,那么它可以使s的任何子串
        for i in range(m+1):
            dp[i][n]=1
        #如果m=0,没有字串
        #开始遍历
        for i in range(m-1,-1,-1):
            for j in range(n-1,-1,-1):
                #如果当前字母匹配
                if s[i]==t[j]:
                    #那么有可能是从s+1,j+1转移,也可能是s+1,j转移
                    dp[i][j]=dp[i+1][j+1]+dp[i+1][j]
                else:
                    #如果不相等,只能考虑s+1,j
                    dp[i][j]=dp[i+1][j]
        return dp[0][0]

2、Leetcode 92:反转链表II

题目描述

在这里插入图片描述

解题思路

  • 先将指针定位到反转区域的前一个元素,定义pre,cur,next
  • 对于需要反转的区域,每次不算的把pre cur next的位置翻转为pre next cur。pre是固定的

代码实现

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def reverseBetween(self, head: ListNode, left: int, right: int) -> ListNode:
        #设置头节点
        dummy=ListNode(-1)
        dummy.next=head
        #前节点
        pre=dummy
        #定位到left的前一个位置
        for i in range(left-1):
            pre=pre.next
        #定位到left的第一个位置
        cur=pre.next
        #遍历需要反转的区域
        for j in range(right-left):
            #找到前一个节点
            next=cur.next
            #将cur指向前一个的前一个
            cur.next=next.next
            #将next指向cur
            next.next=pre.next
            #将pre指向next
            pre.next=next
        return dummy.next

3、Leetcode 1603 :设计停车系统

题目描述
在这里插入图片描述

解题思路
这题想通过还是很简单的,不做赘述了。

代码实现

class ParkingSystem:

    def __init__(self, big: int, medium: int, small: int):
        self.big=big
        self.medium=medium
        self.small=small


    def addCar(self, carType: int) -> bool:
        if carType==3:
            if self.small>=1:
                self.small-=1
                return True
            else:
                return False
        elif carType==2:
            if self.medium>=1:
                self.medium-=1
                return True
            else:
                return False
        else:
            if self.big>=1:
                self.big-=1
                return True
            else:
                return False

4、Leetcode 290:单词规律

题目描述
在这里插入图片描述

解题思路

  • 使用两个哈希表
    word2char:存储当前词key对应的字母value
    char2word:存储当前字母key对应的单词value
  • 那么判断其为false的就有两个条件
    1、当前词已经出现在word2char中了,但对应的char并不是当前的char
    2、当前char已经出现在char2word里了,但对应的word不是当前的word
  • 使用zip(pattern,word_list)进行打包遍历

代码实现

class Solution:
    def wordPattern(self, pattern: str, s: str) -> bool:
        word2chr={}
        chr2word={}
        #如果长度不相等,直接返回错
        word_list=s.split()
        if len(pattern)!=len(word_list):
            return False
        for char,word in zip(pattern,word_list):
            #如果词在词典中出现,但对应的字母和word2chr的不同
            #如果字母在字典中出现,但对应的词和chr2word存储的不同
            if (word in word2chr and word2chr[word] !=char) or (char in chr2word and chr2word[char]!=word):
                return False
            word2chr[word]=char
            chr2word[char]=word
        return True

5、Leetcode 6:Z字形变换

题目描述
在这里插入图片描述

解题思路
把这道题想得简单些,加入输入的numsRows有三行,那么添加字符串中的顺序应该是012|1|012|1
也就是说只要定义一个变量去存储方向状态即可

  • i==0时,flag为+1
  • 达到i==row-1后,flag为-1

代码实现

class Solution:
    def convert(self, s: str, numRows: int) -> str:
        if numRows < 2: return s
        res = ["" for _ in range(numRows)]
        i, flag = 0, -1
        for c in s:
            res[i] += c
            if i == 0 or i == numRows - 1: flag = -flag
            i += flag
        return "".join(res)

作者:jyd
链接:https://leetcode-cn.com/problems/zigzag-conversion/solution/zzi-xing-bian-huan-by-jyd/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

6、Leetcode 914:卡牌分组

题目描述
在这里插入图片描述

代码实现+步骤详解

class Solution:
    def hasGroupsSizeX(self, deck: List[int]) -> bool:
        #统计牌组中每一种牌出现的次数
        count_list=collections.Counter(deck)
        #统计长度
        n=len(deck)
        #对X进行遍历
        for x in range(2,n+1):
            #条件1:x是长度的约数
            if n%x==0:
                #条件2:x是每个牌出现次数的约数
                if all(v%x==0 for v in count_list.values()):
                    return True
        #如果遍历完所有x取值的可能都没达到条件,返回False
        return False

7、面试题 01.08 零矩阵:

题目描述
在这里插入图片描述

代码实现
第一次遍历,存储matrix位置为0的row和col,第二次遍历将row和
col为0的所有位置都变成0。

class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """
        Do not return anything, modify matrix in-place instead.
        """
        row=len(matrix)
        col=len(matrix[0])
        r,c=[0]*row,[0]*col
        for i in range(row):
            for j in range(col):
                if matrix[i][j]==0:
                    r[i]=1
                    c[j]=1
        for i in range(row):
            for j in range(col):
                if r[i]==1 or c[j]==1:
                    matrix[i][j]=0

8、Leetcode 150:逆波兰表达式求值

题目描述
在这里插入图片描述

解题思路
维护一个栈,遍历tokens

  • 如果token为数字,压栈
  • 当遇到了加减乘除符号,拿出栈的后两个进行计算后,将计算值压栈。
  • 返回stack中最后的值。

代码实现

class Solution:
    def evalRPN(self, tokens: List[str]) -> int:
        stack=[]
        for token in tokens:
            if token in ["+","-","/","*"]:
                b=stack.pop()
                c=stack.pop()
                if token=="*":
                    stack.append(b*c)
                elif token=="/":
                    stack.append(int(c/b))
                elif token=="+":
                    stack.append(b+c)
                else:
                    stack.append(c-b)
            else:
                stack.append(int(token))
        return int(stack[-1])

9、Leetcode 232:用栈实现队列

题目描述

在这里插入图片描述

解题思路
这种简单的题是真实存在的么…

代码实现

class MyQueue:

    def __init__(self):
        """
        Initialize your data structure here.
        """
        self.queue=[]


    def push(self, x: int) -> None:
        """
        Push element x to the back of queue.
        """
        self.queue.append(x)

    def pop(self) -> int:
        """
        Removes the element from in front of queue and returns that element.
        """
        return self.queue.pop(0)


    def peek(self) -> int:
        """
        Get the front element.
        """
        return self.queue[0]

    def empty(self) -> bool:
        """
        Returns whether the queue is empty.
        """
        return len(self.queue)==0

10、Leetcode 503:下一个更大元素II

题目描述

在这里插入图片描述

解题思路
做多了dp感觉这种题还是很简单的。主要说一下三个核心点:

  • 1、因为是循环数组,所以遍历n*2-1次,每个位置用i对n取模达到定位
  • 2、代码中,res列表存储的是每个位置的下一个最大值,stack中存储的是nums中每个的下标。
  • 3、对于当前nums[i]的值来说,遍历stack中其他的下标,如果stack中其他下标对应的值小于了nums[i],那么先要将res对应的位置添加nums[i],说明这些位置的下一个最大值就是nums[i],然后再把nums[i]压栈去寻找nums[i]的下一个最大元素。

代码实现

class Solution:
    def nextGreaterElements(self, nums: List[int]) -> List[int]:
        n=len(nums)
        #维护一个列表储存每个下标对应的下一个最大值
        res=[-1]*n
        #建立一个栈,存储的是下标
        stack=[]
        #因为是循环数组,遍历2n次
        for i in range(n*2-1):
            #只要栈中的元素比当前元素小,就pop
            while stack and nums[stack[-1]]<nums[i%n]:
                res[stack.pop()]=nums[i%n]
            stack.append(i%n)
        return res

11、Leetcode 131:分割回文串

题目描述
在这里插入图片描述

解题思路
这道题分为递归和dp两个部分
1、dp部分::判断
判断i到j是否为回文串,如果当前i和j的位置相等,且i+1到j-1的位置也是回文串,那么 d p [ i ] [ j ] dp[i][j] dp[i][j]是回文串。
2、递归部分::遍历
对于当前位置的i,遍历i后面的所有元素,判断i,j是否为回文串,如果是,将level中存储这部分的值,因为i,j已经是回文串了,那么要对j+1开始判断是j+1,n是否为回文串,当j到达n时,把这部分的值存储到res中。返回上一层,每返回一层还要讲该层的信息删除pop掉。

代码实现

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        n=len(s)
        #DP部分,表示i-j部分的字符串是否为回文串的判断
        dp=[[True]*n for _ in range(n)]
        for i in range(n-1,-1,-1):
            for j in range(i+1,n):
                #i=j是回文串的条件是当前元素对应值相等且i+1:j-1也是回文串
                dp[i][j]=dp[i+1][j-1] and s[i]==s[j]
        #存储所有结果
        res=[]
        #递归过程中存储每个结果
        level=[]
        def dfs(i):
            #终止条件:
            if i==n:
                res.append(level[:])
                return
            #对于每个i进行分析
            for j in range(i,n):
                #如果当前是回文串
                if dp[i][j]:
                    level.append(s[i:j+1])
                    #对于j+1开始判断
                    dfs(j+1)
                    #删除前面的可能
                    level.pop()
        dfs(0)
        return res

第二章 动态规划

分治+最优子结构
将一个复杂的问题分解成很多简单的子问题
关键点

  • 动态规划和递归没有根本上的区别
  • 共性:找到重复子问题
  • 差异性:最优子结构,中途可以淘汰次优解

在这里插入图片描述

1、Leetcode 62:不同路径

题目描述
在这里插入图片描述
解题思路:

  • 核心:对于没有障碍物的,每一个点要么从上面过来,要么从左面过来。
  • 特殊情况:最左边的一列和最上面的一行只能平行的过来,没有第二条路。

代码实现
Approach 1
每一个位置都由左边或者上边状态转移得到

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        #建立dp矩阵
        dp=[[0] *(n+1) for i in range(m+1)]
        for i in range(m+1):
            dp[i][1]=1
        for j in range(n+1):
            dp[1][j]=1
        #开始循环
        for i in range(2,m+1):
            for j in range(2,n+1):
                dp[i][j]=dp[i-1][j]+dp[i][j-1]
        return dp[m][n]

Approach 2
只需要维护一层的状态列表就可以,因为每个点的状态等于上面的状态加左边的状态,在累加每一行的时候,上面的状态自动地被类加上了。

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-1]
        return dp[-1]

2、Leetcode 63:不同路径II

题目描述

在这里插入图片描述

解题思路
和上一题思路差不多,只不过对于dp的状态更新要保证点不是障碍物即可

代码实现

#二维矩阵
class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        #行和列
        row=len(obstacleGrid)
        col=len(obstacleGrid[0])
        #建立dp矩阵
        dp=[[0]*col for i in range(row)]
        #初始化矩阵的值
        for i in range(col):
            if obstacleGrid[0][i]==0:
                dp[0][i]=1
            else:
                break
        for j in range(row):
            if obstacleGrid[j][0]==0:
                dp[j][0]=1
            else:
                break
        #开始遍历
        for i in range(1,row):
            for j in range(1,col):
                if obstacleGrid[i][j]!=1:
                    dp[i][j]=dp[i-1][j]+dp[i][j-1]
        return dp[-1][-1]

为了让j可以在边界取到j-1,在每一行的左边加入一个1充当障碍物,所以最后也要返回dp[-2]

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        #行和列
        row=len(obstacleGrid)
        col=len(obstacleGrid[0])
        #建立一维dp矩阵
        dp=[1]+[0]*col
        #开始遍历
        for i in range(0,row):
            for j in range(0,col):
                if obstacleGrid[i][j]:
                    dp[j]=0
                else:
                    
                    dp[j]+=dp[j-1]
        return dp[-2]

3、Leetcode 1143:最长公共子序列

题目描述

在这里插入图片描述

解题思路
状态转移:
在这里插入图片描述
可以分为两种情况:

  • 当前对比的字母相同时: d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j]=dp[i-1][j-1]+1 dp[i][j]=dp[i1][j1]+1
  • 不同时: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j]=max(dp[i-1][j],dp[i][j-1]) dp[i][j]=max(dp[i1][j],dp[i][j1])

代码实现

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        #判断特殊条件
        if not text1 or not text2:
            return 0
        #两个字符串的长度
        m=len(text1)
        n=len(text2)
        #建立dp矩阵
        dp=[[0]*(n+1) for i in range(m+1)]
        for i in range(1,m+1):
            for j in range(1,n+1):
                #如果当前对比的字母是一样的,那么就是i-1和j-1的最长公共子序列+1
                if text1[i-1]==text2[j-1]:
                    dp[i][j]=dp[i-1][j-1]+1
                #如果当前对比的字母不相同,那么就是i-1,j或者i,j-1两者之间最长的公共子序列。
                else:
                    dp[i][j]=max(dp[i-1][j],dp[i][j-1])
        return dp[-1][-1]

4、Leetcode 70:爬楼梯

重写,不多解释了
代码

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

5、Leetcode 120:三角形的最小路径和

题目描述
在这里插入图片描述
解题思路

  • 对于三角形两条边上的点,它的值只能是沿着边的上一个状态加自己本身得值得到,而在三角形内的点可以由左上或右上的状态加自己本身的值得到,取最小的值即可。

代码

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        #建立一个三角形的动态矩阵
        n=len(triangle)
        dp=[[0]*n for i in range(n)]
        #初始化,第一层的值就等于他本身
        dp[0][0]=triangle[0][0]
        #开始遍历
        for i in range(1,n):
            #三角形左斜边的值的累加只能来自于边
            dp[i][0]=dp[i-1][0]+triangle[i][0]
            for j in range(1,i):
                #不在三角形边上的值可以来自于左斜上方或者右斜上
                dp[i][j]=min(dp[i-1][j-1],dp[i-1][j])+triangle[i][j]
            #三角形右斜边的值累加只能来自于边
            dp[i][i]=dp[i-1][i-1]+triangle[i][i]
        #返回最后一行的最小值
        return min(dp[n-1])

6、Leetcode 55: 最大子序和

题目描述
在这里插入图片描述

解题思路

这道题已经写了很多遍了,就不多说。

代码实现

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        #特殊情况,如果长度为1,输出本身
        if len(nums)==1:
            return nums[0] 
        #定义两个变量,一个存储当前的0,i的和,一个存储上一次的
        cur=last=nums[0]
        #定义一个储存最大值的变量
        max_count=nums[0]
        #遍历 
        for i in range(1,len(nums)):
            #如果之前的加当前值比当前值还小:
            if last+nums[i]<=nums[i]:
                #储存的状态变为当前值
                cur=nums[i]
            else:
                cur=last+nums[i]
            #对比当前的最大子序和max的关系
            if max_count<cur:
                #更新
                max_count=cur
            #更新last
            last=cur
        #返回最大值
        return max_count

7、Leetcode 152:乘积最大子树和

题目描述
在这里插入图片描述

解题思路
因为列表中有负数的存在,如果对于每个位置的遍历只看它子序列中最大的乘积乘以它和它自身的话,是不正确的。对于当前位置为负数,乘以它子序列的最小值,即也为负的情况才是对于当前位置的最大状态。
所以在进行动态规划时,对于每个时刻,它的前面子序列的最大乘积就是在max(i-1),min(i-1)和nums[i]中选择最大的。那么计算min(i-1),就是在max(i-1),min(i-1),和nums[i]中选择最小的。

代码实现

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if not nums:
            return 0
        #最大值的状态统计
        max_pre=nums[0]
        #最小值的状态统计
        min_pre=nums[0]
        #最后输出的值
        res=nums[0]
        #开始遍历
        for i in range(1,len(nums)):
            #如果i为正数,那要判断乘不乘当前的值
            max_cur=max(max_pre*nums[i],min_pre*nums[i],nums[i])
            min_cur=min(max_pre*nums[i],min_pre*nums[i],nums[i])
            #更新一下最大值
            res=max(max_cur,res)
            #移动
            max_pre=max_cur
            min_pre=min_cur
        return res

8、Leetcode 332:零钱兑换问题

题目描述
在这里插入图片描述

解题思路

对于amount+1,遍历一遍coins中的硬币c,它的转移是从amount+1-c这个状态加1得到的。即对于凑11元,coins中有一个5元,3元,和1元,那么可行的方案就是:

  • 凑6元+一个五元硬币
  • 凑8元+一个三元硬币
  • 凑10元+加一个1元硬币
    因为都是加了一个硬币,那么就要对比的是凑6、8、10元中需要最小的硬币数。
    在代码中多cur代表使用最少的硬币书凑得到的amount,每次要和amount+1对比一下,因为如果cur一直没更新,说明coins中凑不出来amount+1

代码实现

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        #建立dp
        dp=[0]*(amount+1)
        #当前最大值
        
        #开始遍历:
        for i in range(1,amount+1):
            cur=amount+1
            #遍历每个coins
            for c in coins:
                #如果c比当前target小的话
                if c<=i:
                    cur=min(cur,dp[i-c])
            dp[i]=cur+1 if cur <amount+1 else amount+1
        return -1 if dp[-1]==amount+1 else dp[-1]

9、Leetcode 198:打家劫舍

题目描述

在这里插入图片描述

解题思路
对于长度为0 :i的偷窃方案,只能选择从0 :i-2加上当前的值或者0:i-1的值。
在这里n-1和n-2的状态值是一样的。

代码实现

class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:
            return 0
        #如果只有一个数
        if len(nums)==1:
            return nums[0]
        #建立dp列表
        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[len(nums)-1]

10、Leetcode 213:打家劫舍

题目描述
在这里插入图片描述
解题思路
与第九题的状态转移是一样的概念,但是加入了环以后,第一个和最后一个连接到了一起,表示第一个和最后一个不可能同时偷盗,故将列表刨除第一个和最后一个分开讨论,取到最大值。

代码实现

class Solution:
    def rob(self, nums: List[int]) -> int:
        if len(nums)==1:
            return nums[0]
        def Dp(nums):
            n=len(nums)
            if not nums:
                return 0
            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]
        p1=Dp(nums[1:])
        p2=Dp(nums[:-1])
        return max(p1,p2)

10、Leetcode 121:买卖股票的最佳时机

题目描述
在这里插入图片描述

解题思路

使用dp列表去存储对于0到n每个时刻的利润最大的值。对于每个时刻i,有两种操作:

  • 不卖:那它的值等于i-1时刻的值
  • 卖:它的值等于当前时刻股票的价格减去前面所有时刻的最低价格
    取两种情况较大的值,保存在dp列表中。

在这里插入图片描述

代码实现

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n=len(prices)
        #特殊条件
        if n==0:
            return 0

        #动态列表
        dp=[0]*n
        #定义一个存储最低价格的变量
        min_price=prices[0]
        #开始遍历
        for i in range(n):
            #找到对于每个时刻前面最低的价格
            min_price=min(min_price,prices[i])
            #对于每个时刻,要么不抛,要么卖掉,利润为当前价格减去之前的最低价格
            dp[i]=max(dp[i-1],prices[i]-min_price)
        return dp[-1]

11、Leetcode 122:买入股票的最佳时机II

题目描述
在这里插入图片描述

解题思路
现在是凌晨12:30,写不动了,大致的思路是,对于dp增加一个维度表示当前是否持有股票的状态。分情况讨论

  • 当前持有:那么最大利润就是继续持有,或者从上一个没持有的时刻以当前时刻的价格买入的二者最大值。
  • 当前未持有,最大利润就是上一时刻也没持有和上一时刻持有但当前时刻卖出的二者最大值。
    附上官方解释:
    在这里插入图片描述

代码实现

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n=len(prices)
        dp=[[0]*2for _ in range(n)]
        #如果第一天不持有股票,利润为0
        dp[0][0]=0
        #如果第一天就买入,利润为负
        dp[0][1]=-prices[0]
        for i in range(1,n):
            #如果第i天没持有,可能上一时刻也没有,也可能i-1持有,今天卖掉了
            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])
        #最后时刻持有一定小于不持有
        return dp[n-1][0]

12、Leetcode 123:买卖股票的最佳时机III

题目描述
在这里插入图片描述

解题思路
对于在第i天的状态,大概有四个操作:

  • 买入了一次
  • 买卖一次
  • 买卖一次+买入一次
  • 买卖两次

状态转移方程:
在这里插入图片描述
初始化的时候,在i=0天的时候,如果是买入一次和两次都是-prices[0],如果是买卖一次和买卖两次,对于第一天,手里的收益为0.

代码实现

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n=len(prices)
        #初始化状态
        #对于只买入一次和买入两次
        buy1=-prices[0]
        buy2=-prices[0]
        #对于买入卖出一次和两次
        sell1,sell2=0,0
        #开始循环
        for i in range(1,n):
            #对于买入一次,可以选择什么都不做,和当前时刻买入
            buy1=max(buy1,-prices[i])
            #对于买卖一次的,可以选择当前继续持有,也可以选择卖出
            sell1=max(sell1,buy1+prices[i])
            #对于买入两次,可以选择当前不买入,也可以选择卖过一次后买入
            buy2=max(buy2,sell1-prices[i])
            #对于买卖两次,可以选择当前继续持有,也可以选择卖出
            sell2=max(sell2,buy2+prices[i])
        return sell2

13、Leetcode 309:最佳买卖股票时机含冷冻期

题目描述
在这里插入图片描述

解题思路
三个状态:

  • 当前时刻持有
    对于当前时刻持有,可能是前一天也持有,也可能是前一天未持有但不在冷静期,然后今天买入
  • 当前时刻未持有,且在冷静期
    如果当前处于冷静期,说明前一天必然进行了卖出,则从前一天持有的状态+prices[i]
  • 当前时刻未持有,且不在冷静期
    第一种为前一天也未持有,不在冷静期,也可能是前一天未持有,但在冷静期。

取n-1时刻最大的值

代码实现

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n=len(prices)
        #dp列表
        dp=[[0]*3 for i in range(n)]
        #状态初始化
        dp[0][0]=-prices[0]
        dp[0][1],dp[0][2]=0,0
        for i in range(1,n):
            #对于当前持有股票,可能是一直持有,也可能是在前一天且不在冷静期下当天买入
            dp[i][0]=max(dp[i-1][0],dp[i-1][2]-prices[i])
            #对于当前未持有,且在冷静期中,说明前一天持有,今天卖出了
            dp[i][1]=dp[i-1][0]+prices[i]
            #对于当天未持有,且不在冷静期,可能是前一天也未持有,也可能是在前一天处于冷静期
            dp[i][2]=max(dp[i-1][2],dp[i-1][1])
        return max(dp[n-1][1],dp[n-1][2])

14、Leetcode 188:买卖股票的最佳时机IV

题目描述
在这里插入图片描述

解题思路
将dp列表扩充到二维数组,第二个维度储存完成了第j次交易。
那么对于i时刻当前持有股票的buy来说:

  • i-1时刻就已经持有了
  • i-1时刻未持有,在当前时刻进行一个买入

对于i时刻当前未持有股票

  • i-1时刻未持有
  • i-1时刻未持有,交易次数为j-1,当前时刻选择卖出,完成了j笔交易

代码实现

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        if not prices:
            return 0
        n=len(prices)
        #将k的范围缩小
        k=min(k,n//2)
        #建立dp矩阵
        buy=[[0]*(k+1) for _ in range(n)]
        sell=[[0]*(k+1) for _ in range(n)]
        #初始化buy
        buy[0][0]=-prices[0]

        for i in range(1,k+1):
            buy[0][i]=-99999
            sell[0][i]=-99999
            
        #开始循环
        for i in range(1,n):
            #j=0,不需要考虑sell
            buy[i][0]=max(buy[i-1][0],sell[i-1][0]-prices[i])
            for j in range(1,k+1):
                #对于当前持有,可能是上一个时刻就持有,也可能是上一时刻未持有当前时刻买入
                buy[i][j]=max(buy[i-1][j],sell[i-1][j]-prices[i])
                #对于当前未持有,可能是上一个时刻未持有,也可能是上一时刻持有,当前时刻卖出,完成了第j笔交易
                sell[i][j]=max(sell[i-1][j],buy[i-1][j-1]+prices[i])
        #最大利润一定是卖出的
        return max(sell[n-1])

15、Leetcode 714:买卖股票的最佳时机含手续费

题目描述
在这里插入图片描述

解题思路
这题与买卖股票的最佳时机ii类似,但多了一个手续费,由于一次买入卖出只交一次手续费,那么选择在卖出时交手续费即可。两种状态:

  • 当前时刻持有
  • 当前时刻未持有

代码实现

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n=len(prices)
        dp=[[0]*2 for _ in range(n)]
        #初始化
        #持有股票
        dp[0][0]=-prices[0]
        #未持有股票
        dp[0][1]=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]-fee)
        #最大利润一定是未持有的状态
        return dp[n-1][1]

16、Leetcode 32:最长有效括号

题目描述
在这里插入图片描述

解题思路
解释起来感觉好复杂,直接看题解吧,,,
在这里插入图片描述

代码实现

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n=len(s)
        if n==0:
            return 0
        #建立dp
        dp=[0]*n
        #开始遍历
        for i in range(n):
            if i>0 and s[i]==")":
                if s[i-1]=="(":
                    dp[i]=dp[i-2]+2
                elif s[i-1]==")" and 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]
        return max(dp)

17、Leetcode 64:最小路径和

题目描述
在这里插入图片描述

解题思路
这道题的思路其实和之前的不同路径的思路是一样的,只不过在计算中计算的是每一个格子的具体值是多少,因为走法只能是往下走或往右走,所以对于一个不在上边界和左边界的格子,它要么从上面来,要么从左边来,选择两个前状态最小的路径值。对于边界条件的考虑,除了初始点为当前值以外,上边界和左边界的值只能来自左边和上面,对于这两条路径来说,它的值路径值是固定的。

代码实现

class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        #定义一下矩阵的长宽
        row=len(grid)
        col=len(grid[0])
        #建立dp
        dp=[[0]*(col) for _ in range(row)]
        dp[0][0]=grid[0][0]
        #边际处理
        #对于最上层的格子只能横着走
        for j in range(1,col):
            dp[0][j]=dp[0][j-1]+grid[0][j]
        #对于j=0的格子只能往下走
        for i in range(1,row):
            dp[i][0]=dp[i-1][0]+grid[i][0]
        #开始遍历
        for i in range(1,row):
            for j in range(1,col):
                #每一个位置可能是从上面或者左边过来的
                dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j]
        return dp[-1][-1]

18、Leetcode 72 :编辑距离

题目描述

在这里插入图片描述

解题思路
虽然在dp中是困难题,但做过好多遍了,对于当前i和j相等的情况很好分析。当二者不相等时,就是插入、添加和删除三种情况

  • 删除:i和j都删除的话,转移为 d p [ i − 1 ] [ j − 1 ] + 1 dp[i-1][j-1]+1 dp[i1][j1]+1
  • insert,以word1为基准的话,要插入一个才能保证word1和word2相同,那么对于word1的i等于word2的j-1
  • 移除,以word2为基准的话,要移除一个才保证word1和word2相同,那么就是 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j]+1

代码实现

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        #定义两个word的长度
        m=len(word1)
        n=len(word2)
        #建立dp
        dp=[[0]*(n+1) for _ in range(m+1)]
        #考虑边界条件,如果word1为空,
        for i in range(1,n+1):
            dp[0][i]=i
        #如果word2为空
        for j in range(1,m+1):
            dp[j][0]=j
        #开始遍历
        for i in range(1,m+1):
            for j in range(1,n+1):
                #如果当前字符相同
                if word1[i-1]==word2[j-1]:
                    dp[i][j]=dp[i-1][j-1]
                else:
                    dp[i][j]=min(dp[i-1][j],
                                dp[i-1][j-1],
                                dp[i][j-1])+1
        return dp[-1][-1]

19、Leetcode 91:解码方法

题目描述

在这里插入图片描述

解题思路
对于状态的转移,可以有以下两种情况:

  • 对于当前时刻i对应的数字不为0,那么它自己就可以算一种情况,与i-1的状态应该一致,因为目标结果是有多少种解码的方法
  • 对于当前时刻的对应的数字,要考虑它和前一位进行组合,如果组合后的数字是在10-26这个区间,那么对于dp[i+1]这个状态它还要加上从dp[i-1]过来的。

代码实现

class Solution:
    def numDecodings(self, s: str) -> int:
        n=len(s)
        if n==0:
            return 0
        #建立dp状态列表
        dp=[0]*(n+1)
        #如果第一位是0,无法编译
        if s[0]=="0":
            return 0
        #初始化
        dp[0],dp[1]=1,1
        #开始遍历
        for i in range(1,n):
            #只要s[i]不等于0,那么它自身可以编译
            if s[i]!="0":
                dp[i+1]=dp[i]
                #计算i-1和i是否可以组成一个被编译的十位数
            nums=10*(ord(s[i-1])-ord("0"))+ord(s[i])-ord("0")
            if 10<=nums<=26:
                dp[i+1]+=dp[i-1]
        return dp[n]

20、Leetcode 221:最大正方形

题目描述
在这里插入图片描述

解题思路
对于 d p [ i ] [ j ] dp[i][j] dp[i][j]的定义是以(i,j)为右底角所能达到的最多的正方形的个数,初始化的限制条件为对于上层和左边的每一个点值为1,那么最多就是1,值为0,则为0。
对于那些在matrix中值为1的点,它的状态转移来自于(i-1,j-1)(i,j-1)(i-1,j)中最小的加1,因为要在三个方向上保证都是正方形,那么要取最小的,否则不满足正方形的边长的限制。由于返回值不一定由最后时刻的dp决定,所以要维护一个变量去在迭代中更新最小值。

代码实现

class Solution:
    def maximalSquare(self, matrix: List[List[str]]) -> int:
        row=len(matrix)
        col=len(matrix[0])
        if row==0 or col==0:
            return 0
        #建立dp
        dp=[[0]*col for _ in range(row)]
        max_side=0
        #开始遍历
        for i in range(row):
            for j in range(col):
                #如果当前为1
                if matrix[i][j]=="1":
                    if i==0 or j==0:
                        dp[i][j]=1
                    else:
                        dp[i][j]=min(dp[i-1][j-1],dp[i-1][j],dp[i][j-1])+1
                    #更新max_side
                    max_side=max(max_side,dp[i][j])
                
        return max_side*max_side

21、Leetcode 403:青蛙过河

题目描述
在这里插入图片描述

解题思路
定义 d p [ i ] [ j ] dp[i][j] dp[i][j]为跳跃至i位置,所需要的跳跃数量,j表示上一个位置,True or False
代码中的diff代表上一次跳跃的的单元格的数量,那么当前只能跳跃diff+1,diff或者diff-1的单元格的数量。

代码实现

class Solution:
    def canCross(self, stones: List[int]) -> bool:
        n = len(stones)
        dp = [[False] * n for _ in range(n)]
        dp[0][1] = True
        for i in range(1, n):
            for j in range(i):
                diff = stones[i] - stones[j]
                # print(i, diff)
                if diff < 0 or diff >= n or not dp[j][diff]: continue
                dp[i][diff] = True
                if diff - 1 >= 0: dp[i][diff - 1] = True
                if diff + 1 < n: dp[i][diff + 1] = True
                
        return any(dp[-1])

22、Leetcode 410:分割数组的最大值

题目描述
在这里插入图片描述

解题思路
d p [ i ] [ j ] dp[i][j] dp[i][j]代表对于第i个位置分成j段后每种情况下每段的最大值的最小值。
那么状态的转移可以定义为
假设在0-i区间第j段分为k:i,那么转移的就是 d p [ k ] [ j − 1 ] dp[k][j-1] dp[k][j1]和i-k区间所有元素加和的最大值,对于每种k 注意这里j即分割的段数不能超过m也不能超过i。
这里的代码应该是没错的,但超时了好烦,,,,

代码实现

class Solution:
    def splitArray(self, nums: List[int], m: int) -> int:
        #nums的长度
        n=len(nums)
        #建立dp,表示前i个数中可分为j段
        dp=[[10**18]*(m+1) for _ in range(n+1)]
        #初始化
        dp[0][0]=0
        #定义一个列表,每个位置是之前的nums的累加
        sub=[0]
        for num in nums:
            sub.append(sub[-1]+num)
        #开始循环
        for i in range(1,n+1):
            #j的范围不能超过m也不能超过i
            for j in range(1,min(i,m)+1):
                #所有可能的分割情况:0-k,k-i
                for k in range(i):
                    #当前分段后的最大值应该是前k个数分为j-1段的和或者第j段的和中的最大值中的最小值
                    dp[i][j]=min(dp[i][j],max(dp[k][j-1],sub[i]-sub[k]))
        return dp[n][m]

23、Leetcode 552:学生出勤记录II

题目描述

在这里插入图片描述

解题思路
d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]中i表示出勤的长度,j表示A的个数,K表示末尾连续的L的个数。
根据题目要求,A不得超过1,K不得超过2
1、初始化状态的分析,如果长度为1,那么只能是A,P,L的其中一个。

  • 出勤为p: d p [ 1 ] [ 0 ] [ 0 ] = 1 dp[1][0][0]=1 dp[1][0][0]=1
  • 出勤为A d p [ 1 ] [ 1 ] [ 0 ] = 1 dp[1][1][0]=1 dp[1][1][0]=1
  • 出勤为L: d p [ 1 ] [ 0 ] [ 1 ] = 1 dp[1][0][1]=1 dp[1][0][1]=1

2、当出现连续的k时,且k不超过2,那么把这些k删掉的状态转移是相同的
d p [ i ] [ j ] [ k ] = d p [ i − 1 ] [ j ] [ k − 1 ] dp[i][j][k]=dp[i-1][j][k-1] dp[i][j][k]=dp[i1][j][k1]
3、如果末尾没有L,当没有A时,末尾的为P,那么可以删掉也不影响,删掉p,可能末尾是一个L,两个L或者0个L
d p [ i ] [ 0 ] [ 0 ] = d p [ i − 1 ] [ 0 ] dp[i][0][0]=dp[i-1][0] dp[i][0][0]=dp[i1][0]
4、当末尾没有L,但却是A时,那么吧i位置的A删掉与情况3相同。
代码实现

class Solution:
	def checkRecord(self, n: int) -> int:
		mod=pow(10,9)+7
		# 长度、'A'出现的次数、末尾连续'L'的次数
		dp=[[[0]*3 for _ in range(2)]for _ in range(n+1)]
		# base case:长度为1,那么是'A'、'L'、'P'中的一种
		dp[1][1][0]=1 #A
		dp[1][0][1]=1 #L
		dp[1][0][0]=1 #P

		for i in range(2,n+1):
			# 若末尾是k个连续的L(即k>0)
			for j in range(2):
				for k in range(1,3):
					dp[i][j][k]=dp[i-1][j][k-1]%mod
			# 若末尾没有L(即k=0)
			dp[i][0][0]=noA=sum(dp[i-1][0])%mod
			dp[i][1][0]=noA+sum(dp[i-1][1])%mod
		# 结果等于dp[n]的每种可能
		return sum(sum(col) for col in dp[-1])%mod

24、Leetcode 647:回文子串

题目描述

在这里插入图片描述

解题思路
使用 d p [ i ] [ j ] dp[i][j] dp[i][j]表示对于字符串s,从i到j位置是否是回文串,由于每个子串都要计数,那么只要 d p [ i ] [ j ] = 1 dp[i][j]=1 dp[i][j]=1时就count+1,对于每个字符本身,肯定是回文串,所以count+1,对于不相连得位置,如果它们对应元素相等,且它们距离小于2说明它们是连续的,那么也为1,如果距离大于2,就要看i和j中间的字符是不是回文串了。对于每个为1得dp位置,count+1。最后返回count的值。

代码实现

class Solution:
    def countSubstrings(self, s: str) -> int:
        n=len(s)
        #建立dp,表示i到j位置有多少个回文数
        dp=[[0]*n for _ in range(n)]
        count=0
        #开始循环
        for i in range(n-1,-1,-1):
            #自身肯定是回文数
            dp[i][i]=1
            count+=1
            for j in range(i+1,n):
                #如果i和j对应的相同
                if s[i]==s[j]:
                    #只有两个数的前提下
                    if j-i<=2:
                        dp[i][j]=1
                    else:
                        dp[i][j]=dp[i+1][j-1]
                if dp[i][j]:
                    count+=1
        return count

25、Leetcode 76:最小覆盖子串

题目描述

在这里插入图片描述

解题思路
这题做了挺久的,第一次做滑动窗口的题,直接上解题思路吧,复习的时候再回来完善自己的思路。
在这里插入图片描述

代码实现

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        #建立一个空字典储存t中的元素和需要的个数
        need=collections.defaultdict(int)
        #遍历t,将t存储进去
        for char in t:
            need[char]+=1
        #统计滑动窗口需要找到的数量
        count=len(t)
        res=(0,float("inf"))
        #滑动窗口的起始点
        i=0
        #开始遍历s
        for j,c in enumerate(s):
            #如果当前c出现在t中,将count-1
            if need[c]>0:
                count-=1
            #如果没出现,把c添加到need,并-1,为了i增加时可以把包括的在抛出出去
            need[c]-=1
            #当count已经为0时,代表当前窗口已经包含了所有的t中元素,那么要将i扩大
            if count==0:
                #让i扩大,直到s[i]找到t中的元素
                while True:
                    c=s[i]
                    #找到了
                    if need[c]==0:
                        break
                    #没找到的话,把原先那些-1的值加回来
                    need[c]+=1
                    #移动i
                    i+=1
                #当前i,j为第一次找到的最小字串区间,对比更新
                if j-i<res[1]-res[0]:
                    #更新
                    res=(i,j)
                #将i移动一个,重新寻找一个滑动窗口
                #先要把need中的属于t的元素加一个1
                need[s[i]]+=1
                #将count+1
                count+=1
                #移动i
                i+=1
        #返回时如果j大于了长度,那么说明没找到合适的窗口
        return ""if res[1]>len(s) else s[res[0]:res[1]+1]

26、Leetcode 312:戳气球

题目描述
在这里插入图片描述

解题思路
在这里插入图片描述
对于i,j区间内的所有可能的数字k,它可以和i,j组成一个戳爆气球后的硬币组合,对于每个位置k,它的硬币总数为val[i]*val[k]*val[j]再加上 d p [ i ] [ k ] + d p [ k ] [ j ] dp[i][k]+dp[k][j] dp[i][k]+dp[k][j]的值,找到最大的位置k,并存储在 d p [ i ] [ j ] dp[i][j] dp[i][j]中。

代码实现

class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        #长度
        n=len(nums)
        #建立dp
        dp=[[0]*(n+2) for _ in range(n+2)]
        #nums的扩充
        val=[1]+nums+[1]
        #开始遍历
        for i in range(n-1,-1,-1):
            for j in range(i+2,n+2):
                for k in range(i+1,j):
                    total=val[i]*val[k]*val[j]
                    total+=dp[i][k]+dp[k][j]
                    dp[i][j]=max(total,dp[i][j])
        return dp[0][n+1]

总结

做完了课程中所有的dp题,对于做出dp题最重要的三点:

  • 1、找到转移方程即最优子问题
  • 2、确定边际条件
  • 3、维护的dp的长度

你可能感兴趣的:(动态规划,leetcode)