跳转至

动态规划

本页统计信息
  • 本页约 2356 个字, 198 行代码, 预计阅读时间 10 分钟。

  • 本页总阅读量

0005. 最长回文子串

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

经典的动态规划问题,用一个数组dp[i][j]表示s从i到j这一段是不是回文字符串,dp的情况可以分成下面几种: - 如果i == j 那么很明显是的 - 如果i + 1 == j 那么就看s[i + 1] == s[j]是否成立 - 其他情况,要先判断s[i + 1] == s[j - 1]是否成立,然后再根据dp[i + 1][j - 1]是不是1来决定dp[i][j]的结果 同时,因为遍历过程中,需要知道i+1的情况,所以第一层关于i的循环要倒着进行,保证dp[i + 1][j - 1]是一个有效值。具体的代码如下:

class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        dp = [[0 for i in range(n)] for j in range(n)]
        maxl = 1
        res = s[0]
        # 经典dp,经典中的经典
        for i in range(n - 1, -1, -1):
            dp[i][i] = 1
            for j in range(i + 1, n):
                if j == i + 1:
                    dp[i][j] = 1 if (s[i] == s[j]) else 0
                else:
                    if s[i] == s[j]:
                        dp[i][j] = dp[i + 1][j - 1]
                    else:
                        dp[i][j] = 0
                if dp[i][j] and j - i + 1 > maxl:
                    maxl = j - i + 1
                    res = s[i: j + 1]
        return res

1105.填充书架

给定一个数组 books ,其中 books[i] = [thicknessi, heighti] 表示第 i 本书的厚度和高度。你也会得到一个整数 shelfWidth 。按顺序 将这些书摆放到总宽度为 shelfWidth 的书架上。先选几本书放在书架上(它们的厚度之和小于等于书架的宽度 shelfWidth ),然后再建一层书架。重复这个过程,直到把所有的书都放在书架上。 需要注意的是,在上述过程的每个步骤中,摆放书的顺序与你整理好的顺序相同。 以这种方式布置书架,返回书架整体可能的最小高度。

很经典的dp问题,首先books必须按顺序摆放,这才使得我们可以使用dp,然后dp的具体方式就是对于每一个下标i,我们找到一个小于i的下标j,使得前j-1本书摆好之后,第j到第i本书另外放置一层,然后更新dp数组,这样就可以完成dp,具体的代码如下:

class Solution:
    def minHeightShelves(self, books: List[List[int]], shelfWidth: int) -> int:
        # dp[i] 来表示放下前i本书所用的最小高度
        n = len(books)
        dp = [999999999] * (n + 1)
        dp[0] = 0
        for i in range(n):
            b = books[i]
            width = 0
            height = 0
            j = i
            # 向前进行调整
            while j >= 0:
                width += books[j][0]
                if width > shelfWidth:
                    break
                height = max(height, books[j][1])
                # 前j-1个的结果加上j-i个放成一排后的结果
                dp[i + 1] = min(dp[i + 1], dp[j] + height)
                j -= 1
        return dp[-1]

1335.工作计划的最低难度

你需要制定一份 d 天的工作计划表。工作之间存在依赖,要想执行第 i 项工作,你必须完成全部 j 项工作( 0 <= j < i)。你每天 至少 需要完成一项任务。工作计划的总难度是这 d 天每一天的难度之和,而一天的工作难度是当天应该完成工作的最大难度。 给你一个整数数组 jobDifficulty 和一个整数 d,分别代表工作难度和需要计划的天数。第 i 项工作的难度是 jobDifficulty[i]。 返回整个工作计划的 最小难度 。如果无法制定工作计划,则返回 -1 

首先每天至少要完成一项工作,所以如果jobDifficulty的长度小于d那就无法制定工作计划。然后我们仔细分析题意就会发现,实际上我们是需要将jobDifficulty分成d份,然后求出这d份的每一份中元素最大值的最小值(即难度),所以我们可以考虑用dp或者记忆化搜索,用dfs(i, day)表示从第i天开始,将剩余工作安排在day日完成的最小难度,实现记忆化搜索的代码如下:

class Solution:
    def minDifficulty(self, jobDifficulty: List[int], d: int) -> int:
        n = len(jobDifficulty)
        if n < d:
            return -1

        # dfs(i, day)表示从第i个工作开始做day天的最小工作量
        @cache
        def dfs(i, day):
            if day == 1:
                return max(jobDifficulty[i:])
            # 把i-j的固定为一天然后开始搜索
            return min(max(jobDifficulty[i: j]) + dfs(j, day - 1) for j in range(i + 1, n - day + 2))

        return dfs(0, d)

1653. 使字符串平衡的最少删除次数

给你一个字符串 s ,它仅包含字符 'a' 和 'b'​​​​ 。你可以删除 s 中任意数目的字符,使得 s 平衡 。当不存在下标对 (i,j) 满足 i < j ,且 s[i] = 'b' 的同时 s[j]= 'a' ,此时认为 s 是 平衡 的。 请你返回使 s 平衡 的 最少 删除次数。

