动态规划(逐级总结)

本文详细介绍了动态规划在解决斐波那契数列、路径问题(如地下城)、买卖股票IV、单词拆分、最长等差数列、回文子串检测、最长重复子数组、01背包问题(如分割等和子集)以及完全背包问题(如零钱兑换II)中的应用,通过实例演示了状态转移方程、初始化和填表顺序的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

注:此篇宏观看待动态规划问题(分步解决问题)

日升时奋斗,日落时自省

目录

1、斐波那契数列模型(爬楼梯)

2、路径问题(地下城)

3、简单多状态问题(买卖股票IV)

4、子数组系列(单词拆分)

5、子序列问题(最长等差数列)

6、回文串问题(回文子串)

 7、两个数组的dp问题(最长重复子数组)

8、01背包问题(分割等和子集)

9、完全背包问题(零钱兑换II)


1、斐波那契数列模型(爬楼梯)

来源力扣:746. 使用最小花费爬楼梯 - 力扣(LeetCode)

 圈了这么几个地方,就是想要到最后一个格子的时候需要的最小花费

状态表示:dp[i]表示第i个位置,最小花费 (题上想要啥满足就是)

状态转移方程:

 初始化(最后两个位置):dp[m-1]=cost[m-1];dp[m-2]=cost[m-2];

前者的数据需要的后者来支持,所以这里初始化后面的数据,我们从后面开始动态规划

填表顺序:

从右往左

返回值:min(dp[0],dp[1])   因为我们可能是从0或者1出发的,所以这里取0或者1最小值

代码:

    public int minCostClimbingStairs(int[] cost) {
        int m=cost.length;
        int[] dp=new int[m];
        dp[m-1]=cost[m-1];
        dp[m-2]=cost[m-2];
        for(int i=m-3;i>=0;i--){
            dp[i]=Math.min(dp[i+1],dp[i+2])+cost[i];
        }
        return Math.min(dp[0],dp[1]);
    }

2、路径问题(地下城)

来源力扣:174. 地下城游戏 - 力扣(LeetCode)

状态分析:

 状态表示(明显是一个二维表):

错误分析:dp[i][j]表示从起点出发,到[i][j]位置的时候,所需血量最小

正确分析:dp[i][j]:表示从[i,j]出发,到终点

初始化:

边界初始化,上图已经给了

 填表顺序:

从下往上,从有往左

返回值:dp[0][0]

代码:

    public int calculateMinimumHP(int[][] dungeon) {
        int n=dungeon.length;
        int m=dungeon[0].length;
        int[][] dp=new int[n+1][m+1];
        for(int i=0;i<m-1;i++){
            dp[n][i]=Integer.MAX_VALUE;
        }
        for(int j=0;j<n-1;j++){
            dp[j][m]=Integer.MAX_VALUE;
        }
        dp[n-1][m]=1;
        dp[n][m-1]=1;
        for(int i=n-1;i>=0;i--){
            for(int j=m-1;j>=0;j--){
                dp[i][j]=Math.min(dp[i+1][j],dp[i][j+1])-dungeon[i][j];
                dp[i][j]=Math.max(1,dp[i][j]);
            }
        }
        return dp[0][0];
    }

3、简单多状态问题(买卖股票IV)

 来源力扣:188. 买卖股票的最佳时机 IV - 力扣(LeetCode)

状态分析:

f[i][j](表示买入状态):  到第i天 ,完成j次交易 

g[i][j](表示卖出状态):到第i天,完成j次交易

状态转移方程:

状态转移:

f[i][j]=max(f[i-1][j],g[i-1][j]-p[i]); 

由两部分构成

一部分前一天啥也不做还是买入状态就是 i-1,次数还是当前次数j;

另一部分前一天卖出次数仍然没有改变(买入态)i-1,次数还是当前次数j;

g[i][j]=max(f[i-1][j-1]+p[i],g[i-1][j]);

由两部分构成

一部分前一天啥也不做还是卖出状态就是 i-1,次数还是当前次数j;

另一部分前一天买入次数改变状态(卖出态)i-1,次数+1,我们需要的是上一次的j-1;

初始化:

 填表顺序:

从上往下,从左往右

返回值:需要找到最后一行的最大值,相当于是最后一天(没有交易,交易1次....交易n次的结果)

