动态规划算法专题

目录

🌞动态规划概念

🌻动态规划具备了以下三个特点

🌻动态规划的本质

🌻动态规划的问题一般从以下四个角度考虑

💧动态规划例题

🌂一维数组相关:

☔斐波那契数列

☔股票的最大利润

☔连续子数组的最大和

🌂字符串相关:

☔字符串分割

☔回文串分割

☔编辑距离

☔不同的子序列

🌂矩阵与路径问题:

☔三角形中的最小路径和

☔路径的数目&最小路径和&礼物的最大价值

☔背包问题

😀总结

🌈动态规划状态定义

🌈状态的形式可以定义很多,如何验证状态的合理性

🌈常见问题的状态


🌞动态规划概念

动态规划是分治思想的延伸,通俗一点来说就是大事化小,小事化无的艺术。
在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。

🌻动态规划具备了以下三个特点

1.把原来的问题分解成了几个子问题

2.所有的子问题都只需要解决一次

3.储存子问题的解

🌻动态规划的本质

1.对问题状态的定义

2.状态转移方程的定义

🌻动态规划的问题一般从以下四个角度考虑

1.状态定义

2.状态间的转移方程定义

3.状态的初始化

4.返回结果

💧动态规划例题

🌂一维数组相关:

☔斐波那契数列

这是一道最经典的动态规划入门题,我们可以用递归和迭代两种思路去解答,也可以利用动态规划的思路。

public static int fibc(int n){
    if(n==0||n==1){
        return n;    
    }
    int fn=0;
    int fn1=1;
    int fn2=0;
    for(int i=2;i<=n;++i){
        fn=fn1+fn2;
        fn2=fn1;
        fn1=fn;    
    }
    return fn;
}

☔股票的最大利润

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

例如:[7,1,5,3,6,4]   5

最大利润为在最低点1买入,在之后的最高点6卖出,最大利润为5。

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length==0){
            return 0;
        }
        //定义股票的最小价格作为买入点
        int min=prices[0];
        //定义数组array[i]存放股票在i点的最大利润
        int[] array = new int[prices.length];
        //初始化在第0点时利润为0
        array[0]=0;
        for(int i=1;i<prices.length;++i){
            if(prices[i]<=prices[i-1]){       //股票跌了,此时更新买入点,但是最大利润不变
                min=Math.min(prices[i],min);
                array[i]=array[i-1];
            }else{                            //股票涨了,此时更新最大利润
                array[i]=Math.max(prices[i]-min,array[i-1]);
            }
        }
        //返回结果
        return array[prices.length-1];
    }
}

☔连续子数组的最大和

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。

例如:[-2,1,-3,4,-1,2,1,-5,4]   6

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

class Solution {
    public int maxSubArray(int[] nums) {
        int[] array = new int[nums.length];
        //定义一个数组储存0到i中子数组和的最大值
        array[0]=nums[0];
        //返回的max为array[0]到array[n]里的最大值
        int max=array[0];
        for(int i=1;i<nums.length;++i){
            //如果array[i-1]是负数,那么array[i]要更新为nums[i],否则更新为array[i-1]+nums[i]
            array[i]=Math.max(nums[i]+array[i-1],nums[i]);
            //更新最大值
            max=Math.max(array[i],max);
        }
        return max;
    }
}

🌂字符串相关:

☔字符串分割

给定一个字符串s和一簇单词dict,判断s是否可以用空格分割成一个单词序列,是的单词序列中所有的单词都是dict中的单词。

例如:s = "leetcode";    dict = ["leet","code"];

返回true

public boolean workBreak(String s,Set<String> dict){
    boolean[] canBreak = new boolean[s.length()+1];
    //设置初始状态为true
    canBreak[0] = true;
    for(int i=0;i<=s.length();++i){
        for(int j=0;j<i;++j){
            //如果前j个字符符合条件,并且第j个到第i个字符组成的单词也在字典里面,则F(i)=true
            if(canBreak[j]&&dict.contains(s.sunstring(j,i))){
                canBreak[i]=true;
                break;
            }
        }
    }
    //返回最后一个字符的状态
    return canBreak[s.length()];
}

☔回文串分割

给出一个字符串s,分割s使得分割出的每一个子串都是回文串

计算将字符串s分割成会问分割结果的最小切割数

public class Solution {
    //判断回文串函数
    public boolean isPal(String s, int start, int end){
        while(start < end){
            if(s.charAt(start) != s.charAt(end))
                return false;
            ++start;
            --end;
        }
        return true;
    }
    public int minCut(String s) {
        int len = s.length();
        int[] minCut = new int[len + 1];
        //因为每一个字符都是一个单独的回文串,那么我们初始化为最大的分割次数(字符串个数减一)
        for(int i = 0; i <= len; ++i){
            minCut[i] = i - 1;
        }
        for(int i = 1; i <= len; ++i){
            for(int j = 0; j < i; ++j){
                if(isPal(s, j,i - 1)){
                    //如果是回文串,那就更新最小分割次数
                    minCut[i] = Math.min(minCut[i], minCut[j] + 1);
                }
            }
        }
        //返回结果
        return minCut[len];
    }
}