这道题目的意思非常明确,就是要把字符串s处理成前面全是a后面全是b的形式(当然a和b可以没有) 我们可以将这个问题转化成一个DP问题,用dp表示字符串s的最少删除次数,如果我们在s尾部新增一个字符,那么dp的更新可以分成两种情况讨论: - 如果新增了一个a,我们可以把这个a删除,或者把前面的b全部删除,我们需要记录前面b的个数 - 如果新增了一个b,因为前面的字符串已经平衡了,所以可以先记录b的数量,不需要进行删除操作 最终的代码如下:

class Solution:
    def minimumDeletions(self, s: str) -> int:
        # 所有的a必须出现在所有的b前面,当然a也可以没有
        count = 0
        dp = 0
        for i in s:
            # 简单动态规划,如果最后是a,要不就删除,要不就把前面的b全部删除
            if i == 'a':
                dp = min(dp + 1, count)
            else:
                count += 1
        return dp

6310. 获得分数的方法数

考试中有 n 种类型的题目。给你一个整数 target 和一个下标从 0 开始的二维整数数组 types ,其中 types[i] = [counti, marksi] 表示第 i 种类型的题目有 counti 道,每道题目对应 marksi 分。 返回你在考试中恰好得到 target 分的方法数。由于答案可能很大,结果需要对 109 +7 取余。

这道题是一道很经典的dp计数的题目,我们可以开一个长度为target+1的数组dp,dp[i]代表了分数恰好为i的时候的方案总数,并遍历types数组,对可以获得的分数进行dp,具体的代码如下:

