动态规划

动态规划与分治的区别

问题都可以分小为子问题,原问题的解由子问题的最优解构成(最优子结构),要满足最优子结构子问题必须相互独立,也就是说子问题之间不能相互制约;不同之处在于,动态规划求解的问题分出来的子问题有相互重叠的部分(重叠子问题),所以如果这类问题用分治法去求效率很低(分治法不会保留子问题的解,所以相同子问题的解无法公用,会被重复计算),动态规划所要做的就是保存已经得到子问题的答案,再之后需要用的时候可以直接拿过来用(以空间换取时间)

这也就是为什么分治法一般采用递归从顶向下,而动态规划不使用递归,自底向上求解

学习告别动态规划,连刷 40 道题,我总结了这些套路,看不懂你打我一文总结

动态规划的基本要素

  1. 最优子结构:问题的最优解包含着其子问题的最优解,满足最优子结构的子问题必须相互独立
  2. 重叠子问题:每次产生的子问题不总是新问题,有些子问题被重复计算多次

动态规划的三大步骤

动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存

  1. 定义数组元素的含义
    数组(dp[])是用来保存历史数据的

  2. 找出数组元素之间的关系式
    dp[n]与dp[n-1]之间的关系

  3. 寻找初值

一维dp例子(青蛙跳台阶)

问题描述:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

这个问题的纯递归求法可以看之前文章的思路
递归与分治策略

我们知道递推关系式中f(n) = f(n-1) + f(n-2)f(n-1) = f(n-2) + f(n-3),很明显有重叠子问题,所以用动态规划来解决

  1. 定义数组元素的含义
    数组dp[]存储跳上台阶的跳法,即dp[i]的含义是小青蛙跳上i节台阶有dp[i]种跳法
  2. 找出元素之间的关系式
    dp[n]=dp[n-1]+dp[n-2]
  3. 找出初始条件
    dp[0] = 0. dp[1] = 1. 即 n <= 1 时,dp[n] = n
public static int dpFrog(int n){
        int[] dp = new int[n+1];

        //dp[n] = dp[n-1]+dp[n-2]
        for(int i = 0; i<=n; i++){
            if(i<=2){
                dp[i] = i;
            }else {
                dp[i] = dp[i-1]+dp[i-2];
            }
        }
        return dp[n];
    }

其实到这里我还要有点
所以我又学习了第二篇文章
动态规划套路详解

动态规划套路总结

递归的暴力解法 -> 带备忘录的递归解法 -> 非递归的动态规划解法

斐波那契数列

暴力递归算法

public static int fib(int n){
        if(n<=2) {
            return 1;
        }
        return fib(n-1)+fib(n-2);
    }

递归树如下(n=10)(红框部分为重复计算的子问题)
动态规划_第1张图片
该算法的时间复杂度为O(2n)

带备忘录的递归解法

	public static int[] memo;
    public static int helperFib(int n) {
        if(memo[n] == 0){
            return memo[n-1]+memo[n-2];
        }
        return memo[n];
    }

红框部分(重复子问题被剪掉不会重复计算)
动态规划_第2张图片
时间复杂度为O(n)

动态规划

动态规划实际上就是把带备忘录的递归解法变成自底向上的非递归解法

  1. 定义数组元素的含义
  2. 找出元素之间的关系式
  3. 找出初始条件
	public static int dpFib(int n){
        //数组含义
        int[] dp = new int[n+1];
        //初始条件
        dp[1] = dp[2] =1;
        for(int i = 3; i<=n; i++){
            dp[i] = dp[i-1] +dp[i-2];
        }
        return dp[n];
    }

其实还可以优化

根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1)

int fib(int n) {
    if (n < 2) return n;
    int prev = 0, curr = 1;
    for (int i = 0; i < n - 1; i++) {
        int sum = prev + curr;
        prev = curr;
        curr = sum;
    }
    return curr;
}

凑零钱问题

题目:给你 k 种面值的硬币,面值分别为 c1, c2 … ck,再给一个总金额 n,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,则回答 -1

例:coin[] = {1, 2, 5}
amount = 11

状态转移方程
动态规划_第3张图片

递归

	public static int coinChange(int amount, int[] coins) {
        if(amount == 0){
            return 0;
        }
        int miniCoin = Integer.MAX_VALUE;
        for(int i = 0; i<coins.length; i++){
            if(amount-coins[i] < 0 ){
                continue;
            }
            //1 + min{f(n-ci)}
            int subProb = coinChange(amount-coins[i],coins);
            if(subProb == -1){
                continue;
            }
            if(subProb+1 < miniCoin){
                miniCoin = subProb+1;
            }
        }
        return miniCoin==Integer.MAX_VALUE ? -1 : miniCoin;
    }