代码:

    public int maxProfit(int k, int[] prices) {
        int n=prices.length;
        int[][] f=new int[n][k+1];
        int[][] g=new int[n][k+1];
        f[0][0]=-prices[0];
        g[0][0]=0;
        //初始化
        for(int i=1;i<3;i++){
            f[0][i]=-0x3f3f3f;
            g[0][i]=-0x3f3f3f;
        }
        //状态转义
        for(int i=1;i<n;i++){
            for(int j=0;j<k+1;j++){
                f[i][j]=Math.max(f[i-1][j],g[i-1][j]-prices[i]);
                g[i][j]=g[i-1][j];
                if(j-1>=0){
                    g[i][j]=Math.max(g[i][j],f[i-1][j-1]+prices[i]);
                }
            }
        }
        int ret=0;
        //从最后一行动规中找最大值
        for(int i=0;i<k+1;i++){
            ret=Math.max(ret,g[n-1][i]);
        }
        return ret;
    }

4、子数组系列(单词拆分)

 来源力扣:139. 单词拆分 - 力扣(LeetCode)

状态分析:

状态转义方程:

决定dp[i]的就是两部分是否为 true

dp[j-1]&& substring(j,i+1)

但是为了判断方便这里将单词存到hash表中为了判断截取是否包含单词

初始化:

开始的时候dp[0]就是空串,啥也没有同样为真,因为单词序列中也没有 dp[0]=true

填表顺序:

从左往右

返回值:dp[n]

代码:

    public boolean wordBreak(String s, List<String> wordDict) {
        /*
        * 优化 将字典里面的单词存到哈希表中
        * */
        Set<String> hash=new HashSet<>(wordDict);

        int n=s.length();
        boolean[] dp=new boolean[n+1];
        dp[0]=true;
        s=" "+s;   //加空串是为了动规 不用去处理下标映射问题
        for(int i=1;i<=n;i++){
            //第二次for循环没有方向要求, j=1; j<=i ;j++ 也行
            for(int j=i;j>=1;j--){
                if(dp[j-1]&&hash.contains(s.substring(j,i+1))){
                    dp[i]=true;
                    break;
                }
            }
        }
        return dp[n];
    }

5、子序列问题(最长等差数列)

注:子序列和子数组是不一样的

子序列是不一定连续的子集,子数组是连续的子集

 来源力扣:1027. 最长等差数列 - 力扣(LeetCode)

状态分析:

动态转义方程:

那这么才能知道k下标的数字是不是在数组中,那就把下标和数字关联起来,来一个哈希关联起来

 dp[i][j]=dp[2b-c][i]+1 

然后记录数据

初始化:等差数列至少需要3个的,基本数量为2,a需要b、c才能确定

填表顺序:从前往后,因为第三个数据需要依赖前两个值才能确定找不找到

返回值:统计的个数

代码:

    public int longestArithSeqLength(int[] nums) {
        Map<Integer,Integer> hash=new HashMap<>();
        //首先将第一个值
        hash.put(nums[0],0);
        int n=nums.length;
        int[][] dp=new int[n][n];
        //进行初始化为 个数2
        for(int i=0;i<n;i++){
            Arrays.fill(dp[i],2);
        }
        int ret=2; //计算个数为2
        for(int i=1;i<n;i++){   //相当于 b
            //第二个值从 b位置开始   j的位置相当于c
            for(int j=i+1;j<n;j++){
                //计算 a 的 2b-c
                int a=2*nums[i]-nums[j];
                //此时判断 hash 是否包含 a 
                if(hash.containsKey(a)){
                    dp[i][j]=dp[hash.get(a)][i]+1;
                    //计算次数
                    ret=Math.max(dp[i][j],ret);
                }
            }
            //将本次数字添加到hash
            hash.put(nums[i],i);
        }
        return ret;
    }

6、回文串问题(回文子串)

 来源力扣:647. 回文子串 - 力扣(LeetCode)

状态分析:

状态转义方程:

根据状态分析写

如果s[i]!=s[j] 说明dp[i][j]==false;这个基本不用写

后面三个条件可以直接合成一个三目运算

如果s[i]==s[j]则 dp[i][j]=i+1<j?dp[i+1][j-1]:true

如果i+1<j 说明不是相邻也不是重叠那就需要区间[i+1,j-1]是true

初始化:不用初始化

填表顺序:其实需要看状态分析 中的dp[i][j]可能会依赖到dp[i+1][j-1]

从下往上,从左往右  i表示行  j表示列

返回值:统计有多少个子串,把dp中为true都计数一下

代码:

    public int countSubstrings(String s) {
        int n=s.length();
        //创建后默认为false  为false的情况不用处理
        boolean[][] dp=new boolean[n][n];
        int ret=0;
        for(int i=n-1;i>=0;i--){
            for(int j=i;j<n;j++){
                //只需要相等设置为true
                if(s.charAt(i)==s.charAt(j)){
                    dp[i][j]=i+1<j?dp[i+1][j-1]:true;
                }
                //如果是true说明本身计数
                if(dp[i][j]==true){
                    ret++;
                }
            }
        }
        return ret;
    }

