代码随想录算法训练营第三十三天 | Leetcode随机抽题检测

Leetcode随机抽题检测

70 爬楼梯

未看解答自己编写的青春版

动态规划写多了,果然就会不太注重初始值的设置,会有些想当然,比如本题我一开始想的是,设定 dp[0] = 1 , dp[1] = 1 , 这样的结果也是正确的,但是就是可能初始化的解释性上不太好,还是设定 dp[1] dp[2] 更好一些。

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

重点

题解的代码

日后复习重新编写

118 杨辉三角

未看解答自己编写的青春版

感觉这道题没什么DP的味道。

class Solution:
    def generate(self, numRows: int) -> List[List[int]]:
        res = [[1]]
        for i in range(1,numRows):
            temp = [1]*(i+1)
            flag = res[-1]
            for j in range(1,i):
                temp[j] = flag[j-1]+flag[j]
            res.append(temp)
        return res

重点

题解的代码

日后复习重新编写

198 打家劫舍

未看解答自己编写的青春版

其实不需要设置两个初始状态, dp[1] dp[2] ,一个 dp[1] = nums[0] 就够了,这样也不用单独处理:只有一个房屋的情况了。

class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 0 :
            return 0
        if n == 1 :
            return nums[0]
        dp = [0]*(n+1)
        dp[1] = nums[0]
        dp[2] = max(nums[0],nums[1])
        for i in range(3,n+1):
            dp[i] = max(dp[i-1],dp[i-2]+nums[i-1])
        return dp[n]

重点

动态规划,在人为设置初始状态时,要考虑输入直接被设置好的情况,及时判断并return , 不然会导致 index 错误。

题解的代码

日后复习重新编写

279 完全平方数

未看解答自己编写的青春版

这次写了个没有提前算好的版本,比提前算法的版本要耗时啊。

class Solution:
    def numSquares(self, n: int) -> int:
        dp = [inf]*(n+1)
        dp[0] = 0
        dp[1] = 1
        if n == 1 :
            return 1
        for i in range(2,n+1):
            mini = inf
            temp = int(sqrt(i)+1)
            for j in range(1,temp):
                if i-j*j >= 0 :
                    mini = min(mini,dp[i-j*j])
            dp[i] = 1 + mini
        print(dp)
        return dp[n]

提前算好每个平方数:(注意,双重循环的循序,先遍历 item ,再遍历 n )

class Solution:
    def numSquares(self, n: int) -> int:
        items = []
        for i in range(1,int(sqrt(n))+2):
            if i*i <= n :
                items.append(i*i)

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

提前算好每个平方数:(颠倒遍历顺序,耗时和第一版,不提前算好的方法,差不多了)

class Solution:
    def numSquares(self, n: int) -> int:
        items = []
        for i in range(1,int(sqrt(n))+2):
            if i*i <= n :
                items.append(i*i)

        m = len(items)
        dp = [inf]*(n+1)
        dp[0] = 0
        
        for i in range(1,n+1):
            for j in range(m):
                if i >= items[j] :
                    dp[i] = min(dp[i],dp[i-items[j]]+1)
        
        return dp[n]

重点

综上,这道题还是选用方法二,因为迭代的次数较少,可以发现,每一个数,至少都可以写为 n 个 1 相加的情况,所以我们应该先遍历 item ,去判断取不取当前 item 就好了。

统一写法:

class Solution:
    def numSquares(self, n: int) -> int:
        items = []
        for i in range(1,int(sqrt(n))+2):
            if i*i <= n :
                items.append(i*i)

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

怎么没看出来,这道题是完全背包啊!因为是求最少的数目,所以不管是组合数求法还是排列数求法都可以,这意味着,先遍历物品还是先遍历背包都可以!

01背包中,二维DP数组的两个for遍历,先后顺序可以颠倒。一维DP数组的两个for循环的顺序,一定是先遍历物品,再遍历背包,另外背包要倒序遍历,因为要保证每个物品只拿一个。而如果,01背包的一维DP数组,倒序遍历背包,但是先遍历背包,再遍历物品,得到的结果是,最后的背包中只有一个物品。(这里因为背包是倒序遍历,上来就把结果位置的输出遍历掉了,此时其他位置的值还都是初始值)

完全背包,不管是一维DP数组还是二维DP数组,遍历顺序都可以颠倒,一维DP数组时,背包的遍历顺序为正序遍历,且必须是正序遍历。