递归树:很明显有重叠子问题
动态规划_第4张图片

备忘录

	public static int memoCoinChange(int amount, int[] coins, int[] memo){
        if(amount == 0){
            return 0;
        }
        //memo初始化为-2
        if(memo[amount] != -2){
            return memo[amount];
        }
        int miniCoin = Integer.MAX_VALUE;
        for(int i = 0; i<coins.length; i++){
            if(amount-coins[i] < 0 ){
                continue;
            }
            //1 + min{f(n-ci)}
            int subProb = coinChange(amount-coins[i],coins);
            if(subProb == -1){
                continue;
            }
            if(subProb+1 < miniCoin){
                miniCoin = subProb+1;
            }
        }
        memo[amount] =  miniCoin==Integer.MAX_VALUE ? -1 : miniCoin;
        return memo[amount];
    }

动态规划

	public static int dpCoinChange(int amount, int[] coins){
        int[] dp = new int[amount+1];
        for(int i =0; i<dp.length; i++){
            dp[i] = amount;
        }
        dp[0] = 0;
        for(int i = 0; i<amount+1; i++){
            for(int n = 0; n<coins.length; n++){
                if(i - coins[n]<0){
                    continue;
                }
                dp[i] = Math.min(dp[i],1+dp[i-coins[n]]);
            }
        }
        return dp[amount];
    }

动态规划表如下:
在这里插入图片描述

0-1背包问题

背包问题:给定n种物品和一背包。物品 i 的重量为 wi,其价值为 vi,背包的容量为 c。问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?

问题分析

参考01背包问题
对于物品m — n 的装法实际上可以分成对于物品m — (n-1)的装法:
也就是说对于物品 m — n 装到容量为 j 的包中实际上可以分为
① 把物品 m — (n-1) 装到容量为 j-wn的包,再把物品n放入,或者
② 不装物品n,把物品 m — (n-1) 装到容量为 j 的包中

  1. 定义数组元素的含义
    m[n][c]表示对于第n个物品,背包容量为c时所能获得的最大价值
    (这要是不看网上的我自己咋想得到)

  2. 找出元素之间的关系式
    对于物品 i 在背包容量为 j 的时候的求法分为2种情况
    i 能被背包装下
    在这种情况下,当前背包的最大价值因该是

m[i][j]=max(m[i-1][j],m[i-1][j-w[i]]+v[i]);

也就是说要么
Ⅰ. 先把背包腾出一个物品 i 的容量,再把物品i 装进去,这个时候总价值当然就是

m[i-1][j-w[i]]+v[i]

但是因为要给物品 i 腾出容量,所以先腾出容量再把物品 i 装进去后的价值不一定比不腾出容量并且不装物品 i 产生的价值大

所以要么就
Ⅱ. 不腾空间不装物品 i

m[i-1][j]

然后在上述两种装法选择出能产生价值最大的选择

② i 不能被装下
这个时候背包的价值和之前一样

m[i][j]=m[i-1][j]

所以总结起来的状态转移方程是
动态规划_第5张图片
3. 找出初始条件

其实求解过程也就是一个填表的过程

动态规划_第6张图片

