算法及数据结构系列 - 动态规划

系列文章目录
算法及数据结构系列 - 二分查找
算法及数据结构系列 - BFS算法

框架思路

动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说求最长递增子序列,最小编辑距离等等。

既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值呗。

首先,动态规划的穷举有点特别,因为这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。

而且,动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。

另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出正确的「状态转移方程」才能正确地穷举。

以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。我来提供我研究出来的一个思维框架,辅助你思考状态转移方程:

明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case

子序列问题解题模板

一维dp数组

时间复杂 O(n^2)

int n = array.length;
int[] dp = new int[n];

for (int i = 1; i < n; i++) {
    for (int j = 0; j < i; j++) {
        dp[i] = 最值(dp[i], dp[j] + ...)
    }
}

举例:
300.最长上升子序列

二维dp数组

int n = arr.length;
int[][] dp = new dp[n][n];

for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        if (arr[i] == arr[j]) 
            dp[i][j] = dp[i][j] + ...
        else
            dp[i][j] = 最值(...)
    }
}

2.1 涉及两个字符串/数组时(比如最长公共子序列),dp 数组的含义如下:

在子数组 arr1[0…i] 和子数组 arr2[0…j] 中,我们要求的子序列(最长公共子序列)长度为 dp[i][j]。

举例:
1143.最长公共子序列
72.编辑距离

2.2 只涉及一个字符串/数组时(比如最长回文子序列),dp 数组的含义如下:

在子数组 array[i…j] 中,我们要求的子序列(最长回文子序列)的长度为 dp[i][j]。

举例:

经典题型

322. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

暴力递归

class Solution {
    public int coinChange(int[] coins, int amount) {
        return dp(coins, amount);
    }

    public int dp(int[] coins, int amount){
        // Base Case
        if(amount == 0){
            return 0;
        }
        if(amount < 0){
            return -1;
        }
        int res = Integer.MAX_VALUE;
        for(int coin: coins){
            int sub = dp(coins, amount - coin);
            if(sub == -1){
                // 不符合要求
                continue;
            }
            res = Math.min(res, sub + 1);
        }
        res = (res == Integer.MAX_VALUE? -1:res);
        return res;
    }
}

带备忘录的暴力递归

class Solution {
    public int coinChange(int[] coins, int amount) {
        Map<Integer, Integer> memo = new HashMap<Integer, Integer>();
        return dp(coins, amount, memo);
    }

    public int dp(int[] coins, int amount, Map<Integer, Integer> memo){
        if(amount == 0){
            return 0;
        }
        if(amount < 0){
            return -1;
        }
        if(memo.containsKey(amount)){
            return memo.get(amount);
        }
        int res = Integer.MAX_VALUE;
        for(int coin: coins){
            int sub = dp(coins, amount - coin, memo);
            if(sub == -1){
                continue;
            }
            res = Math.min(res, sub + 1);
        }
        res = (res == Integer.MAX_VALUE? -1:res);
        memo.put(amount, res);
        return res;
    }
}

动态规划

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount+1];
        for(int i = 0; i < amount + 1; i++){
            dp[i] = amount + 1; // 初始化为 amount+1 相当于是正无穷
        }
        dp[0] = 0; // Base Case
        for(int i = 0; i < amount + 1; i++){
            for(int coin: coins){
                if(i - coin >= 0){
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);
                }
            }
        }
        return dp[amount] == amount + 1? -1:dp[amount];
    }
}

300. 最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

提示:dp[i] 表示以i结尾的最长子序列长度

class Solution {
    public int lengthOfLIS(int[] nums) {
        if(nums.length == 0){
            return 0;
        }
        int[] dp = new int[nums.length];
        for(int i = 0 ; i < nums.length; i++){
            dp[i] = 1;
        }
        dp[0] = 1;
        int maxLen = 1;
        for(int i = 1; i < nums.length; i ++){
            for(int j = 0; j <i; j++){
                if(nums[j] < nums[i]){
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            maxLen = Math.max(maxLen, dp[i]);
        }
        return maxLen;
    }
}

1143. 最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace",它的长度为 3

第一步,一定要明确 dp 数组的含义。对于两个字符串的动态规划问题,套路是通用的。

比如说对于字符串 s1 和 s2,一般来说都要构造一个这样的 DP table:

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int[][] dp = new int[text1.length() + 1][text2.length() + 1];
        for(int i = 0; i < text1.length() + 1; i ++){
            dp[i][0] = 0;
        }
        for(int i = 0; i < text2.length() + 1; i++){
            dp[0][i] = 0;
        }
        for(int i = 1; i < text1.length() + 1; i ++){
            for(int j=1; j < text2.length() + 1; j ++){
                if(text1.charAt(i - 1) == text2.charAt(j - 1)){
                    dp[i][j] = 1 + dp[i-1][j-1];
                }else{
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[text1.length()][text2.length()];
    }
}

72. 编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse ('h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值