但是完全背包有一个要注意的点,就是完全背包问题的拓展应用题中,会涉及组合数和排列数的问题,而01背包没有类似的问题。求组合数,必须是先遍历物品,再遍历背包;求排列数,是先遍历背包,再遍历物品、

完全背包:一维DP时,背包必须正序遍历,先物品后背包:求组合数;先背包后物品:求排列数。

题解的代码

日后复习重新编写

322 零钱兑换

未看解答自己编写的青春版

也是完全背包!也是求可能结果中的最少元素个数,所以先遍历物品或背包,都无所谓,一维DP,正序遍历。

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

重点

题解的代码

日后复习重新编写

139 单词拆分

未看解答自己编写的青春版

典型完全背包,求排列数的题目,因为字符的排列是要求顺序的.

用一维背包去写,全部顺序遍历,先遍历背包,再遍历物品.

注意这里的递推关系,要有 dp[i],并且逻辑关系是 or 。

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        n = len(wordDict)
        m = len(s)
        dp = [False]*(m+1)
        dp[0] = True
        # 典型完全背包,求排列数的题目,因为字符的排列是要求顺序的
        # 用一维背包去写,全部顺序遍历,先遍历背包,再遍历物品
        for i in range(1,m+1):
            for j in range(n):
                if i >= len(wordDict[j]):
                    if s[i-len(wordDict[j]):i]==wordDict[j] :
                        # 注意这里的递推关系,要有 dp[i],并且逻辑关系是 or
                        dp[i] = dp[i] or dp[i-len(wordDict[j])]
        return dp[m]
        

重点

题解的代码

日后复习重新编写

这几道题做下来,我觉得动态规划我又需要复习了,果然是做一道忘一道,怎么都记不住啊。

300 最长递增子序列

未看解答自己编写的青春版

DP 方法容易想到,很简单,DP数组的含义就是 dp[i] : 以 i 为结尾的最长子序列的长度。

DP方法的时间复杂度为 O(n^2) 。

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [1]*n
        maxi = 0

        for i in range(n):
            # 判断 i>0 可以省略,因为不涉及index=i-1的操作            
            for j in range(i):
                if nums[i] > nums[j] :
                    dp[i] = max(dp[i],dp[j]+1)
            maxi = max(maxi,dp[i])
        #print(dp)
        return maxi

重点

题目进阶:时间复杂度为 O( nlogn ) 的方法是什么?

想不到。看题解。

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

这个贪心+二分的思想,真的太奈斯了!之前从来没接触过。

题解的代码

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
                d[loc] = n
        return len(d)

日后复习重新编写

152 乘积最大子数组

未看解答自己编写的青春版

用2个指标,去记录以 i 结尾的子串,最大正值和最小负值。

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        n = len(nums)
        # dp[i][0] : 以i为结尾的最大正值
        # dp[i][1] : 以i为结尾的最小负值
        dp = [[0]*2 for _ in range(n)]
        if nums[0] > 0 :
            dp[0][0] = nums[0]
        else :
            dp[0][1] = nums[0]
        # 这里初始化,如果给 maxi 为 -inf,那么就要单独考虑数组中只有一个元素的情况
        # 如果初始化为,第一个元素,就不需要特殊考虑了。
        maxi = nums[0]
        for i in range(1,n):
            if nums[i] > 0 :
                dp[i][0] = max(nums[i],dp[i-1][0]*nums[i])
                dp[i][1] = dp[i-1][1]*nums[i]
            else :
                dp[i][0] = dp[i-1][1]*nums[i]
                dp[i][1] = min(nums[i],dp[i-1][0]*nums[i])
            maxi = max(maxi,dp[i][0])
        #print(dp)
        return maxi

我的方法速度倒是挺快,但是在内存占用上只打败了5%。

重点

评论中的一种贪心的思想:很有道理
在这里插入图片描述

class Solution {
    public int maxProduct(int[] nums) {
        int a=1;  
        int max=nums[0];
        
        for(int num:nums){
            a=a*num;
            if(max<a)max=a;
            if(num==0)a=1;

        }
        a=1;
        for(int i=nums.length-1;i>=0;i--){
            a=a*nums[i];
            if(max<a)max=a;
            if(nums[i]==0)a=1;
        }  
        return max;
    }
}