代码实现

	public static void maxBag(){
        //F(i,C) = max{F(i-1, C), v(i) + F(i-1, C-w(i))}
        //F(i-1,C)就是第i个物体装不下,没有装
        //v(i) + F(i-1, C-w(i)) 其中F(i-1,C-w(i))就是子问题的最优解
        int[] value = {0, 8, 10, 6, 3, 7, 2};//value[i]物体i的价值
        int[] weight = {0, 4, 6, 2, 2, 5, 1};//weight[i]物体i的重量
        int c = 12;
        int[][] dp = new int[7][13];// dp[i][j]指的是面对第i件物品时 背包容量为j的时候的所能获得的最大价值
        for(int i = 1; i<=6;i++){
            for(int j =1; j<=c; j++){
                if(j>=weight[i]){//装得下
                    dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
                }else {//装不下
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        System.out.println(dp[6][12]);
    }

时间复杂度:T(n) = O(n)

二维0-1背包问题

给定n种物品和一个背包,物品i的重量为wi,体积为bi,价值为vi,背包的容量为c,容积为d。问应当如何选择装入背包的物品使得装入背包的物体的总价值最大

问题分析

问题其实和普通的0-1背包问题一样,只不过多了个限制条件,即背包容积
动态规划_第7张图片

代码实现

	public static void twoDimesionBag(int[] weight, int[] value, int[] volume, int capacity, int cubage){
        //weight[i]:物体i的重量
        //value[i]:物体i的价值
        //volume[i]:物体i的体积
        //capacity:背包的容量
        //cubage:背包的容积
        int[][][] dp = new int[weight.length][capacity+1][cubage+1];
        for(int i = 1; i<weight.length;i++){
            for(int j = 1; j<=capacity; j++){
                for(int k = 1; k<=cubage; k++){
                    if(j>=weight[i]&&k>=volume[i]){
                        dp[i][j][k]=Math.max(dp[i-1][j][k],dp[i-1][j-weight[i]][k-volume[i]]+value[i]);
                    }else {
                        dp[i][j][k]=dp[i-1][j][k];
                    }
                }
            }
        }

    }

最长公共子序列

学习借鉴

字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列,使得对所有的j=0,1,…,k-1,有xij=yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列

例子:X = BDCABA,Y = ABCBDA

解题思路
对于X = x0x1x2…xm-1和Y = y0y1y2…yn-1
如果他们的最长公共子序列为Z = z0z1…zk-1
那么很明显有以下规律
(1) 如果xm-1=yn-1,则zk-1=xm-1=yn-1,且“z0,z1,…,zk-2”是“x0,x1,…,xm-2”和“y0,y1,…,yn-2”的一个最长公共子序列;

(2) 如果xm-1!=yn-1,则若zk-1!=xm-1,蕴涵“z0,z1,…,zk-1”是“x0,x1,…,xm-2”和“y0,y1,…,yn-1”的一个最长公共子序列;

(3)如果xm-1!=yn-1,则若zk-1!=ym-1,蕴涵“z0,z1,…,zk-1”是“x0,x1,…,xm-1”和“y0,y1,…,yn-2”的一个最长公共子序列;

也就是说我们求解这个问题的时候,如果用递归的思想,那么从X,Y串的最后一个元素xm-1和yn-1开始:
① 如果相等则变为求解“x0,x1,…,xm-2”和“y0,y1,…,yn-2”的子问题
② 如果不相等则是求解“x0,x1,…,xm-2”和“y0,y1,…,yn-1”和“x0,x1,…,xm-1”和“y0,y1,…,yn-2”中最长公共子序列的问题
在这里插入图片描述
但是很明显用递归求解有重复子问题,所有可以用动态规划记录下来每一个子问题,自底向上求解,也就是一个填表的过程

public static void LCSLength(String x, String y, int[][] dp, int[][] parent) {
        //x 字符串1
        //y 字符串2
        //dp 填表dp[i][j]即x的字串x[0:i-1]与y的子串y[0:j-1]的最长公共子序列
        //parent记录c[i][j]从哪个子问题得来 =1 斜上 =0左 =-1上
        for (int i = 1; i <= x.length(); i++) {
            for (int j = 1; j <= y.length(); j++) {
                if (x.toCharArray()[i - 1] == y.toCharArray()[j - 1]) {//相等的情况
                    dp[i][j] = dp[i - 1][j - 1] + 1;//dp[0][] = 0 dp[][0]=0
                    parent[i][j] = 1;//通过表格斜上方德来
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                    if (dp[i - 1][j] > dp[i][j - 1]) {
                        parent[i][j] = -1;
                    } else {
                        parent[i][j] = 0;
                    }
                }
            }
        }
    }

填表如下:
动态规划_第8张图片

最大子段和

求一个序列的最大子段和即最大连续子序列之和。例如序列[4, -3, 5, -2, -1, 2, 6, -2]的最大子段和为11=[4+(-3)+5+(-2)+(-1)+(2)+(6)]

dp[i] 表示以A[i]结尾的若干个连续子段的和的最大值,为什么dp[i]要代表这个含义而不是直接代表A[0:i]的最大子段和呢,这个是因为dp[i+1]的最大子段和如果包含了A[i+1]则一定要包含A[i],(因为要连续)明白了这个求解明了了
用max变量保存最大子段和,如果dp[i-1]>0的话,dp[i]=dp[i-1]+A[i](要以A[i]结尾),否则dp[i] = A[i];记得保留最大值

代码如下:

int max = 0;
for(int i = 1; i <= n; i ++){
   if(dp[i-1] > 0){
        dp[i] = dp[i-1] + A[i];
   }else{
        dp[i] = A[i];
   }
   if(dp[i]> max){
        max = dp[i];
   }
}

O(n)

矩阵连乘问题

学习借鉴

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

A1={30x35} ; A2={35x15} ;A3={15x5} ;A4={5x10} ;A5={10x20} ;A6={20x25}
; 最后的结果为:((A1(A2A3))((A4A5)A6)) 最小的乘次为15125。

能用动态规划解决的问题页一定可以用递归分治来解决,这个问题用递归分治来解决的思路是这样的:n个矩阵{A1,A2,…,Aj}的相乘可以转化为k个矩阵{A1,A2,…,Ak}相乘再和j-k个矩阵{Ak+1,…,Aj}相乘的问题,但是很明显,有重复子问题,所有我们要用动态规划把每个子问题记录下来,自底向上求解

从连乘矩阵个数为2开始计算每次的最小乘次数m[i][j]: m[0][1] m[1][2] m[2][3] m[3][4] m[4][5]
//m[0][1]表示第一个矩阵与第二个矩阵的最小乘次数,m[i][j]表示的就是矩阵i到j的连乘结果

然后再计算再依次计算连乘矩阵个数为3:m[0][2] m[1][3] m[2][4] m[3][5]

连乘矩阵个数为4:m[0][3] m[1][4] m[2][5]

连乘矩阵个数为5:m[0][4] m[1][5]

连乘矩阵个数为6:m[0][5] //即最后我们要的结果

那对于每个m[i][j]怎么计算呢
动态规划_第9张图片

代码实现:

public static void matrix_chain(int[] p, int n, int[][]dp,int[][]s){
        //p存矩阵的行列
        //矩阵的行列数用一个一维数组p[]来存就可以,因为两个相邻矩阵,第二个矩阵的行数一定等于第一个矩阵的列数才能相乘
        //n存矩阵的个数
        //dp[][]是动态规划表,dp[i][j]即从矩阵i到矩阵j的连乘结果
        //填表
        for(int r = 2; r<=n; r++){
            //r为要连乘矩阵的个数
            for(int i = 0; i<=n-r; i++){
                int j = i+r-1;
                dp[i][j] = Integer.MAX_VALUE;
                //找从哪里分
                for(int k = i; k<=j-1;k++){
                    int temp = dp[i][k] + dp[k+1][j] + p[i]*p[k+1]*p[j+1];
                    if(temp<dp[i][j]){
                        dp[i][j] = temp;
                        //s[][]存从矩阵i到j相乘的断点
                        s[i][j] = k;
                    }
                }
            }
        }

    }

电路布线问题

在一块电路板的上、下两端分别有n个接线柱。根据电路设计,要求用导线(i,π(i)) 将上端接线柱i与下端接线柱π(i)相连,如下图。其中,π(i),1≤ i ≤n,是{1,2,…,n}的一个排列。导线(I, π(i))称为该电路板上的第i条连线。对于任何1 ≤ i ≤ j ≤n,第i条连线和第j条连线相交的充要条件是π(i)> π(j).
动态规划_第10张图片
在制作电路板时,要求将这n条连线分布到若干绝缘层上。在同一层上的连线不相交。电路布线问题要确定将哪些连线安排在第一层上,使得该层上有尽可能多的连线。换句话说,该问题要求确定导线集Nets = {i,π(i),1 ≤ i ≤ n}的最大不相交子集。

每个新加进去的线对之前已经放好的线都会有影响
记N(i,j) = {t|(t, π(t)) ∈ Nets,t ≤ i, π(t) ≤ j }. N(i,j)的最大不相交子集为MNS(i,j)Size(i,j)=|MNS(i,j)|
(1). 当i = 1的时候
动态规划_第11张图片
(2). 当i > 1的时候,j是一个约束,对于新加进来的j,π(i)一定要小于j,不然就不在这个范围了,如果在这个范围,还要去考虑加进来的线的π(i)对之前的线的影响,既然加进来,那么你之前摆好的线的所有π值一定都要比这个π(i)小才能保证不相交,所以加不加进来是个问题,要去比较是加进来后的线更多还是不加进来线更多
动态规划_第12张图片
代码如下:

public static void MNS(int[] c, int n, int[][] size){
        //c[i]=j指i接线柱去接j
        //size为动态规划表
        //初始化
        for(int j = 0; j<c[1]; j++){
            size[1][j] = 0;
        }
        for(int j = c[1]; j<=n; j++){
            size[1][j] = 1;
        }
        for(int i = 2; i<n; i++){
            //当j
            for(int j = 0; j<c[i]; j++){
                size[i][j] = size[i-1][j];
            }
            //c[i]在范围里
            for(int j = c[i]; j<=n; j++){
                //考虑两种情况,不加入和加入
                size[i][j] =Math.max(size[i-1][j],size[i-1][c[i]-1]+1);
            }
            size[n][n]=Math.max(size[n-1][n],size[n-1][c[n]-1]+1);
        }
    }

你可能感兴趣的:(算法,算法,动态规划,java)