动态规划专题+leetcode例题

基本概念

动态规划能解决的是一个多阶段决策过程。
每步求解的问题是后面阶段求解问题的子问题。
每步决策将依赖于以前步骤的决策结果。

优化原则:一个最优决策序列的任 何子序列本身一定是相对于子序列的初始和结束状态的最优决策序列
考虑通过什么参数界定子问题
确定子问题的依赖关系写出递推方程。
自底向上计算备忘录:

经典例题

矩阵链相乘(针对规模界定子问题)
投资问题(针对项目个数K和投资金额X界定子问题)
背包问题(针对前k个物品和背包总重量y界定子问题)
最长公共子序列(针对序列X终止位置i和序列Y种植位置j界定子问题)

动态规划题目特点

1.计数
有多少种方式走到右下角,
有多少种方法选择出K个数的和是sum
2.求最大最小值
左上到右下最大数字和
最长上升子序列长度
3.存在性
取石子
能不能选出K个数使得和是sum

动态规划类型

坐标型动态规划(机器人)(数组下标就是坐标)
序列型动态规划(前i个)
划分型动态规划(一串序列)
区间型动态规划
背包型动态规划
最长子序列型动态规划(最长上升子序列)
博弈型动态规划(下棋,取石子、取数字算必胜必败)
综合型动态规划(一种是综合以上的,另一种是和其他算法综合比如查找或者树)
**动态规划时间空间的优化——常常在followup考到。比如降维,滚动数组和打印路径。

动态规划解决步骤

  1. 确定状态:最后一步寻找子问题。通过最后一步,可以找到k-1步后的局部最优,也就是一个规模更小的子问题。(最后一枚硬币ak,子问题变成拼出面值(K-ak))
  2. 确定转移方程:想法化为方程,还是从最后一步衍生出来。
  3. 初始条件和边界情况。(解决初值和越界)
  4. 计算顺序(大多数情况是从小到大):原则就是可以复用,需要用到的都算完了。

例题

零钱兑换(最值型要用到min,max)

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
1.考虑最后一步。就是amount-(最后一个硬币)。子问题的newamount变为(amount-最后一枚硬币)
2.确定转移方程:dp[m]=max(dp[m-coin[i])
3.确定初始条件 dp[0]=0,处理边界情况,dp[]规模为amount+1,不能凑出总和要处理。
4.计算顺序就是从小到大。
代码:

class Solution {
    public int coinChange(int[] coins, int amount) {
        //确定规模大小,数组大小经常要多一个
        int []dp =new int [amount+1];
        //确定初值
        dp[0]=0;
        //i一定要从1开始,不要覆盖初始化的dp[0];
        for(int i=1;i<=amount;i++){
            //因为存在不能凑整的情况,所以对每一个dp赋最大值
            dp[i]=amount+1;
            for(int j=0;j<coins.length;j++){
                if(i>=coins[j]){
                    //状态转移方程
                    dp[i]=Math.min(dp[i],dp[i-coins[j]]+1);
                }
            }
        }
        return dp[amount]==amount+1?-1:dp[amount];
    }
}

不同路径(计数型,会用到求和)

一个机器人位于一个 m x n 网格的左上角 (。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。问总共有多少条不同的路径?
1.考虑最后一步。可以变为两个 去掉一行和去掉一列的子问题。
2.转移方程dp[i][j]=dp[i-1][j]+dp[i][j-1]
3.确定初始条件和边界:第一行第一列为1
4.确定计算顺序,按行增顺序,保证按序计算即可。
代码:

class Solution {
    public int uniquePaths(int m, int n) {
        //确定规模
        int ans[][] = new int [m][n];
        // 递推方程
        for(int i =0;i<m;i++){
            for(int j=0;j<n;j++){
                //确定初值
                if(i==0||j==0){
                    ans[i][j]=1;
                }
                else{
                    ans[i][j]=ans[i-1][j]+ans[i][j-1];
                }
            }
        }
        return ans[m-1][n-1];
    }
}

跳跃游戏(存在型,用到条件判断)

这道题可以直接遍历,在这用作举例。
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。
1.考虑最后一步:从i跳到n-1(i+nums[i]>=n-1),子问题变为跳到i。
2.状态转移方程:if(dp[i-j]处满足条件),dp[i]=true;
3.确定初始值和边界:规模n,初始值dp[0]为true;
4.计算顺序:从左到右:
代码:

class Solution {
    public boolean canJump(int[] nums) {
        boolean[] dp = new boolean[nums.length];
        dp[0]=true;
        for(int i=0;i<nums.length;i++){
            for(int j=0;j<i;j++){
                if(dp[j]&&j+nums[j]>=i){
                    dp[i]=true;
                }
            }
        }
        return dp[nums.length-1];
    }
}

三角形最小路径和

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
样例:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
结果:11

暴力DP

    public int minimumTotal(List<List<Integer>> triangle) {
        //最后一步:倒数第二行走到最后一行。子问题变为两个减少一行的子问题
        //递推方程:dp[i][j]=Math.min(dp[i-1][j],dp[i-1][j-1])
        //初始条件dp[0][0]=0;边界条件处理三角形两边边界。
        //计算顺序:从上到下
        int [][] dp= new int [triangle.size()][triangle.size()];
        int k=0;
        for(int i=0;i<triangle.size();i++){
            k++;
            for(int j=0;j<k;j++){
                if(i==0&&j==0)
                    dp[i][j]=0;
                else{
                    if(j==0)
                        dp[i][j]=dp[i-1][j]+triangle.get(i-1).get(j);
                    else if(j==k-1)
                        dp[i][j]=dp[i-1][j-1]+triangle.get(i-1).get(j-1);
                    else
                        dp[i][j]=Math.min(dp[i-1][j]+triangle.get(i-1).get(j),dp[i-1][j-1]+triangle.get(i-1).get(j-1));
                }
            }
        }
        int res =dp[triangle.size()-1][0]+triangle.get(triangle.size()-1).get(0);
        for(int i=1;i<triangle.size();i++){
            res=Math.min(dp[triangle.size()-1][i]+triangle.get(triangle.size()-1).get(i),res);
        }
        return res;
    }

压缩状态矩阵

分析二维dp数组中可以被覆盖的内容,每当遍历到一行时,只需要保留上一行的内容即可。
所以使用类似背包问题的处理方法,从大到小遍历体积改变为从大到小遍历行索引,可以保证dp[j]中存的数为上一行,且不被本行计算覆盖。
代码:

    public int minimumTotal(List<List<Integer>> triangle) {
        int [] dp= new int [triangle.size()];
        int k=1;
        for(int i=1;i<triangle.size();i++){
            k++;
            for(int j=k-1;j>=0;j--){
                if(j==0)
                    dp[j]=dp[j]+triangle.get(i-1).get(j);
                else if(j==k-1)
                    dp[j]=dp[j-1]+triangle.get(i-1).get(j-1);
                else
                    dp[j]=Math.min(dp[j]+triangle.get(i-1).get(j),dp[j-1]+triangle.get(i-1).get(j-1));
            }
        }
        int res =dp[0]+triangle.get(triangle.size()-1).get(0);
        for(int i=1;i<triangle.size();i++){
            res=Math.min(dp[i]+triangle.get(triangle.size()-1).get(i),res);
        }
        return res;
    }

自底向上的方法

这道题可以反过来考虑,从下向上走,可以直接把距离存在原始ArrayList中

扫描二维码关注公众号,回复: 11229798 查看本文章
class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int m=triangle.size();
        if(m==0)return 0;
        int column=m-1;
        for(int i=m-2;i>=0;i--){
            for(int j=0;j<column;j++){
                triangle.get(i).set(j,triangle.get(i).get(j)+Math.min(triangle.get(i+1).get(j),triangle.get(i+1).get(j+1)));
            }
            column--;
        }
        return triangle.get(0).get(0);
    }
}

