力扣刷题框架——动态规划(一)

聊聊动态规划吧

  • 1.1 凑零钱问题
    • 1. 题目描述
    • 2. 解题思路一:暴力穷举(递归)
    • 3. 解题思路二:带备忘录递归
    • 4. 解题思路三:dp
  • 1.2 最长递增子序列 (LIS)
    • 1.题目描述
    • 2. 动态规划解题思路
    • 3.二分查找解题思路
  • 1.3 最大子序和
    • 1. 题目描述
    • 2. 解题思路

本文主要是总结力扣中动态规划问题的共同点,并得到一个通用的解决方案和算法框架,依照这一方案和框架去解决更多问题。也算是labuladong的算法小抄笔记
原文链接:https://labuladong.gitee.io/algo/

题目形式:求最值,什么最长递增子序列balabalabala。核心问题就是穷举,因为要求最值就得把所有答案找出来再找最值。
特点:存在重叠子问题和最优子结构(通过子问题最值得到原问题最值)
关键:状态转移方程。
基本算法框架

# 初始化 base case
dp[0][0][...] = base
# 进行状态转移
for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 求最值(选择1,选择2...)

基本思想:数学归纳——用于寻找状态转移方程

1.1 凑零钱问题

1. 题目描述

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

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

输入
coins = [1, 2, 5], amount = 11

输出
3

解释
11 = 5 + 5 + 1

2. 解题思路一:暴力穷举(递归)

这是一个动态规划问题——求最值,具有最优子结构,子问题之间相互独立

比如你想求 amount = 11 时的最少硬币数(原问题),如果你知道凑出 amount = 10 的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 1 的硬币)就是原问题的答案。因为硬币的数量是没有限制的,所以子问题之间没有相互制,是互相独立的。

那么就开始列状态方程按照如下流程:

  1. 确定base case:目标金额 amount 为 0 时算法返回 0。
  2. 确定状态,也就是原问题和子问题中会变化的变量:题中硬币数量无限,面值也是给定的,那么唯一变量就是amount。只有目标金额会不断向base case 靠近。
  3. 确定选择,也就是导致状态转换的行为:为啥状态会变化是因为选择硬币,每选择一个面值的硬币,目标金额就会减少。
  4. DP函数: 该解法是自顶向下的,所以会有一个递归的dp函数,参数就是状态转移中会变化的量也就是目标金额,返回值就是要求计算的量也就是金币数量。

