动态规划及相关算法题总结

1. 概念

动态规划和分治法类似,把原问题划分成若干个子问题,不同的是,分治法,子问题间相互独立,动态规划,子问题不独立。

在这里插入图片描述

动态规划解题一般分为三个步骤:

  1. 表示状态

分析问题的状态时,不要分析整体,只分析最后一个阶段即可!因为动态规划问题都是划分为多个阶段的,各个阶段的状态表示都是一样,而我们的最终答案在就是在最后一个阶段。每一种状态都是使用数组的位数来表示的。一般我们会先使用数组的第一维来表示阶段,然后再根据需要通过增加数组维数,来添加其他状态。

  1. 找出状态转移方程

状态转移指的是,当前阶段的状态如何通过之前计算过的阶段状态得到。

  1. 初始化边界

初始化边角状态,以及第一个阶段的状态。

整个动态规划,最难的就是定义状态。一旦状态定义出来,表明你已经抽象出了子问题,可以肢解原来的大问题了。

2. 相关算法题

2.1 斐波那契数列

斐波那契数列指的是这样一个数列:1、1、2、3、5、8、13、21、34、…… 其规律很明显,从第3个数开始,每个数都等于它前两个数的和。
递归解法

//递归方式
    public static int fibonacci(int i){
        if(i < 2) return i;
        return fibonacci(i - 1) + fibonacci(i - 2);
    }

使用递归的方式,会有好很多的重复计算
在这里插入图片描述
使用动态规划的方式,来避免重复计算

解题思路

定义一个一维数组dp,dp[ i ] 代表斐波那契数列第i个数字,转移方程为dp[ i ] = dp[ i - 1 ] + dp[ i - 2 ],初始化dp[ 0 ] = 1,dp[ 1 ] = 1,之后,从2开始循环,直到计算出要求的第n个数字结束。

// 动态规划
    public static int fibonacci2(int i){
        //定义一个一维数组dp,dp[i]表示斐波那契数列第i个数字
        int[] dp = new int[i + 1];
        dp[0] = 0;
        dp[1] = 1;
        for(int j = 2; j < i + 1; j++){
            dp[j] = dp[j - 1] + dp[j - 2];
        }
        return dp[i];
    }

与之类似的还有:跳台阶问题:每次只能跳一个或者两个台阶,跳到n层台阶上有几种方法 。

2.2 矩阵连乘问题

给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2…,n-1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。

例如,给定三个连乘矩阵{A1,A2,A3}的维数分别是10100,1005和550,采用(A1A2)A3,乘法次数为101005+10550=7500次,而采用A1(A2A3),乘法次数为100550+10100*50=75000次乘法,显然,最好的次序是(A1A2)A3,乘法次数为7500次。

矩阵乘法
矩阵A 和矩阵B,只有当A的列数和B的行数相等的时候才可以相乘,假如A(m x n),B(n x k),A x B 得到一个m 行 k列的矩阵,具体乘法是拿A的第一行与B的第一列中各个元素的乘积的和为结果矩阵的第一行第一列的数,然后拿A的第一行和B的第二列的各个元素的乘积作为第一行第二列的数…。

解题思路

定义一个二维数组m,m[ i ] [ j ] 表示第i个矩阵到第j个矩阵连乘的最小次数,则最优值就是m[ 1 ] [ n ]。假设A1A2…An的一个最优加括号把乘积在Ak和Ak+1间分开,则前缀子链A1…Ak的加括号方式必定为A1…Ak的一个最优加括号,后缀子链同理。
所以m[ i ] [ j ] = min( m[ i ] [ k ] + m [ k + 1 ] + p(i - 1) * p(k) *p(j) ) ( p(i - 1) 、p(k)、p(j) 分别是第i个矩阵的行数,第k个矩阵的列数,第j个矩阵的列数),如果i = j,m[ i ] [ j ] = 0

一开始并不知道k的确切位置,需要遍历所有位置以保证找到合适的k来分割乘积。

对于一组矩阵:A1(30x35),A2(35x15),A3(15x5),A4(5x10),A5(10x20),A6(20x25) 个数N为6

那么p数组保存它们的行数和列数:p={30,35,15,5,10,20,25}共有N+1即7个元素

p[0],p[1]代表第一个矩阵的行数和列数,p[1],p[2]代表第二个矩阵的行数和列数…p[5],p[6]代表第六个矩阵的行数和列数

在这里插入图片描述
在这里插入图片描述
辅助表m: m[i][j]代表从矩阵Ai,Ai+1,Ai+2…直到矩阵Aj最小的相乘次数,比如A[2][5]代表A2A3A4A5最小的相乘次数,即最优的乘积代价。我们看上图,从矩阵A2到A5有三种断链方式:A2{A3A4A5}、{A2A3}{A4A5}、{A2A3A4}A5,这三种断链方式会影响最终矩阵相乘的计算次数,我们分别算出来,然后选一个最小的,就是m[2][5]的值,同时保留断开的位置k在s数组中。

代码