我们发现,判断是否为回文串的函数也可以利用动态规划问题来解决

因此,代码可以优化为:

public class Solution {
    public boolean[][] getMat(String s){
        int len = s.length();
        //利用一个二维数组来储存s(i,j)是否为回文串
        boolean[][] Mat = new boolean[len][len];
        for(int i = len - 1; i >= 0; --i){
            for(int j = i; j < len; ++j){
                if(j == i){
                    //单个字符为true
                    Mat[i][j] = true;
                }else if(j == i + 1){
                    //只需判断s(i)和s(j)想不想等
                    if(s.charAt(i) == s.charAt(j)){
                        Mat[i][j] = true;
                    }else{
                        Mat[i][j] = false;
                    }
                }else{
                    //利用(i+1,j-1),并且判断s(i)和s(j)是否相等,就能知道(i,j)是否为回文串
                    Mat[i][j] = (s.charAt(i) == s.charAt(j)) && Mat[i + 1][j - 1];
                }
            }
        }
        return Mat;
    }
    public int minCut(String s) {
        int len = s.length();
        boolean[][] Mat = getMat(s);
        int[] minCut = new int[len + 1];
        for(int i = 0; i <= len; ++i){
            minCut[i] = i - 1;
        }
        for(int i = 1; i <= len; ++i){
            for(int j = 0; j < i; ++j){
                if(Mat[j][i - 1]){
                    minCut[i] = Math.min(minCut[i], minCut[j] + 1);
                }
            }
        }
        return minCut[len];
    }
}

☔编辑距离

给定两个单词word1和word2,请计算将word1转换为word2至少需要多少步操作。

你可以对一个单词执行以下三种操作

1.在单词中插入一个字符

2.删除单词中的一个字符

3.替换单词中的一个字符

public class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length();
        int len2 = word2.length();
        int[][] minDis = new int[len1 + 1][len2 + 1];
        //word1和word2有一个为0的时候,最小编辑次数就是i,可以依此进行初始化
        for(int i = 0; i <= len1; ++i){
            minDis[i][0] = i;
        }
        for(int i = 0; i <= len2; ++i){
            minDis[0][i] = i;
        }
        for(int i = 1; i <= len1; ++i){
            for(int j = 1; j <= len2; ++j){
                //先对插入和删除情况进行判断
                minDis[i][j] = 1 + Math.min(minDis[i - 1][j], minDis[i][j - 1]);
                //再对修改这种情况进行判断
                if(word1.charAt(i - 1) == word2.charAt(j - 1)){
                    minDis[i][j] = Math.min(minDis[i][j],minDis[i - 1][j - 1]);
                }else{
                    minDis[i][j] = Math.min(minDis[i][j],minDis[i - 1][j - 1] + 1);
                }
            }
        } 
        //返回结果
        return minDis[len1][len2];
    }
}

☔不同的子序列

给定两个字符串S和T,返回S子序列等于T的不同子序列的个数有多少个?

字符串的子序列是由原来的字符串删除一些字符,也可以不删除,在不改变相对位置的情况下的剩余字符

例如:S = "nowcccoder"; T = "nowccoder";

返回3

public class Solution {
    public int numDistinct(String S, String T) {
        int sLen = S.length();
        int tLen = T.length();
        int[][] numDis = new int[sLen + 1][tLen + 1];
        //都为0的时候,返回结果为1
        numDis[0][0] = 1;
        //字符串S为0的时候,返回结果为0
        for(int i = 1; i <= tLen; ++i){
            numDis[0][i] = 0;
        }
        //字符串T为0的时候,返回结果为1
        for(int i = 1; i <= sLen; ++i){
            numDis[i][0] = 1;
        }
        for(int i = 1; i <= sLen; ++i){
            for(int j = 1; j <= tLen; ++j){
                if(S.charAt(i - 1) == T.charAt(j - 1)){
                    //如果S(i-1)==T(j-1),可以选择使用和不使用两种
                    numDis[i][j] = numDis[i - 1][j] + numDis[i - 1][j - 1];
                }else{
                    //如果不等于那只能接着往后走了
                    numDis[i][j] = numDis[i - 1][j];
                }
            }
        }
        //返回结果
        return numDis[sLen][tLen];
    }
} 

🌂矩阵与路径问题:

☔三角形中的最小路径和