d p ( n ) = { 0 , n = 0 − 1 , n < 0 m i n { d p ( n − c o i n ) + 1 ∣ c o i n ∈ c o i n s } , n > 0 dp(n) = \begin{cases}0,n=0\\-1,n<0\\min\{dp(n-coin)+1|coin \in coins\},n>0\end{cases} dp(n)=0,n=01,n<0min{dp(ncoin)+1coincoins},n>0

   //暴力递归
    //时间复杂度是指数级别(可以化递归树,时间复杂度就是递归树节点个数×每一次递归的时间复杂度)
    //在力扣里面超时,因为子问题重复计算了
    public int coinChange(int[] coins, int amount) {
        if(amount==0) return 0;
        if(amount<0) return -1;
        int res = Integer.MAX_VALUE;
        for(int coin:coins){
            int sub = coinChange(coins,amount-coin);
            if(sub == -1) continue;
            res = Math.min(res,sub+1);

        }
        if(res!=Integer.MAX_VALUE)
            return res;
        else
            return -1;
    }

3. 解题思路二:带备忘录递归

思路一因为大量子问题被重复计算,可以用一个“备忘录”来记录子问题的解。这样在状态转移时先查找备忘录看看有没有子问题的解可以拿来直接用。这样可以把子问题数量缩小到总金额数n,处理自问题时间不变还是k,时间复杂度就是 O ( k n ) O(kn) O(kn)

   //带备忘录递归
    public int coinChange2(int[] coins, int amount) {
        if(amount<1) return 0;
        return coinChange2(coins,amount,new int[amount+1]);

    }
    public int coinChange2(int[] coins, int amount,int[] memory) {
        //base case
        if(amount==0) return 0;
        if(amount<0) return -1;
        //查看备忘录避免重复计算
        if(memory[amount]!=0) return memory[amount];
        int res = Integer.MAX_VALUE;
        for(int coin:coins){
            int sub = coinChange2(coins,amount-coin,memory);
            if(sub == -1) continue;
            res = Math.min(res,sub+1);

        }
        if(res!=Integer.MAX_VALUE) {
            memory[amount] = res;
        }
        else{
            memory[amount] = -1;

        }
        return memory[amount];

    }

4. 解题思路三:dp

思路一和思路二都是自顶向下进行状态转移,思路二使用备忘录来存储所有子问题的解,我们也可以使用dp table来记录子问题的解,解决子问题重叠的问题。也就是自底向上
思路一思路二通过定义递归函数实现状态转移:
dp函数:输入目标金额,返回凑出目标金额最少的硬币数
思路三通过定义递归数组实现状态转移:
dp数组:当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出。
代码就根据状态转移方程写

    //dp数组迭代
    public int coinChange3(int[] coins, int amount) {
        //base case
        if(amount==0) return 0;
        if(amount<0) return -1;
        int[] dp = new int[amount+1];
        int max = amount+1;//使用硬币的数量是不会超过amount+1的,实现最差也是用amount个1元硬币凑
        Arrays.fill(dp,max);//先全部置为最大
        dp[0] = 0;
        for(int i=1;i<=amount;i++){
            for(int coin:coins){
                if(i - coin<0) continue;
                dp[i] = Math.min(dp[i],1+dp[i-coin]);

            }
        }
        return dp[amount] > amount ? -1:dp[amount];

    }

1.2 最长递增子序列 (LIS)

1.题目描述

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

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

输入
nums = [10,9,2,5,3,7,101,18]

输出
4

解释
最长递增子序列是 [2,3,7,101],因此长度为 4 。

2. 动态规划解题思路

动态规划的核心设计思想是数学归纳法。
数学归纳法:我们先假设这个结论在 k 动态规划:我们可以假设 dp[0…i-1] 都已经被算出来了,然后问自己:怎么通过这些结果算出 dp[i]?
定义dp:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
base case:dp[i] 初始值为 1,因为以 nums[i] 结尾的最长递增子序列起码要包含它自己。
状态转移:已知dp[0…4]推dp[5]。只要找到前面以nums[j]结尾的子序列其中nums[j] 代码

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

    }

3.二分查找解题思路

比较难想到,稍作了解就好
纸牌游戏(耐心排序)
像遍历数组那样从左到右一张一张处理扑克牌,最终要把这些牌分成若干堆。

  • 规则:只能把点数小的牌压到点数比它大的牌上;如果当前牌点数较大没有可以放置的堆,则新建一个堆,把这张牌放进去;如果当前牌有多个堆可供选择,则选择最左边的那一堆放置。如下图所示:
    力扣刷题框架——动态规划(一)_第1张图片
  • 这样一来牌堆顶的牌有序(2, 4, 7, 8, Q)。按照上述规则执行,可以算出最长递增子序列,牌的堆数就是最长递增子序列的长度。
    LIS
    LIS就是把处理扑克牌的过程写出来,寻找适合的牌堆放牌就是二分查找。代码就不写了,有兴趣的童鞋可以试试。

1.3 最大子序和

1. 题目描述

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

输入
nums = [-2,1,-3,4,-1,2,1,-5,4]

输出
6

解释
连续子数组 [4,-1,2,1] 的和最大,为 6 。

2. 解题思路

定义dp:dp[i]表示nums[0…i]中以nums[i]结尾的最大子数组和
base case:dp[0] = nums[0]
状态转移:已知dp[i-1]怎么算dp[i]?dp[i]有两种选择,要么与前面的相邻子数组连接,形成一个和更大的子数组;要么不与前面的子数组连接,自成一派,自己作为一个子数组。
d p [ i ] = m a x ( d p [ i ] , d p [ i − 1 ] + n u m s [ i ] ) dp[i] = max(dp[i],dp[i-1]+nums[i]) dp[i]=max(dp[i],dp[i1]+nums[i])
状态压缩:注意到 dp[i] 仅仅和 dp[i-1] 的状态有关,可以只用两个状态变量

    public int maxSubArray(int[] nums) {
        int n = nums.length;
        if(n==0) return 0;
        int dp0 = nums[0],dp1 = nums[0];
        int res = dp0;
        for(int i=1;i<n;i++){
             dp1 = Math.max(nums[i],dp0+nums[i]);
             res = Math.max(res,dp1);
             dp0 = dp1;
        }
        return res;

    }

你可能感兴趣的:(力扣刷题,算法,leetcode)