嫌空间占用大?在最原始的DP代码编写中,发现当前状态只和前一个状态有关,那就进行状态压缩!压缩后占用空间打败60%。

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        n = len(nums)
        # dp[i][0] : 以i为结尾的最大正值
        # dp[i][1] : 以i为结尾的最小负值
        dp = [0,0]
        if nums[0] > 0 :
            dp[0] = nums[0]
        else :
            dp[1] = nums[0]
        # 这里初始化,如果给 maxi 为 -inf,那么就要单独考虑数组中只有一个元素的情况
        # 如果初始化为,第一个元素,就不需要特殊考虑了。
        maxi = nums[0]
        for i in range(1,n):
            if nums[i] > 0 :
                dp[0] = max(nums[i],dp[0]*nums[i])
                dp[1] = dp[1]*nums[i]
            else :
                # 在当前nums[i]是负数时,需要先保存一下dp[0],因为在dp[1]更新要用到dp[0]
                # 但是这个值已经被改变了!其他情况,不存在这种被改变的case
                temp = dp[0]
                dp[0] = dp[1]*nums[i]
                dp[1] = min(nums[i],temp*nums[i])
            maxi = max(maxi,dp[0])
        #print(dp)
        return maxi

题解的代码

日后复习重新编写

416 分割等和子集

未看解答自己编写的青春版

01背包应用的典型题,一维DP,倒序遍历背包。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        # 这不就是个01背包嘛
        n = len(nums)
        sumsum = sum(nums)
        if sumsum % 2 == 1 :
            return False
        target = sumsum // 2
        dp = [0]*(target+1)

        for i in range(n):
            for j in range(target,nums[i]-1,-1):
                dp[j] = max(dp[j],dp[j-nums[i]]+nums[i])
        if dp[-1]==target :
            return True
        else :
            return False

重点

题解的代码

日后复习重新编写

62 不同路径

未看解答自己编写的青春版