注:为啥第二次for循环从i开始 因为j等于>=i所以j从i开始

 7、两个数组的dp问题(最长重复子数组)

力扣来源:718. 最长重复子数组 - 力扣(LeetCode)

状态分析:

 状态转义方程:

如果两个数组数值相同的话

不等的情况下就不用处理了,相等的话满足动规dp[i][j]=dp[i-1][j-1]+1

初始化:

填表顺序:

从上往下,从左往右

返回值:统计的个数

代码:

    public int findLength(int[] nums1, int[] nums2) {
        int m=nums1.length;
        int n=nums2.length;
        int[][] dp=new int[m+1][n+1];
        //计数器
        int ret=0;
        for(int i=1;i<=m;i++){
            for(int j=1;j<=n;j++){
                //如果两个值相等的话
                if(nums1[i-1]==nums2[j-1]){
                    //相当于个数加一
                    dp[i][j]=dp[i-1][j-1]+1;
                    //求取最大长度
                    ret=Math.max(ret,dp[i][j]);
                }
            }
        }
        return ret;
    }

8、01背包问题(分割等和子集)

01背包问题:就是满足于

牛客来源:416. 分割等和子集 - 力扣(LeetCode)

状态分析:

状态转义方程:

初始化:看见 dp[i-1][j-num[i]],需要依赖上一行,所以需要一个扩展行

填表顺序:

从左往右,从上往下

返回值:dp[n][sum/2]

代码:

    public boolean canPartition(int[] nums) {
        int n=nums.length;
        int sum=0;
        //求所有和
        for(int x:nums)sum+=x;
        //如果是奇数 就没有相等的情况
        if(sum%2==1)return false;
        //求子集的一半
        int aim=sum/2;
        //行要多一行  aim 可以不加1 这里只是为了方便理解
        boolean[][] dp=new boolean[n+1][aim+1];
        //一行所有的值 为 true
        for(int i=0;i<=n;i++){
            dp[i][0]=true;
        }
        for(int i=1;i<=n;i++){
            //从 i 开始  避开开始的位置
            for(int j=1;j<=aim;j++){
                //不选择 第i个位置 
                dp[i][j]=dp[i-1][j];
                //选择第i个位置 需要条件的 当前值 大于 数值
                if(j>=nums[i-1]){
                    //选i 和 不选i 的情况 只要能构成就可以子集一半就行
                    dp[i][j]=dp[i][j]||dp[i-1][j-nums[i-1]];
                }
            }
        }
        return dp[n][aim];
    }

 优化:

 代码:

简单点说:直接把行都删了就行  (这里算是优化版的优化,直接让数组从大于nums[i-1]的位置开始的就不用判断j>nums[i-1])

    public boolean canPartition(int[] nums) {
        int n=nums.length;
        int sum=0;
        for(int x:nums)sum+=x;
        if(sum%2==1)return false;
        int aim=sum/2;
        boolean[] dp=new boolean[aim+1];
        dp[0]=true;
        for(int i=1;i<=n;i++){
            for(int j=aim;j>=nums[i-1];j--){
                dp[j]=dp[j]||dp[j-nums[i-1]];
            }
        }
        return dp[aim];
    }

9、完全背包问题(零钱兑换II)

力扣来源:518. 零钱兑换 II - 力扣(LeetCode)

状态分析:

 状态转义方程:

但是 能不能选 i 是需要满足条件的  满足条件就是 j-n*coins>=0 就能满足

初始化:

填表顺序:从左往右,从上往下

返回值: dp[n][amount]

代码:

    public int change(int amount, int[] coins) {
        int n=coins.length;
        int[][] dp=new int[n+1][amount+1];
        //设置第一个值 为 1
        dp[0][0]=1;
        for(int i=1;i<=n;i++){
            //从 0 开始 
            for(int j=0;j<=amount;j++){
                //不选 i
                dp[i][j]=dp[i-1][j];
                //选 i 的时候 就需要 j>=coins 才能加上
                if(j>=coins[i-1]){
                    dp[i][j]+=dp[i][j-coins[i-1]];
                }
            }
        }
        return dp[n][amount];
    }

空间优化: 

简单点说,去掉行,填表顺序是从左往右

代码:

    public int change(int amount, int[] coins) {
        int n=coins.length;
        int[] dp=new int[amount+1];
        dp[0]=1;
        for(int i=1;i<=n;i++){
            for(int j=coins[i-1];j<=amount;j++){
                    dp[j]+=dp[j-coins[i-1]];
            }
        }
        return dp[amount];
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值