class Solution:
    def waysToReachTarget(self, target: int, types: List[List[int]]) -> int:
        dp = [0 for i in range(target + 1)]
        mod = 10 ** 9 + 7
        dp[0] = 1
        # 直接dp居然真的可以过,惊了
        for tp in types:
            c, m = tp
            for i in range(target, -1, -1):
                for j in range(1, min(c, i // m) + 1):
                    dp[i] = (dp[i] + dp[i - j * m]) % mod
        return dp[-1]

6433. 矩阵中移动的最大次数

给你一个下标从 0 开始、大小为 m x n 的矩阵 grid ,矩阵由若干 正 整数组成。你可以从矩阵第一列中的 任一 单元格出发,按以下方式遍历 grid : 从单元格 (row, col) 可以移动到 (row - 1, col + 1)、(row, col + 1) 和 (row + 1, col + 1) 三个单元格中任一满足值 严格 大于当前单元格的单元格。 返回你在矩阵中能够 移动 的 最大 次数。

很明显的dp,但是要注意必须要从第一列出发,所以后面的每一列不仅要看前一列是否严格限于,还要看前面一列dp过来的位置是否有效。

class Solution:
    def countCompleteComponents(self, n: int, edges: List[List[int]]) -> int:
        g = [[0 for i in range(n)] for j in range(n)]
        for edge in edges:
            a, b = edge
            g[a][b] = 1
            g[b][a] = 1
        visit = set()
        count = 0
        comps = []

        def dfs(i, comp):
            visit.add(i)
            comp.append(i)
            for j in range(len(g[i])):
                if j not in visit and g[i][j] == 1:
                    dfs(j, comp)


        for i in range(n):
            if i not in visit:
                comp = []
                dfs(i, comp)
                comps.append(comp)
        for comp in comps:
            l = len(comp)
            flag = 1
            for i in range(l):
                for j in range(i + 1, l):
                    if g[comp[i]][comp[j]] == 0:
                        flag = 0
            count += flag
        return count

6449. 收集巧克力

给你一个长度为 n 、下标从 0 开始的整数数组 nums ,表示收集不同巧克力的成本。每个巧克力都对应一个不同的类型,最初,位于下标 i 的巧克力就对应第 i 个类型。在一步操作中,你可以用成本 x 执行下述行为: 同时对于所有下标 0 <= i < n - 1 进行以下操作, 将下标 i 处的巧克力的类型更改为下标 (i + 1) 处的巧克力对应的类型。如果 i == n - 1 ,则该巧克力的类型将会变更为下标 0 处巧克力对应的类型。 假设你可以执行任意次操作,请返回收集所有类型巧克力所需的最小成本。

可以发现我们最多进行n次下标转移的操作,就一定能找到最合适的方法,假设进行的转移次数是i,那么我们转移需要的成本就是x乘以i,我们接下来只要对每个巧克力j,选出右边i步以内最小的成本,再求和,就是移动i次最少的成本和,为了减小时间复杂度,我们可以用dp[i][j]来记录巧克力i距离j以内的最小成本,最终的代码如下:

class Solution:
    def minCost(self, nums: List[int], x: int) -> int:
        n = len(nums)
        dp = [[0 for i in range(n)] for j in range(n)]
        for i in range(n):
            dp[i][0] = nums[i]
            for j in range(1, n):
                k = (i + j) % n
                dp[i][j] = min(dp[i][j - 1], nums[k])
        result = sum(nums)
        # 调整1-n-1次,找出最小的结果
        for i in range(1, n):
            temp = i * x
            for j in range(n):
                temp += dp[j][i]
            result = min(result, temp)
        return result

6898.字符串连接删减字母

给你一个下标从 0 开始的数组 words ,它包含 n 个字符串。 定义 连接 操作 join(x, y) 表示将字符串 x 和 y 连在一起,得到 xy 。如果 x 的最后一个字符与 y 的第一个字符相等,连接后两个字符中的一个会被 删除 。 比方说 join("ab", "ba") = "aba" , join("ab", "cde") = "abcde" 。 你需要执行 n - 1 次 连接 操作。令 str0 = words[0] ,从 i = 1 直到 i = n - 1 ,对于第 i 个操作,你可以执行以下操作之一: 令 stri = join(stri - 1, words[i]) 令 stri = join(words[i], stri - 1) 你的任务是使 strn - 1 的长度 最小 。 请你返回一个整数,表示 str n - 1 的最小长度。

这道题乍一看回溯就可以解决,但是由于搜索空间很大,所以会超时。我们可以发现,这个join操作,不管是正向还是反向,最终能否触发合并长度-1,都只和每个words[i]的第一个字母和最后一个字母有关,我们可以考虑对首尾字母进行dp,假设dp[i][j][k] 表示words前i个单词进行join之后,首位为j而且末尾为k的字符串最小长度,这样一来,对于下一个新的单词,我们考察它能否和dp[i][j][k] 合并并发生缩短操作就可以,具体的实现方式有下面两种,一种是递归DFS,一种是dp

class Solution:
    def minimizeConcatenatedLength(self, words: List[str]) -> int:
        @cache
        def dfs(i: int, s: str, e: str) -> int:
            if i == n:
                return 0
            t = words[i]
            # 两种情况,分别讨论
            choice1 = dfs(i + 1, s, t[-1]) + len(t) - int(t[0] == e)
            choice2 = dfs(i + 1, t[0], e) + len(t) - int(t[-1] == s)
            result = min(choice1, choice2)
            return result

        n = len(words)
        return dfs(1, words[0][0], words[0][-1]) + len(words[0])


# 另一个dp的做法
class Solution:
    def minimizeConcatenatedLength(self, words: List[str]) -> int:
        n = len(words)
        dp = [[[10 ** 9 for i in range(26)] for j in range(26)] for k in range(n)]
        x0, y0 = ord(words[0][0]) - ord('a'), ord(words[0][-1]) - ord('a')
        dp[0][x0][y0] = len(words[0])
        for i in range(1, n):
            l = len(words[i])
            xi, yi = ord(words[i][0]) - ord('a'), ord(words[i][-1]) - ord('a')
            for j in range(26):
                for k in range(26):
                    if yi == j:
                        dp[i][xi][k] = min(dp[i][xi][k], dp[i - 1][j][k] + l - 1)
                    else:
                        dp[i][xi][k] = min(dp[i][xi][k], dp[i - 1][j][k] + l)
                    if xi == k:
                        dp[i][j][yi] = min(dp[i][j][yi], dp[i - 1][j][k] + l - 1)
                    else:
                        dp[i][j][yi] = min(dp[i][j][yi], dp[i - 1][j][k] + l)
        result = 10 ** 9
        for i in range(26):
            for j in range(26):
                result = min(result, dp[n - 1][i][j])
        return result

6931.访问数组中的位置使分数最大

给你一个下标从 0 开始的整数数组 nums 和一个正整数 x 。你 一开始 在数组的位置 0 处,你可以按照下述规则访问数组中的其他位置: 如果你当前在位置 i ,那么你可以移动到满足 i < j 的 任意 位置 j 。 对于你访问的位置 i ,你可以获得分数 nums[i] 。 如果你从位置 i 移动到位置 j 且 nums[i] 和 nums[j] 的 奇偶性 不同,那么你将失去分数 x 。请你返回你能得到的 最大 得分之和。 注意 ,你一开始的分数为 nums[0] 。

乍一看这道题很简单,我们只要用dp+两层循环就能很轻松解决,但实际上这道题数据量比较大,所以有时间复杂度上的要求,我们可以想办法把时间复杂度降低成线性的。具体来说,我们用s[i]表示nums前i项以偶数结尾的最大得分和,用t[i]表示nums前i项以奇数结尾的最大得分和,然后遍历一次nums,根据nums[i]的奇偶性,分别来更新s[i]和t[i]

class Solution:
    def maxScore(self, nums: List[int], x: int) -> int:
        # 分奇偶的动态规划题
        n = len(nums)
        s = [0 for i in range(n)]
        t = [0 for i in range(n)]
        if nums[0] % 2 == 0:
            s[0] = nums[0]
            t[0] = -x
        else:
            s[0] = -x
            t[0] = nums[0]
        for i in range(1, n):
            if nums[i] % 2 == 0:
                s[i] = max(s[i - 1] + nums[i], t[i - 1] + nums[i] - x)
                t[i] = t[i - 1]
            else:
                s[i] = s[i - 1]
                t[i] = max(t[i - 1] + nums[i], s[i - 1] + nums[i] - x)
        return max(max(s), max(t))

颜色主题调整

评论区~

有用的话请给我个star或者follow我的账号!! 非常感谢!!
快来跟我聊天~