动态规划(Dynamic Programming)(壹)

目录

简介

案例分析

斐波那契数列

 分割字符串

三角矩阵


简介

动态规划是分治思想的延伸,通常来说就是大事化小,小事化了的艺术。在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接
使用这些结果。说人话就是利用历史记录来避免我们重复计算。

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


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


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


3. 储存子问题的解。
 

动态规划的本质,是对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)
动态规划问题一般从以下四个角度考虑:


1. 状态定义

因为要利用历史记录来计算,我们就需要使用一个容器来保存,大部分我们使用的是数组,因为数组方便嘛,也有一些使用二维数组。因此我们需要知道每个数组元素代表的含义,这个数组名就命为dp[]吧,也就是说要定义dp[i]的意义。


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

状态转移方程,我的理解是有点类似于高中时学过的归纳法(再次回到被数学支配的恐惧),当我们要求dp[n] 时,是可以利用 dp[n-1]、dp[n-2]……dp[1]来求出dp[n]的,也就是说他们必然满足一种关系,比如:dp[n] = dp[n-1] + dp[n-2].这一步可以说是最难得一步,但也是最关键的一步。


3. 状态的初始化

学过数学归纳法的都应该知道,我们能利用关系求出第n个值,但总得有个初值,不然你就算知道了dp[n] = dp[n-1] + dp[n-2],要求d[n]能求出来吗?因此这一步我们需要定义初值,也就是dp[1] = ? 或者 dp[0] = ?


4. 返回结果

要求的的是第n个,直接返回数组中dp[n]即可(假设从1开始)。
 

 适用场景:最大值/最小值, 可不可行, 是不是,方案个数

案例分析

斐波那契数列

举个简单的例子,都知道斐波那契数列吧,斐波那契数列每一项都是前两项的和,利用这个特点我们就能使用递归,例如下面这种:

 但是有个问题,那就是我们每次计算第n个时,都要把原来的重新计算一遍,这个时间复杂度达到了O(N^2),效率太低了,而且当n达到一定值时还会造成栈溢出。

高级一点的可能会使用迭代:

定义一个first = 1,second = 1,third = 0,然后循环遍历从3开始到n结束,把first + second的值赋给third,second的值赋给first,trird的值再赋给second,就这样一直循环。

public int Fibonacci(int n) {
        int first = 1;
        int second = 1;
        int third = 1;
        for(int i = 3 ; i <= n ; i++){
            third = first + second;
            first = second;
            second = third;
        }
        return third;
    }

 但其实这个迭代的过程其实就相当于动态规划:

一:状态定义

创建一个数组dp,每个dp[i]的意义就是第i个斐波那契数列的值

二:状态间的转移方程

由题目就可以得出,第n个的值为前两项的和,也就是说 dp[n] = dp[n-1] + dp[n-2].

三:状态的初始化

也可以由已知的出,dp[1] = 1,dp[2] = 1

四:返回结果

返回dp[n]

迭代只是每次循环都把值更新了,而动态规划是把每个数据都保存了,本质是类似的。

public int Fibonacci(int n) {
        int dp[] = new int[n+1];
        dp[1] = 1;
        dp[2] = 1;
        for(int i = 3 ; i <= n ; i++) {
            dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];
    }

在线OJ链接斐波那契数列

 分割字符串

先看题目:

这个题目我们第一眼看去最简单想到的就是哈希表存储string,boolean,判断并返回,这种方法虽然可行,但不太好,如果字符串很长,哈希表就要存储许多数据。因此我们使用动态规划:

一:状态定义

定义数组dp[],一个字符串能否被分割也就意味着整个数组是否能被分割,所以,我们可以将dp[i]定义为前 i个元素能否被分割,能就将其置为true,否则置为false。

二:状态间的转移方程

因为字符串可能不止被分割为两个,因此我们在求dp[i]时,要判断前面是否已经有单词在词典中找到,如果前面已经有找到的将其下标元素置为true,于是我们可以设置求dp[ i ]的进入条件为前面的dp[ j ] == true ,而如果 contains( substring( j , i ) ),则可将其设置为true

因此通过归纳法,我们可以得出要求dp [n] 需要判断两层条件,一层是通过保存的历史记录得出,而另一层则是需要求的时候顺带判断。即dp[ i ] = dp[ j ]  && contains(substring( j , i )) 而且这里有个小细节,当字符串是"nowcodewe",词典不变时,当下标指向 w 时,dp[3] 结束 没有找到 "codew"  不应该结束而是应该调到dp[7]并继续判断"w",所以这里还需要设计一层循环。

三:状态的初始化

因为数组下标包含0,而前0个字符就是"",我们认为词典是包含的,否则循环进不去,因此状态的初始化有时还是需要根据代码和思路一起得出,否则容易出问题

四:返回结果

返回dp[s.length()]

