【202309】算法基础-L5-动态规划(2)

上一篇:动态规划(1)

动态规划讲解例题:

补充练习题:力扣上经典的四个动态规划系列题

(以下正片开始)

第3章 动态规划(续)

回顾总结:动态规划三大特性

  • 最优子结构
    • 当一个最优解包含子结构的解时,子结构的解也必定是最优解
    • 一个问题可以使用动态规划的前提条件
    • 证明该问题可用动态规划求解
  • 重叠子问题
    • 使用动态规划的必要性
    • 解释了动态规划为什么能优化算法
  • 备忘录方法
    • 求解重叠子问题的工具
    • 使用动态规划解题的特征

例题4 LC300 最长递增子序列

本题为补充自学内容,要点提示如下:

  • 注意 d p [ i ] dp[i] dp[i] 的定义:在 n u m s [ 0.. i ] nums[0..i] nums[0..i] 中,并且以 n u m s [ i ] nums[i] nums[i] 为结尾的最长递增子序列的长度
  • 在求解 d p [ i ] dp[i] dp[i] 的过程中必须枚举 前面[0…i-1]的子问题结果,首先必须 n u m s [ j ] < n u m s [ i ] nums[j] < nums[i] nums[j]<nums[i],然后计算得到的 d p [ i ] = d p [ j ] + 1 dp[i]=dp[j] + 1 dp[i]=dp[j]+1是否更长
  • 时间复杂度为 O ( n O(n O(n2 ) ) )

Java 代码实现

提交记录原链接 (登录你自己的力扣账号之后可见)

class Solution {
    public int lengthOfLIS(int[] a) {
        int n = a.length, max_len = 1; // max_len: 最终结果
        int[] dp = new int[n];
        for (int i = 0; i < n; i++) {
            dp[i] = 1; // 至少为1
            for (int j = 0; j < i; j++) // 枚举前面的结果
                if (a[j] < a[i]) { // 前提:前一个数和当前为递增
                    dp[i] = Math.max(dp[i], dp[j] + 1); // 递推
                    max_len = Math.max(max_len, dp[i]);
                }
        }    
        return max_len;
    }
}

Python代码实现

提交记录原链接 (登录你自己的力扣账号之后可见)

强烈建议阅读Python代码,Python简洁的语法能够更直观地体现算法思想

class Solution:
    def lengthOfLIS(self, a: List[int]) -> int:
        n = len(a)
        dp = [0] * n
        for i in range(n):
        	# 注意这里必须加上default参数,因为当i前面没有更小的元素时,列表生成式为空,此时无法求得max
            dp[i] = 1 + max((dp[j] for j in range(i) if a[j] < a[i]), default = 0)
        return max(dp)

例题5 LC1143 最长公共子序列

问题分析

1. 最优子结构

我们假设问题的最优解 c c c,由两个子串 a a a b b b组成(即 c = a + b c=a+b c=a+b),又知 a a a s 1 ′ s1' s1 s 2 ′ s2' s2的公共子序列,那我们必定有 a a a s 1 ′ s1' s1 s 2 ′ s2' s2的最长公共子序列

2. 重叠子问题

例如我们求解 ( s 1 [ 4 ] , s 2 [ 6 ] ) (s1[4], s2[6]) (s1[4],s2[6]),和 ( s 1 [ 5 ] , s 2 [ 4 ] ) (s1[5], s2[4]) (s1[5],s2[4])这两个问题时,都会遇到 ( s 1 [ 3 ] , s 2 [ 3 ] ) (s1[3], s2[3]) (s1[3],s2[3])这个子问题,如果每次都计算一遍就会造成大量重复计算

3. 备忘录方法

我们用二维数组(备忘录)dp[a][b] 表示 s 1 s1 s1的前 a a a个字母与 s 2 s2 s2的前 b b b个字母组成的最长公共子序列的长度,避免重叠子问题的重复计算

求解过程

