五大常用算法之三:动态规划

动态规划:

动态规划(Dynamic Programming,简称DP),需要分解出问题的子结构以及通过子结构重新构造最优解。动态规划不像回溯法,有套路可以套用,动态规划需要大量练习,才能掌握规律。

一般思路:

1.判断问题的子结构,有最优子结构时,可用动态规划!!(即从子问题的最优解,就能拼接出整体的最优解!!)

2.求解重叠子问题。一个递归算法不断地调用同一问题(子问题大量重复,可以存储,减少计算),递归可以转化为查表从而利用子问题的解。(注意与分治法区别开,分治法每次递归都产生新的问题)

特殊思路:备忘录法

备忘录法是动态规划的一种变形,最开始随机填一个数字,遇到子问题时,计算并填表。

实例可参照矩阵链乘法。

常见题目:

1. 硬币找零:假设有1,3,5三种硬币,数量无限,问当总额为某个值时需要最少硬币数为多少。

2. 装配线调度:某个工厂有两条装配线,汽车底盘经过每个流程时都有相应的时间消耗,问怎么安排,可以让进入流水线到出厂时,总时间最少。

3. 字符串相似度/编辑距离(edit distance)

有两个不同的字符串,只对一个字符串进行“增删改”操作,每次变化只有一个字符,问变成另一个字符串时,最少操作几次?

4.最长公共子序列(Longest Common Subsequence,LCS)

对于序列S和T,求它们的最长公共子序列。例如X={A,B,C,B,D,A,B},Y={B,D,C,A,B,A}则它们的LCS是{B,C,B,A}和{B,D,A,B}。求出一个即可。

5.铺瓷砖问题(剑指offer,编程之美原题)

用 1 * 2 的瓷砖覆盖 n * m 的地板,问共有多少种覆盖方式?

答:可以转换为斐波那契数列题。f(n)=f(n-1)+f(n-2);

6.  0-1背包

题目背景如下:有一个背包,容量有限。还有好几样物品,体积和价值各不相同,如何尽量装最高价值的物品呢?用实际数据举例如下:

* 物品编号: 1 2 3 4
* 物品体积: 2 3 4 5
* 物品价值: 3 4 5 6

即前i件物品,容积为j时,最大价值为dp[i][j],用图表法计算如下:

有3种可能。

总容量连当前物品都放不下,那么当前物品肯定不放。前i个物品总体组合与前i-1一致。那么就把问题抛给前i-1了

总容量可以放下当前这一个物品。那么,给当前物品留有相应空间下,剩下的空间内,前i-1个物品总价值加当前的物品价值的组合。

总容量放得下当前物品,但还有一种可能性,我不放当前的,那么照顾当前的物品,还不如之前的i-1个物品呢。

实际举例如下:

第一种情况。如dp[3][3],当前物品体积是4,但是总容量只有3,根本放不下,那么dp[3][3],只能和dp[2][3]一致,都是4.

第二种情况,放得下。如dp[3][4],此时第3个体积为4,容量为4,总容量恰好能放下当前的。如果放当前的,那么留给之前的体积仅为0,一个都不放。第3个价值为5,所以总共价值为5。

如果不放该物品,那么dp[i][j]=dp[i-1][j]=4。它俩取最大值。当然是5.所以dp[3][4]=5.

总结:

注意:当前装得下时,指的是总容量装得下!!!要不装这个最大的,把问题抛给前面。要不不装大的,直接用前面的。这个思路和青蛙跳楼梯是一样的!!!!

参考文献:https://www.cnblogs.com/jiyongjia/p/13475026.html

import java.util.ArrayList;

/**
 * Date: 2020/11/11 11:36
 * Description:
 * <p>
 * 问题:现有背包。其中有四个商品。价值体积如下
 * 物品编号: 1    2    3    4
 * 物品体积: 2    3    4    5
 * 物品价值: 3    4    5    6
 * 问:如何才能保证在背包容量为8的情况下装的价值最大?
 * <p>
 * 步骤1:填表之后的结果:
 * 0	0	0	0	0	0	0	0	0
 * 0	0	3	3	3	3	3	3	3
 * 0	0	3	4	4	7	7	7	7
 * 0	0	3	4	5	7	8	9	9
 * 0	0	3	4	5	7	8	9	10
 * 步骤2:回溯
 *
 * @author zf
 */