public boolean wordBreak(String s, Set<String> dict) {
        boolean dp[] = new boolean[s.length()+1];
        dp[0] = true;
        //dp[i] 代表前i个字符是否能被分割,因此i要可以等于字符串的长度
        for(int i = 1 ; i <= s.length() ; i++ ){
            //进入条件首先是 j < i,不能等于,等于意味着你已经知道了dp[i]的结果
            for(int j = 0 ; j < i ; j++){
                //要求的是dp[j] && contains([j+1,i])
                //因为substring函数是前闭后开[j,i),所以第 j 个下标对应的是 j+1 , i-1下标对应 i  
                if(dp[j] && dict.contains(s.substring(j,i))){
                    dp[i] = true;
                    //只要满足即为true,不关心它是怎么分割的
                    break;
                }
            }
        }
        return dp[s.length()];
}

 在线OJ链接:拆分字符串

三角矩阵

例图如下:

由题目可知,每一步只能移到下面一行相邻的数字,就是说50只能往下走10或者80,而不能到40或30,并且最左和最右只有一条路径可以走:

话不多说,直接上动态规划:

一:状态定义

函数给的是一个集合嵌套集合,因此我们也可以构建一个二维数组dp[][],dp[ i ][ j ]代表到达第 i 行 第 j 列的最小路径和。

但是如果行数很多的话有点浪费空间了,因此其实这道题并不推荐重新构建一个二维数组,而是用它所给的那种集合套集合的形式,但写这道题主要是为了熟悉动态规划所以还是按步骤来吧。

二:状态间的转移方程

由题目我们可以推断出到达dp[ i ] [ j ] 只有从 dp[ i - 1 ][ j - 1 ] 和 dp[ i - 1 ][ j ]

因为要求最小,所以要返回两个点中数值小的并且加上自己本身的数值。

即dp[ n ]= min(dp [ i - 1 ][ j - 1 ] ,dp[i - 1] [ j ]) + array[ i ] [ j ]

三:状态的初始化

题目给出的集合中的第一个元素就是dp[ 0 ][ 0 ].

四:返回结果

返回 最后一行中最小的元素

dp数组代码:

public int minimumTotal(ArrayList<ArrayList<Integer>> triangle) {
        int size = triangle.size(); //定义行
        if(triangle.isEmpty() || size == 0){
            return 0;
        }
        int [][] dp = new int[size][size];
        dp[0][0] = triangle.get(0).get(0);
        for(int i = 1 ; i < size ; i++){
            int sum = 0;
            for(int j = 0 ; j <= i ; j++){
                //累计往下加
                if(j == 0){
                    //最左边一行的情况
                    sum = dp[i-1][j];
                }else if(j == i){
                    //最右边一行的情况
                    sum = dp[i-1][j-1];
                }else{
                    //非边界,即0<j<i
                    sum = Math.min(dp[i-1][j],dp[i-1][j-1]);
                }
                dp[i][j] = triangle.get(i).get(j) + sum;
            }
        }
        //到这一步已经累计到最后一行了,最后一行所有元素中找最小的即可
        int minsum = dp[size-1][0];
        for(int j = 1 ; j < size ; j++){
           minsum = Math.min(minsum,dp[size-1][j]);
        }
         
        return minsum;
}

 ArrayList代码:

public int minimumTotal(ArrayList<ArrayList<Integer>> triangle) {
        if (triangle.isEmpty())
            return 0;
        ArrayList<ArrayList<Integer>> minPathSum = new ArrayList<>();
        //每一行重新new一个集合
        for (int i = 0; i < triangle.size(); ++i) {
            minPathSum.add(new ArrayList<>());
        }
        minPathSum.get(0).add(triangle.get(0).get(0));
        for (int i = 1; i < triangle.size(); ++i) {
            int curSum = 0;
            for (int j = 0; j <= i; ++j) {
                // 处理左边界和右边界
                if (j == 0) {
                    curSum = minPathSum.get(i - 1).get(0);
                } else if (j == i) {
                    curSum = minPathSum.get(i - 1).get(j - 1);
                } else {
                    curSum = Math.min(minPathSum.get(i - 1).get(j),
                            minPathSum.get(i - 1).get(j - 1));
                }
                minPathSum.get(i).add(triangle.get(i).get(j) + curSum);
            }
        }
        int size = triangle.size();
        int min = minPathSum.get(size-1).get(0);
        for(int i = 1 ; i < size ; i++){
            min = Math.min(min,minPathSum.get(size-1).get(i));
        }
        return min;
}

其实如果这道题我们也可以逆向思维,从底向上看,那么代码其实比从顶向下看代码更简洁,因为自顶向下要判断边界的条件,而自底向上每个点都有两条到达路径,适应所有点,不用判断边界。

代码:

public int minimumTotal(ArrayList<ArrayList<Integer>> triangle) {
        if (triangle.isEmpty())
            return 0;
        ArrayList<ArrayList<Integer>> minPathSum = new ArrayList<>(triangle);
        int size = minPathSum.size();
        for(int  i = size - 2 ; i >= 0 ; i--){
            for(int j = 0 ; j <= i ; j++){
                int curSum = Math.min(triangle.get(i + 1).get(j),triangle.get(i + 1).get(j + 1))
                             + triangle.get(i).get(j);
                minPathSum.get(i).set(j, curSum);
            }
        }
        return minPathSum.get(0).get(0);
}

在线OJ链接: 三角矩阵

 本文收录专栏《数据结构与算法

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海绵宝宝养的的小窝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值