动态规划从入门到入土——入门篇

一、什么是动态规划?

要先理解动态规划的话,我们要先知道什么叫“分治”思想。
这就要提一下分治的祖师爷了——大禹
当面对波涛汹涌的洪水,人力显得多么的苍白无力,然鹅,大佬站出来了,提出:我们把洪水不断的分流,然后对每一条支流进行处理就可以处理好洪水了。

那么分治思想就是:大事化小,小事化无
与此同时,动态规划算是分治思想的延伸,它主要是把原问题分治为各个大问题,把大问题分解成各个小问题,然后保存各个小问题的解用于处理上层大问题的解。

那么动态规划的特点也就出现了:

  1. 把原来的问题分解成几个相似的子问题
  2. 所有子问题都只解决一次
  3. 存储子问题的解

二、如何使用动态规划

我们先要认知动态规划的本质:
是对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)

我们一般从四个角度来考虑问题:

  1. 状态的定义
  2. 状态间的转移方程定义
  3. 状态初始化
  4. 返回结果

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

三、具体案例

不谈案例的算法都是耍流氓   		 --沃兹基硕德

下面从简单到难的案例,来学会动态规划。

1.Fibonacci

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为 0)。 n<=39 int
Fibonacci(int n)

 public int Fibonacci(int n) {
        if(n == 0){
            return 0;
        }
        if(n == 1 || n == 2){
            return 1;
        }
        return Fibonacci(n-1)+Fibonacci(n-2);
    }

这是递归版本的斐波那契数列,我们都知道它的缺陷,太费栈空间,重复运算太多次,那么我们用动态规划试一下。

1.状态的定义
这里很明显,我们求F(n)的值,F(n)为第n项的值
2.转移方程
就是通项公式 F(N) = F(N-1)+F(N-2);
3.初始化
F(0)=0
F(1) = 1;
4.返回结果
F(n)

到此,对于这道题已经完成了85%以上了,代码只占15%,那么我们写一下代码吧。

		if(n == 0){
            return 0;
        }
        if(n == 1 || n == 2){
            return 1;
        }
        int F1 = 1;
        int F2 = 1;
        int result = 0;
        for (int i=3; i<=n; i++){
            result = F1 + F2;
            F1 = F2;
            F2 = result;
        }
        return F2;

这道题只是初步感受一下,下面我们感受第二题。

2.变态青蛙跳台阶(Climbing Stairs)

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

我们来分析一下。

1.状态的定义
这和上一题差不多,我们定义F(n),表示到n阶台阶的走法。

2.转移方程
这里就需要思考了,我们到N阶台阶的话,那么我们前一步应该在哪?
由于这个青蛙比较牛批,它可以任意跳,也就是说,它在n-1个台阶里面的任何一个台阶都可以一步跳到最后一格,那么,也就是说,F(N)的值,应该是它前面每一个台阶值的累计和,换而言之,就是到第1个台阶的路径个数+到第二个台阶的路径个数+…+n-1个台阶路径个数。

F(N) = F(N-1) + F(N-2) + F(N-3) + … + F(0);

那么这个东西还是不好整啊,这是什么鬼?我们不妨再推一下,

F(N-1) = F(N-2)+F(N-3)+…F(0);

用这个算式替代一下第一个算式的部分,那么就有

状态方程: F(N) = 2*F(N-1);

3.状态初始化:
F(0) = 0; // 辅助状态,表明没有台阶
F(1) = 1; // 一个台阶的话,只有一种走法

4.返回结果
F(N)

至此,这道题结束了,上代码。

public int JumpFloorII(int target) {
        if(target <= 0){
            return 0;
        }
        if(target == 1){
            return 1;
        }
        int F1 = 1;
        for (int i=2;i<=target;i++){
            F1 *= 2;
        }
        return F1;
    }

是不是很简单了,下面上老生常谈的问题。

3.最大连续子数组和(Maximum Subarray)

HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?
例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

这道题就是在求最大连续子数组和。继续分析

1.状态的定义
我们先定义一下:F(i),表明第i个元素连续最大子数组和。
这样开始分析,我们要找到第i个的最大连续子数组和,那么我们势必要找i-1,但是一直递推下去,我们根本不知道前面的元素该不该用。那么这样定义肯定是不行的。我们转念一想,如果这样定义呢:F(i)表示,以第i个元素元素结尾的最大连续子数组和。因为我们知道,当前的元素值,那么根据前面的最大连续和来判断这个当前该不该加。不该加就是从当前开始

2.转移方程
F(i) = Max(A[i],F(i-1)+A[i]);
3.初始化
建立一个矩阵用于保存到i的最大值
F[0] = Array[0];
4.结果
返回这个矩阵里面最大的那个值。

public int FindGreatestSumOfSubArray(int[] array) {
       int[] dp = new int[array.length];
        dp[0] = array[0];
        int result = array[0];
        for (int i=1; i<array.length; i++){
            dp[i] = Math.max(array[i], dp[i - 1] + array[i]);
            if(dp[i] > result){
                result = dp[i];
            }
        }
        return result;
    }

这样还是浪费空间,可以改进一下,如果题对原数组不做要求,直接改原数组就可以了

public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
         int result = array[0];
         for (int i=1; i<array.length; i++){
            array[i] = Math.max(array[i], array[i - 1] + array[i]);
            if(array[i] > result){
                result = array[i];
            }
        }
        return result;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章