public class MartixChain {
    /**
     *
     * @param p p为矩阵链,p[0],p[1]代表第一个矩阵的行数和列数,p[1],p[2]代表第二个矩阵的行数和列数
     * @return 返回最优值
     */
    public static int martixChain(int[] p){

        //计算矩阵的个数
        int n = p.length - 1;
        //m[i][j],表示第i个矩阵到第j个矩阵连乘的最小相乘次数
        int[][] m = new int[n + 1][n + 1];
        //s[i][j]=k,表示,第i个矩阵到第j个矩阵的连乘,从第k个矩阵分割,相乘次数最小
        int[][] s = new int[n + 1][n + 1];
        //初始化二维数组,将对角线位置上的值设为0,本身就是0,可以上略
        for(int i = 0; i < n + 1; i++){
           m[i][i] = 0;
        }

        for(int r = 2; r < n + 1; r++){//表示r个矩阵相乘
            // i表示从第i个矩阵开始,r个矩阵连乘。n-r,表示r个矩阵相乘,最左边的矩阵是第n-r+1个,
            // 如果超过这个,最后一组r个矩阵就不是r个了,也就是i的左边界
            for(int i = 1; i <= n - r + 1; i++){
                //j 表示从第i个矩阵开始,长度为r的矩阵链的最后一个矩阵
                int j = i + r - 1;
                //先以i进行划分
                m[i][j] = m[i + 1][j] + p[i - 1] * p[i] *p[j];
                //记录划分位置
                s[i][j] = i;

                //寻找使矩阵相乘次数最小的划分点
                for(int k = i + 1; k < j; k++){
                    int t = m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j];
                    if(t < m[i][j]){
                        m[i][j] = t;
                        s[i][j] = k;
                    }
                }
            }
        }
        return m[1][n];
    }
    
    public static void main(String[] args) {
        int[] p = new int[]{5, 7, 4, 3, 5};
        System.out.println(martixChain(p)); // 264
    }
}

2.3 剑指 Offer 42. 连续子数组的最大和

输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

示例1:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

解题思路

使用动态规划算法,定义一个数组dp,dp[i]代表以nums[i]结尾的连续子数组的最大和。如果dp[i - 1] >= 0,dp[i]=dp[i-1]+nums[i],如果
dp[i - 1] < 0,dp[i] = nums[i]。dp[0] = nums[0]