这道题注意初始化就可以了,

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[0] * n for _ in range(m)]
        dp[0][0]=1
        for i in range(1,m):
            dp[i][0] = 1
        for i in range(1,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[m-1][n-1]

重点

题解的代码

日后复习重新编写

64 最小路径和

未看解答自己编写的青春版

和上一题解法基本一致,但是时间和空间指标较差。

class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        m = len(grid)
        n = len(grid[0])
        dp = [[0]*n for _ in range(m)]
        dp[0][0] = grid[0][0]
        for i in range(1,n):
            dp[0][i] = dp[0][i-1]+grid[0][i]
        for i in range(1,m):
            dp[i][0] = dp[i-1][0]+grid[i][0]
        for i in range(1,m):
            for j in range(1,n):
                dp[i][j] = min(dp[i-1][j],dp[i][j-1])+grid[i][j]
        return dp[-1][-1]

重点

看了下评论,也没有什么更好的思路,就是最基础的动态规划题目。

题解的代码

日后复习重新编写

5 最长回文子串

未看解答自己编写的青春版

回文串,回文串!遇到和动态规划相关的回文串题目,就用这种思路!

class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        dp = [[False]*n for _ in range(n)]
        left = 0
        right = 0
        # dp[i][j]代表[i,j]是否是回文串,可以由dp[i+1][j-1]推出
        for i in range(n-1,-1,-1):
            for j in range(i,n):
                if i==j :
                    dp[i][j]=True
                else :
                    if s[i]==s[j]:
                        # 这里进过思考,不需要加一个条件,防止i+1溢出
                        # 因为如果溢出,则i=n-1,那么j大于等于i,且要小于n
                        # 那么j只能是n-1,则会执行上面的判断,不会进入到下面
                        if j-i == 1 or dp[i+1][j-1] :
                            dp[i][j]=True
                            if j-i > right-left :
                                left,right = i,j
        return s[left:right+1]

重点

本题的双指针也值得学习!提供了一种另外的思路。

本质思路是:遍历每一个位置 i ,考虑两种情况,以当前位置 i 向两边进行扩散,以当前位置 i 和下一个位置 i+1 ,

class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        if n <= 1 :
            return s

        start = 0
        end = 0
        for i in range(n):
            left,right = self.find_point(i,i,s)
            start,end = self.compare(end,start,right,left)

            left,right = self.find_point(i,i+1,s)
            # 注意这里,第二次比较,其实这次比较的,已经是前面,以单字符为中心的最大结果了
            # 即是上一次的 left 和 right 结果,和这次的 left right 进行比较
            start,end = self.compare(end,start,right,left)

        return s[start:end]

        

    def find_point(self,i,j,s):
        while i >= 0 and j < len(s) and s[i]==s[j] :
            i -= 1
            j += 1
        return i+1,j


    def compare(self,a,b,c,d):
        if a-b > c-d :
            return b,a
        else :
            return d,c


题解的代码

日后复习重新编写

自己复写了二分法的方法:难点在于 find 函数的理解。以及时刻抓住循环不变量:左闭右闭区间。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        start = 0
        end = 0
        # 注意区间,这里我定义为:左闭右闭
        for i in range(n):
            left,right = self.find(s,i,i)
            if right-left > end-start :
                start,end = left,right
            # 想不明白,find函数到底能不能正确处理i+1=n(即超出index)的情况
            # 那就用if逻辑把它剔除嘛,本来这种逻辑也是非法的。
            if i < n-1 :
                left,right = self.find(s,i,i+1)
                if right-left > end-start :
                    start,end = left,right
        return s[start:end+1]

    def find(self,s,i,j):
        n = len(s)
        while i >= 0 and j < n :
            # 这里注意二分法的定义,是以i,j这两个位置的元素为中心
            # abba叫以'bb'为中心,而'aba'不叫以'ab'为中心!
            # 'aba'的情况,被i=j='b'的情况所包含了
            if s[i]!=s[j]:
                break
            else :
                i-=1
                j+=1
        # 始终牢记左闭右闭规则
        return [i+1,j-1]

1143 最长公共子序列

未看解答自己编写的青春版

一开始题意理解错了,以为 text1 是母串, text2 是子串,但是重读题后,发现两个串是互相独立的,这也就意味着:dp[i][j] 更新要同时考虑 max(dp[i-1][j],dp[i][j-1]) , 所以本题也无法做状态压缩,必须是二维DP数组。

这是编辑距离的经典类型题了,dp[i][j]的意义是:text1[0:i] 和 text2[0:j] 的最长公共子序列的长度。

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        n = len(text1)
        m = len(text2)
        dp = [[0]*(m+1) for _ in range(n+1)]
        for i in range(1,n+1):
            for j in range(1,m+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]

重点

题解的代码

日后复习重新编写

72 编辑距离

未看解答自己编写的青春版

编辑距离,主要理解两点:如何初始化;如何表示替换操作。

首先明确:插入和删除操作是互逆的,所以我们只需要考虑删除操作就可以了,其中,dp[i-1][j] , dp[i][j-1] ,都代表着删除操作,即:删除word1[i] , 用 word1[0:i-1] 和 word2[0:j] 匹配。删除word2[j] , 用 word1[0:i] 和 word2[0:j-1] 匹配。

而替换操作:dp[i-1][j-1] 将word1[i] or word2[j] 替换为相对应的那个值,这时候,word1[i] 和 word2[j] 是匹配上的,且不能用于前面子串的匹配,所以前面子串的操作个数为 : dp[i-1][j-1] 。

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m = len(word1)
        n = len(word2)
        if n == 0:
            return m
        if m==0 :
            return n

        dp = [[0] * (n+1) for _ in range(m+1)]
        # 初始化很重要
        for i in range(1,n+1):
            dp[0][i] = i
        for i in range(1,m+1):
            dp[i][0] = i

        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][j-1],dp[i-1][j-1])+1
        return dp[-1][-1]

重点

如果想复习编辑距离的原理,可以复习文章。

编辑距离

题解的代码

日后复习重新编写

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第二十二算法训练营主要涵盖了Leetcode题目中的三道题目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序数组的平方",和Leetcode 209 "长度最小的子数组"。 首先是Leetcode 28题,题目要求在给定的字符串中找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977题,题目要求对给定的有序数组中的元素进行平方,并按照非递减的顺序返回结果。这里由于数组已经是有序的,所以可以使用双指针的方法来解决问题。一个指针指向数组的开头,另一个指针指向数组的末尾。通过比较两个指针所指向的元素的绝对值的大小来确定哪个元素的平方应该放在结果数组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209题,题目要求在给定的数组中找到长度最小的子数组,使得子数组的和大于等于给定的目标值。这里可以使用滑动窗口的方法来解决问题。使用两个指针来表示滑动窗口的左边界和右边界,通过移动指针来调整滑动窗口的大小,使得滑动窗口中的元素的和满足题目要求。具体实现的代码如下: ```python def minSubArrayLen(self, target: int, nums: List[int]) -> int: left = 0 right = 0 ans = float('inf') total = 0 while right < len(nums): total += nums[right] while total >= target: ans = min(ans, right - left + 1) total -= nums[left] left += 1 right += 1 return ans if ans != float('inf') else 0 ``` 以上就是第二十二算法训练营的内容。通过这些题目的练习,可以提升对双指针和滑动窗口等算法的理解和应用能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值