力扣刷题记录&整理——(十三)1-D Dynamic Programming


前言

整理力扣刷题思路。

  • 语言:python
  • 题库:来自neetcode: link

一、预备知识

1.动态规划(Dynamic Programming)

动态规划是一种有效的计算机算法设计技术,主要用于解决具有以下特点的问题:

  1. 重叠子问题:问题可以分解为若干个子问题,且子问题的解可以在后续的求解中被重复利用。
  2. 最优子结构:问题的最优解可以由其子问题的最优解得到。

动态规划的核心思想是将问题分解为子问题,通过逐个求解子问题,逐步构建出整体问题的最优解。它通常用于优化问题,如寻找最优路径、最大值、最小值等。

常见的动态规划问题类型和解题思路

  1. 最优化问题

    • 这类问题涉及在有限的资源或约束条件下寻找最优解。例如背包问题、旅行商问题等。
    • 解题思路:通过逐个求解子问题,逐步构建出最优解。
  2. 序列比对问题

    • 这类问题需要找到两个序列之间的相似或相同部分。例如DNA序列比对、字符串匹配等。
    • 解题思路:动态规划可以用于构建最长公共子序列或最长公共子串等问题的解。
  3. 树和图的问题

    • 例如二叉树的最小路径和、最小生成树等。
    • 解题思路:将问题分解为子问题,并存储子问题的解,避免重复计算。

示例:机器人的路径问题

让我们以一个具体的问题为例,来看看动态规划的解题思路。

问题描述

一个机器人位于一个 m x n 网格的左上角,每次只能向下或向右移动一步,试图达到网格的右下角。问总共有多少条不同的路径?

解题步骤
  1. 定义数组元素的含义

    • 定义 dp[i][j] 为机器人从左上角走到 (i, j) 这个位置时的路径数。
    • dp[m-1][n-1] 即为所求答案。
  2. 关系数组元素间的关系式

    • 机器人可以从 (i-1, j)(i, j-1) 这两个位置走一步到达 (i, j)
    • 因此,dp[i][j] = dp[i-1][j] + dp[i][j-1]
  3. 初始值

    • 初始化最上面一行和最左边一列的路径数:
      • dp[0][j] = 1(机器人只能一直往左走)
      • dp[i][0] = 1(机器人只能一直往下走)
  4. 代码实现

    def uniquePaths(m, n):
        if m <= 0 or n <= 0:
            return 0
        dp = [[0] * n for _ in range(m)]
        for i in range(m):
            dp[i][0] = 1
        for j in range(n):
            dp[0][j] = 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]
    

2.背包问题

问题解决思路:link
link


二、解题思路

1.动态规划

70.climbing-stairs

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

class Solution:
    def climbStairs(self, n: int) -> int:
        if n<=2:
            return n
        
        #答案是斐波那契数列,dp[n]=dp[n-1]+dp[n-2]
        a,b = 1,1
        for _ in range(1,n):
            a,b = b, a+b
        return b

746.min-cost-climbing-stairs

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。
link

class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        #dp[i]代表到达第i个台阶的花费,因为最终要到顶部,所以dp的长度比cost大1
        dp = [0]*(len(cost)+1)
        for i in range(2,len(cost)+1):
            dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])
        return dp[-1]

198.house-robber

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

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

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

213.house-robber-ii

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

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

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

参考:link
将环形变成两个单排

5.longest-palindromic-substring

给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
link

class Solution:
    def longestPalindrome(self, s: str) -> str:
        self.ans = ''

        def helper(i,j):
            while 0<=i and j<len(s) and s[i]==s[j]:
                if j-i+1>len(self.ans):
                    self.ans = s[i:j+1]
                i -= 1
                j += 1
        
        #以当前字符或加上后一个字符为中心,左右延展判断是否回文
        for i in range(len(s)):
            helper(i,i)
            helper(i,i+1)
        return self.ans

647.palindromic-substrings

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
link

class Solution:
    def countSubstrings(self, s: str) -> int:
        self.cnt = 0
        n = len(s)
        
        def helper(i,j):
            while i>=0 and j<n and s[i] == s[j]:
                self.cnt += 1
                i -= 1
                j += 1
        
        for i in range(n):
            helper(i,i)
            helper(i,i+1)
        
        return self.cnt

91.decode-ways

一条包含字母 A-Z 的消息通过以下映射进行了 编码 :

‘A’ -> “1”
‘B’ -> “2”

‘Z’ -> “26”
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:

“AAJF” ,将消息分组为 (1 1 10 6)
“KJF” ,将消息分组为 (11 10 6)
注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。

给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。

题目数据保证答案肯定是一个 32 位 的整数。
link

class Solution:
    def numDecodings(self, s: str) -> int:
        if s[0]=='0':
            return 0
        
        n = len(s)
        dp = [1]*(n+1)

        for i in range(2,n+1):
            if s[i-1] == '0' and s[i-2] not in '12':
                return 0
            if s[i-2:i] in ['10','20']:
                dp[i] = dp[i-2]
            elif '10' < s[i-2:i] <='26':
                dp[i] = dp[i-1] + dp[i-2]
            else:
                dp[i] = dp[i-1]
        return dp[n]