dp[a][b] 表示 s 1 s1 s1的前 a a a个字母与 s 2 s2 s2的前 b b b个字母组成的最长公共子序列的长度

dp[a][b]分为以下三种状态:

  • s1[a-1]为多出来的字母,所以答案和dp[a-1][b]一样
  • s2[b-1]为多出来的字母,所以答案为dp[a][b-1]
  • s1[a-1]==s2[b-1]时,两个字母构成长度为 1 1 1的子序列,再加上前面子问题的最优解,即1+dp[a-1][b-1]
  • dp[a][b]取以上三种情况的最大值

S1="abcd" S2="acedb" 时,填表情况如下图:

最长公共子序列

Java代码实现

class Solution {
    int longestCommonSubsequence(String s1, String s2) {
        int[][] dp = new int[s1.length()][s2.length()];
        for (int i = 0; i < s1.length(); i++)
            for (int j = 0; j < s2.length(); j++) {
                // 求dp[i][j]: 必定为以下三个分支的最大值

                // 分支1: 由dp[i - 1][j - 1] + 1得到当前值
                if (s1.charAt(i) == s2.charAt(j)) {
                    dp[i][j] = 1;
                    if (i > 0 && j > 0) dp[i][j] += dp[i - 1][j - 1];
                }

                // 分支2: s1[i]是多出来的字符,当前值跟dp[i - 1][j]相等
                if (i > 0 && dp[i - 1][j] > dp[i][j]) dp[i][j] = dp[i - 1][j];

                // 分支3: s2[j]是多出来的字符,当前值跟dp[i][j - 1]相等
                if (j > 0 && dp[i][j - 1] > dp[i][j]) dp[i][j] = dp[i][j - 1];
            }
        // 返回递推表的最后一个值为两个字串的最长公共子序列的长度
        return dp[s1.length() - 1][s2.length() - 1];    
    }
}

Python代码实现

class Solution:
    # 求最长公共子序列
    def longestCommonSubsequence(self, s1: str, s2: str) -> int:
        m, n = len(s1), len(s2)
        # 创建备忘录,求解重叠子问题
        # 令dp[i][j] 为 s1[0..i-1](也就是前i个字符) 
        #            与 s2[0..j-1](也就是前j个字符)的LCS长度
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                # 分支1:两个子串的末尾字符相等
                if s1[i - 1] == s2[j - 1]:
                    dp[i][j] = 1 + dp[i - 1][j - 1]
                # 分支2:末尾字符不相等
                else:
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
        # 返回结果
        return dp[-1][-1]

例题6 01背包问题

本题为自主学习内容,提示如下:
  • 01背包问题为NP难度问题,在背包容量可被枚举的情况下可用动态规划求解
  • 01背包问题的时间复杂度为O(nC) (考试必考点)
  • 定义dp[i][c]:把前 i i i个物品装进容量为 c c c的子背包可获得的最大价值
  • 选择状态:第 i i i件物品不装背包,价值和 i − 1 i-1 i1一样,或者装背包得到v[i],然后查找剩余容量剩余物品下的最大价值dp[i-1][c-w[i]]
  • 递推公式:dp[i][c] = max(dp[i-1][c], v[i]+dp[i-1][c-w[i]])
示例填表如下:

01背包问题填表

示例代码如下:

以下代码已优化,可作为代码模板保存,遇到0-1背包问题时可以直接套用

    /**
     * 01背包问题:计算可装入背包物品的最大价值总和
     * @param n 物品的数量
     * @param c 背包的最大承重量
     * @param w 物品重量 w[i]对应第i件物品的重量
     * @param v 物品价值 v[i]对应第i件物品的价值
     * @return 返回可装入背包物品的最大价值
     */
    public int knapsack(int n, int c, int[] w, int[] v) {
        int[] dp = new int[c + 1];
        for (int i = 0; i < n; i++) 
            for (int j = c; j >= w[i]; j--) 
                dp[j] = Math.max( dp[j], v[i] + dp[j - w[i]]);
        return dp[c];
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值