给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字。

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int n = triangle.size();
        int[][] array = new int[n][n];
        //初始化为顶部路径
        array[0][0] = triangle.get(0).get(0);
        for (int i = 1; i < n; ++i) {
            for (int j = 0; j <= i; ++j) {
                //在三角形最左端的情况
                if(j==0){
                    array[i][j] = array[i-1][0] + triangle.get(i).get(0);
                //在三角形最右边的情况
                }else if(j==i){
                    array[i][j] = array[i-1][j-1] + triangle.get(i).get(j);
                }else{
                    array[i][j] = Math.min(array[i-1][j-1],array[i-1][j])+triangle.get(i).get(j);
                }
            }
        }
        //处理最后一行
        int minTotal = array[n - 1][0];
        for (int i = 1; i < n; ++i) {
            minTotal = Math.min(minTotal, array[n - 1][i]);
        }
        //返回结果
        return minTotal;
    }
}

☔路径的数目&最小路径和&礼物的最大价值

这三种题目其实都是一种解法,所以就把他们放在一起了👀

而且这些题都很简单,就不详细解释了😃

路径的数目

一个机器人位于一个m✖n网格的左上角,机器人每次只能向下或者向右移动一步。机器人试图到达网格的右下角,问总共有多少条不同的路径?

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] pathNum = new int[m][n];
        初始化一直向下和一直向右的路径数目为1
        for(int i=0;i<m;++i){
            pathNum[i][0]=1;
        }
        for(int j=0;j<n;++j){
            pathNum[0][j]=1;
        }
        for(int i=1;i<m;++i){
            for(int j=1;j<n;++j){
                //因为只能向右向下走,所以(i,j)路径等于(i-1,j)和(i,j-1)路径和
                pathNum[i][j]=pathNum[i-1][j]+pathNum[i][j-1];
            }
        }
        //返回重点处的路径数目
        return pathNum[m-1][n-1];
    }
}

最小路径和

给定一个包含非负整数的m✖n网格gird,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length; 
        int n = grid[0].length;
        for(int j = 1; j < n; j++){
            grid[0][j] += grid[0][j - 1];
        }
        for(int i = 1; i < m; i++){
            grid[i][0] += grid[i - 1][0];
        }  
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                grid[i][j] += Math.min(grid[i][j - 1], grid[i - 1][j]);
            }
        }
        return grid[m - 1][n - 1];
    }
}

礼物的最大价值

在以恶搞m✖n的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格,直到到达棋盘的右下角。给定一个棋盘以及上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

class Solution {
    public int maxValue(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        for(int j = 1; j < n; j++){
            grid[0][j] += grid[0][j - 1];
        }
        for(int i = 1; i < m; i++){
            grid[i][0] += grid[i - 1][0];
        }  
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                grid[i][j] += Math.max(grid[i][j - 1], grid[i - 1][j]);
            }
        }
        return grid[m - 1][n - 1];
    }
}

☔背包问题

有n个物品和一个大小为m的背包,给定数组A表示每个物品的大小和数组V表示每个物品的价值,问最多能装入背包的总价值是多大?

public class Solution {
    public int backPackII(int m, int[] A, int[] V) {
        int n = A.length;
        //从前i个商品中选择,背包大小为j时的最大价值
        int[][] maxValue = new int[n + 1][m + 1];
        //初始化
        for(int i = 0; i <= n; ++i){
            maxValue[i][0] = 0;
        } 
        for(int i = 1; i <= m; ++i){
            maxValue[0][i] = 0;
        }
        for(int i = 1; i <= n; ++i){
            for(int j = 1; j <= m; ++j){
                //第i个物品装不下
                if(A[i - 1] > j){
                    maxValue[i][j] = maxValue[i - 1][j];
                //第i个物品装得下
                }else{
                    maxValue[i][j] = Math.max(maxValue[i - 1][j - A[i - 1]]+ V[i - 1], maxValue[i - 1][j]);
                }
            }
        } 
        //返回结果
        return maxValue[n][m];
    }
}

我们发现,二维数组中的 i 没有用到之前的数据,所以可以优化为一维数组

public class Solution {
    public int backPackII(int m, int[] A, int[] V) {
        int n = A.length;
        int[] maxValue = new int[m + 1];
        for(int i = 0; i <= m; ++i){
            maxValue[i] = 0;
        } 
        for(int i = 1; i <= n; ++i){
            for(int j = m; j > 0; --j){
                if(A[i - 1] <= j){
                    maxValue[j] = Math.max(maxValue[j - A[i - 1]] + V[i - 1], maxValue[j]);
                }
            }
        }
        return maxValue[m];
    }
}

😀总结

动态规划问题没有固定的解题模板,需要具体问题具体分析。

我们在分析问题过程中发现,动态规划问题,最难定义的就是问题的状态,那么,动态规划问题中的状态如何定义呢?

🌈动态规划状态定义

状态来源:从问题中抽象状态,一维状态或者二维状态

抽象状态:每一个状态对应一个子问题

🌈状态的形式可以定义很多,如何验证状态的合理性

1.某一个状态的解或者多个状态处理之后能否对应问题的最终解

2.状态之间要可以形成递推关系

🌈常见问题的状态

字符串:状态一般对应子串,状态中每次一般增加一个新的字符

矩阵:二维状态 👉 优化 👉 一维状态

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值