public static int maxSubArray(int[] nums){
        if(nums == null || nums.length == 0) return 0;
        //dp[i]表示以nums[i]结尾的连续子数组的最大和
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        int res = nums[0];
        for(int i = 1; i < nums.length; i++){
            if(dp[i - 1] < 0){
                dp[i] = nums[i];
            }else {
                dp[i] = dp[i - 1] + nums[i];
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }

这道题,也可以不使用数组,只使用一个变量来记录以nums[i]结尾的连续子数组的最大和

public static int maxSubArray2(int[] nums){
    if(nums == null || nums.length == 0) return -1;
    int max = nums[0];
    int res = nums[0];
    for (int i = 1; i < nums.length; i++) {
        if(max < 0){
            max = nums[i];
        }else{
            max = max + nums[i];
        }
        res = Math.max(max, res);
    }
    return res;
}

2.4 01揹包问题

有n个物品,它们有各自的体积和价值,现有给定容量的揹包,如何让揹包里装入的物品具有最大的价值总和?
eg:number=4,capacity=8

i(物品编号) 1 2 3 4
w(体积) 2 3 4 5
v(价值) 3 4 5 6

解题思路

  1. 表示状态
    V[i ,j]表示当前揹包容量 j,前 i 个物品最佳组合获得的最大的价值
  2. 找出状态转移方程
    揹包容量不足以装物品i(w(i) 大于揹包容量)则装入前i个物品得到的最大价值和装入前i - 1个物品的最大价值是相同的,即V[i ,w] = V[i - 1,w]
    揹包容量可以装入物品i,可以装也可以不装,所以V[i,j] = max { V[i - 1 ,j - w(i)] + v(i) , V[i - 1,j] }

为什么揹包容量足够的情况下,还需要 V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}?

V(i-1,j-w(i))+v(i)表示装了第i个物品后背包中的最大价值,所以当前揹包容量 j 中,必定有w(i)个容量给了第i个揹包。
因此只剩余j-w(i)个容量用来装除了第i件物品的其他所有物品。
V(i-1,j-w(i))是前i-1个物品装入容量为j-w(i)的揹包中最大价值。
注意,这里有一个问题。前i-1个物品装入容量为j-w(i)的揹包中最大价值+物品i的价值。可能不如将,前i-1个物品装入容量为j的揹包中得到的价值大。也就是说,可能出现 V(i-1,j) > (V(i-1,j-w(i))+v(i))
比如说,将第i个物品放入揹包,可能会导致前面更有价值的物品放不进揹包。因此,还不如不把第i个物品放进去,把空间让出来,从而能让前i-1个物品中更有价值的物品能够放入揹包。从而让V(i,j)取得最大的值。
所以我们需要 max{V(i-1,j),V(i-1,j-w(i))+v(i)},来作为把前i个物品装入容量为j的揹包中的最优解。

根据上面的例子,重量分别为2,3,4,5 ,价值分别为3,4,5,6,商品编号是1,2,3,4
在这里插入图片描述
表中横座标代表容量j,纵座标代表商品编号i,表中数字代表在揹包容量为j时,前i个物品最佳组合获得的最大价值dp[ i ][ j ]。物品最小的重量为2,所以容量为0、1时,表格中都为0,并且纵座标为0的那一列,也全为0,因为0个商品,价值为0。当容量为2时,1号商品,重量为2,可以放,也可以不放,因为1号商品前面没有商品了,所以前1个物品,获得的最大价值为3,不管容量为几都为3,看2号商品,揹包容量为2时,没法放2好商品,所以dp[ 2 ][ 2 ] = dp[ 1 ][ 2 ],当揹包容量为3时, 2号商品可以放,也可以不放,主要看价值,dp[ 1 ][ 3 ] = 2,
而3号商品的价值为3,所以应该放如揹包,这样,揹包容量为3时,就不放1号商品了,dp[ 2 ][ 3 ] = max { dp[ 1 ] [ 3 -2] + v(2) ,dp [ 1 ][ 3 ] } = 3。

只要上面的图理解了,以及dp[ i ] [ j ] 代表的含义理解了,01揹包问题就很简单了。

代码实现

public class ZeroOnePack {
    /**
     *
     * @param n 物品数量
     * @param v 揹包容量
     * @param weights 物品的重量
     * @param values  物品的价值
     * @return  返回容量为v的揹包所能获得的最大价值
     */
    public static void zeroOnePack(int n, int v, int[] weights, int[] values){
        //初始化动态规划数组,dp[i][j]代表揹包容量为j时,前i个物品最佳组合所能获得的最大价值
        int[][] dp = new int[n + 1][v + 1];
        //为了便于理解,将dp[i][0]和dp[0][j]均置为0,从1开始计算
        for(int i = 1; i < n + 1; i++){
            for(int j = 1; j < v + 1; j++){
                //如果第i件物品的重量大于揹包容量j,则不装入揹包
                //由于weight和value数组下标都是从0开始,故注意第i个物品的重量为weight[i-1],价值为value[i-1]
                if(weights[i - 1] > j){
                     dp[i][j] = dp[i - 1][j];
                 }else {
                     dp[i][j] = Math.max(dp[i - 1][j - weights[i - 1]] + values[i - 1],dp[i - 1][j]);
                 }
            }
        }
        //获得的最大商品重量
        int maxValue = dp[n][v];
        System.out.println("获得的最大商品价值为" + maxValue);
       
        //通过回溯法算出哪些商品被装到揹包里了。从dp[n][v]开始,如果dp[n][v]=dp[n-1][v]
        //说明,第n件商品没有被装到揹包里,如果不相等,说明,被装到揹包里了,之后,减去
        //被装入商品的重量,再判断容量为当前容量时物品是否装入揹包了(重复上述过程)。
        
        int j = v;
        for(int i = n; i > 0; i--){
            //如果dp[i][j] > dp[i - 1][j],说明,第i件商品是放入揹包的
            if(dp[i][j] > dp[i - 1][j]){
                System.out.print(i + " ");
                j = j - weights[i - 1];
            }

            if(j == 0){
                break;
            }
        }
    }

}

2.5 面试题66. 构建乘积数组

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B 中的元素 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。
示例:

输入: [1,2,3,4,5]
输出: [120,60,40,30,24]

提示:

所有元素乘积之和不会溢出 32 位整数
a.length <= 100000

解题思路

题目中求的是每一个元素左边的元素和右边的元素的乘积,不包括当前元素,可以分别求出每一个元素左边元素的乘积和右边元素的乘积,并且计算的时候,后一个左边元素的乘积是前一个数左边元素的乘积乘以前一个元素,比如3的左边元素的乘积,是2的左边元素的乘积乘以2,同理,每一个数的右边元素的乘积,是这个数后边的元素的乘积乘以后边的元素。

代码

class Solution {
    public int[] constructArr(int[] a) {
        //用动态规划的思想解题,先算出每一个元素从第一个元素开始到当前元素之前的一个元素的
        //乘积,再算出从数组末尾开始,每一个元素到当前元素的前一个元素的乘积,将前面算出的
        //结果一一相乘就是答案
        int n = a.length;
        if(n == 0) return new int[0];
        int[] left = new int[n];
        //第一个数的左边的数的乘积是1
        left[0] = 1;
        //算出每一元素左边的乘积(不包括自己)
        for(int i = 1; i < n; i++){
            left[i] = left[i - 1] * a[i - 1];
        }
        //算出每一个元素右边的乘积(不包括自己),可以定义一个temp变量代替数组,temp每一个数的右边元素的乘积
        //最后一个数的右边元素的乘积是1
        int temp = 1;
        int[] res = new int[n];
        res[n - 1] = left[n - 1];
        for(int i = a.length - 2; i >= 0; i--){
            temp = temp * a[i + 1];
            res[i] = left[i] * temp;
        }
        return res;
    }
}

如有不足之处,欢迎指正,谢谢!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章