public class beibao {
    public static void main(String[] args) {
        int[] size = new int[]{0, 2, 3, 4, 5};   //第0位放一个默认的0值,为了下边方便取size[i]就为i号物品的体积
        int[] money = {0, 3, 4, 5, 6};    //第0位放一个默认的0值,为了下边方便取money[i]就为i号物品的价值
        int target = 8;  //指定的背包大小
        //调用
        int[][] ints = dpWrite(size, money, target);
        ArrayList<Integer> huisu = huisu(ints, size, target);
        System.out.println("您输入的背包大小是:" + target);
        System.out.println("添加的物品编号是:" + huisu);
        System.out.println("最大价值:" + ints[size.length - 1][target]);

    }

    public static int[][] dpWrite(int[] size, int[] money, int targetValue) {
        //这里构造一个例如4*8的dp,行代表截止到i背包的最优组合的价值,j代表的背包的容量值
        int[][] dp = new int[size.length][targetValue + 1];
        //初始化第0行的值
        for (int j = 0; j <= targetValue; j++) {
            dp[0][j] = 0;
        }
        //初始化第一列的值
        for (int i = 1; i < size.length; i++) {
            dp[i][0] = 0;
            //dp[i][1] = 0;
        }
        //初始化中间值i
        for (int i = 1; i < size.length; i++) {
            for (int j = 1; j <= targetValue; j++) {
                //如果第i号背包的体积小于当前的j背包容量,就保持和dp[i-1]值相同,即没放入
                if (size[i] > j) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    /*即如果当前i号物品体积可以放入的话,就看预留该体积后剩余的价值在i-1号物品中的最大值加上该物品的价值和是否
                     是大于dp[i-1][j]的值,谁大取谁*/
                    dp[i][j] = Math.max(dp[i - 1][j - size[i] > 0 ? j - size[i] : 0] + money[i], dp[i - 1][j]);
                }
            }
        }
        //这样就填表完成了。剩下的就是需要回溯取出看对应某价值target的物品组合是什么
        //先打印一下填的表
        /*for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j < dp[i].length; j++) {
                System.out.print(dp[i][j]+"\t");
            }
            System.out.println();
        }*/
        return dp;
    }

    public static ArrayList<Integer> huisu(int[][] dp, int[] size, int t) {
        ArrayList<Integer> res = new ArrayList<>();
        int i = dp.length - 1;
        int j = t;
        while (i > 0 || j > 0) {
            if (dp[i][j] == dp[i - 1][j]) {
                i--;
            } else {
                res.add(i);
                j = j - size[i];
                i--;
            }
        }
        return res;
    }
}

 

 

7. 有代价的最短路径

青蛙跳台阶问题。(这个和硬币找零很像) 

8.M*N的矩阵最短路径问题

有一个M*N的矩阵,每个网格都有一个值,求起点grid[0][0]到grid[M-1][N-1]的值的最小累加和。

这个问题很简单,构造一个相同大小的矩阵dp[M-1][N-1],首先计算第一行和第一列的值,然后其余值都为dp[i][j]=grid[i][j]+Math.min(dp[i-1][j],dp[i][j-1]),最后返回dp[M-1][N-1]即可。

public static int minPathSum(int[][] grid) {
        if (grid == null || grid.length == 0 || grid[0].length == 0) {
            return 0;
        }
        int rows = grid.length,cols = grid[0].length;
        int[][] dp = new int[rows][cols];
        dp[0][0] = grid[0][0];
        //第一列的初始值
        for (int i = 1; i < rows; i++) {
            dp[i][0] = dp[i - 1][0] + grid[i][0];
        }
        //第一行的值
        for (int j = 1;j < cols;j++){
            dp[0][j] = grid[0][j] + dp[0][j-1];
        }
        //填写其他地方的值
        for (int i = 1;i<rows;i++){
            for (int j = 1;j<cols;j++){
                dp[i][j] = grid[i][j] + Math.min(dp[i-1][j] , dp[i][j-1]);
            }
        }
        return dp[rows -1][cols -1];
    }

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值