leetcode221 最大正方形

在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。
示例:
输入:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
输出: 4

暴力DP

找到转移方程即可解决

    public int maximalSquare(char[][] matrix) {
        //dp[i][j]=Math.min(dp[i-1][j],dp[i-1][j-1],dp[i][j-1])+1;
        int M=matrix.length;
        if(M==0) return 0;
        int N=matrix[0].length;
        int []dp = new int[N];
        int res=0;
        for(int i=0;i<M;i++){
            for(int j=0;j<N;j++){
                if(i==0||j==0)
                    dp[j]=matrix[i][j]-'0';
                else if(matrix[i][j]=='1')
                    dp[j]=Math.min(dp[j],Math.min(dp[j-1],dp[j-1]))+1;
                else
                    dp[j]=0;
                if(res<dp[j])
                    res= dp[j];
            }
        }
        return res*res;
    }

压缩状态矩阵

这里压缩到一维遇到的问题是会覆盖一部分内容。
dp[i][j]=Math.min(dp[i-1][j],dp[i-1][j-1],dp[i][j-1])+1;
变为dp[j]=Math.min(dp[j],dp[j-1],dp[j-1])+1;
发现在dp[j-1]是冲突的
所以要增加一个prej参数辅助,在每次对dp[j]赋值时把原来的dp[j]保存起来在计算dp[j+1]时使用。
相比于背包的压缩,由于依赖关系不同,计算顺序不同,且需要一个额外的空间记录。

class Solution {
    public int maximalSquare(char[][] matrix) {
        //原则就是在dp[j]赋值之前记录prej
        int M=matrix.length;
        if(M==0) return 0;
        int N=matrix[0].length;
        int []dp = new int[N];
        int res=0,prej=0,tempj=0;
        for(int i=0;i<M;i++){
            for(int j=0;j<N;j++){
                if(i==0)
                    dp[j]=matrix[i][j]-'0';
                else if(j==0){
                    prej=dp[j];
                    dp[j]=matrix[i][j]-'0';
                }
                else if(matrix[i][j]=='1'){
                    //这里增加一个temp保存dp[j],因为这里prej要先用,后赋值为dp[j]
                    tempj=dp[j];
                    dp[j]=Math.min(dp[j],Math.min(dp[j-1],prej))+1;
                    prej=tempj;
                }
                else{
                    prej=dp[j];
                    dp[j]=0;
                }
                if(res<dp[j])
                    res= dp[j];
            }
        }
        return res*res;
    }
}

猜你喜欢

转载自blog.csdn.net/tianyouououou/article/details/105028881
今日推荐