152.maximum-product-subarray

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

测试用例的答案是一个 32-位 整数。
link

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if not nums:
            return
        
        mx = mn = out = nums[0]
        for num in nums[1:]:
            mx,mn = max(num*mx, num*mn, num), min(num*mx, num*mn, num)
            out = max(out, mx)
        return out

300.longest-increasing-subsequence

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

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
link

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        dp = [1]*len(nums) #dp[i]代表以i结尾的最长严格递增子序列的长度
        for j in range(len(nums)):
            for i in range(j):
                if nums[i]<nums[j]:
                    dp[j] = max(dp[i]+1,dp[j])
        return max(dp)

改进

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        tails,cnt = [0]*len(nums), 0
        for num in nums:
            i,j = 0,cnt
            while i<j:
                m = (i+j) // 2
                if tails[m] < num:
                    i = m+1
                else:
                    j = m
            tails[i] = num
            if j == cnt:
                cnt += 1
        return cnt

参考:link

322.coin-change

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。
link

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        
        n = len(coins)
        dp = [[amount+1] * (amount+1) for _ in range(n+1)]    # 初始化为一个较大的值,如 +inf 或 amount+1
        # 合法的初始化
        dp[0][0] = 0    # 其他 dp[0][j]均不合法
        
        # 完全背包:优化后的状态转移
        for i in range(1, n+1):             # 第一层循环:遍历硬币
            for j in range(amount+1):       # 第二层循环:遍历背包
                if j < coins[i-1]:          # 容量有限,无法选择第i种硬币
                    dp[i][j] = dp[i-1][j]
                else:                       # 可选择第i种硬币
                    dp[i][j] = min( dp[i-1][j], dp[i][j-coins[i-1]] + 1 )

        ans = dp[n][amount] 
        return ans if ans != amount+1 else -1

改进

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

参考:link

416.partition-equal-subset-sum

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

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        Sum = sum(nums)
        nums = sorted(nums)
        if Sum%2 == 1 or nums[-1]>Sum//2:
            return False
        
        dp = [[False]*(Sum//2+1) for _ in range(len(nums)+1)]
        dp[0][0] = True
        for i in range(1,len(nums)+1):
            for j in range(Sum//2+1):
                if j<nums[i-1]:
                    dp[i][j] = dp[i-1][j]
                else:
                    dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i-1]]
        
        return dp[-1][-1]

简化

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        Sum = sum(nums)
        if Sum%2 == 1 or max(nums)>Sum//2:
            return False
        
        dp = [False]*(Sum//2+1)
        dp[0] = True
        for num in nums:
            for j in range(Sum//2,num-1,-1):
                dp[j] |= dp[j-num]
        
        return dp[-1]

这一题跟上一题一样,也是背包问题,参考:link

139.word-break

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

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
link

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
                    if j == n:
                        return True
        return dp[-1]

仍然是背包问题

  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,关于力扣刷题C++常用操作,我可以给你一些常见的操作和技巧: 1. 使用 STL 容器和算法库:STL(Standard Template Library)是 C++ 标准库中的一个重要组成部分,包含了许多常用的容器和算法。在力扣刷题中,使用 STL 可以大大提高代码的效率和可读性。例如,vector 可以用来存储动态数组,sort 可以用来排序等等。 2. 使用 auto 关键字:auto 关键字可以自动推导变量类型,可以减少代码量和提高可读性。例如,auto x = 1; 可以自动推导出 x 的类型为 int。 3. 使用 lambda 表达式:lambda 表达式是 C++11 中引入的一种匿名函数,可以方便地定义一些简单的函数对象。在力扣刷题中,使用 lambda 表达式可以简化代码,例如在 sort 函数中自定义比较函数。 4. 使用位运算:位运算是一种高效的运算方式,在力扣刷题中经常会用到。例如,左移运算符 << 可以用来计算 2 的幂次方,右移运算符 >> 可以用来除以 2 等等。 5. 使用递归:递归是一种常见的算法思想,在力扣刷题中也经常会用到。例如,二叉树的遍历、链表的反转等等。 6. 使用 STL 中的 priority_queue:priority_queue 是 STL 中的一个容器,可以用来实现堆。在力扣刷题中,使用 priority_queue 可以方便地实现一些需要维护最大值或最小值的算法。 7. 使用 STL 中的 unordered_map:unordered_map 是 STL 中的一个容器,可以用来实现哈希表。在力扣刷题中,使用 unordered_map 可以方便地实现一些需要快速查找和插入的算法。 8. 使用 STL 中的 string:string 是 STL 中的一个容器,可以用来存储字符串。在力扣刷题中,使用 string 可以方便地处理字符串相关的问题。 9. 注意边界条件:在力扣刷题中,边界条件往往是解决问题的关键。需要仔细分析题目,考虑各种边界情况,避免出现错误。 10. 注意时间复杂度:在力扣刷题中,时间复杂度往往是评判代码优劣的重要指标。需要仔细分析算法的时间复杂度,并尽可能优化代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值