动态规划就是1+1

动态规划就是1+1

1 - 什么是动态规划?

    首先引用一下Wikipedia的词条,我们来认识一下动态规划。

Dynamic programming is both a mathematical optimization method and a computer programming method. The method was developed by Richard Bellman in the 1950s and has found applications in numerous fields, from aerospace engineering to economics.

In both contexts it refers to simplifying a complicated problem by breaking it down into simpler sub-problems in a recursive manner. While some decision problems cannot be taken apart this way, decisions that span several points in time do often break apart recursively. Likewise, in computer science, if a problem can be solved optimally by breaking it into sub-problems and then recursively finding the optimal solutions to the sub-problems, then it is said to have optimal substructure.

If sub-problems can be nested recursively inside larger problems, so that dynamic programming methods are applicable, then there is a relation between the value of the larger problem and the values of the sub-problems.[1] In the optimization literature this relationship is called the Bellman equation.

    其中最重要的一句话是"it refers to simplifying a complicated problem by breaking it down into simpler sub-problems in a recursive manner."。意思是“动态规划使用递归的方式将一个复杂的问题分解为更简单的子问题解决问题。”。乍一听,感觉动态规划比较晦涩,像是个深奥的数学问题。其实,我们每天都在接触动态规划。给大家举个栗子,我们在计算1 + 1的时候,脑子条件反射般的算出答案,没错,等于2。但是当我们算1 + 1 + 1 + 1的时候,大家就会算得1 + 1 = 2,然后2 + 1 = 3, 最后3 + 1 = 4,这也正是是乘法的本质。动态规划的原理也不过如此,将复杂的问题分解为简单的子问题。

    我们通过这个例子,不难发现其中几个需要注意的点。第一,做这个"1+1"难题的时候,最重要的是什么?是我们知道后一项等于前一项加一,所以我们发现,动态规划不仅仅是将大问题化简为小问题,在这其中有一个必要条件,那就是大问题和子问题之间的关系,我们把它叫做“状态转移“。在此例中,状态转移的规律非常明显。a[n] = a[n-1] + 1我们把这种状态转移的公式化描述叫做状态转移方程。第二个需要注意的点也很容易找到,我们总是用前一项推导后一项的值,那么最前面的一项用什么来推导呢?答案是无法推到。那么我们指定一个初始值,方能使用状态转移方程推导复杂问题,我们管这个初始值叫做初始状态。这有点类似于虹吸原理。可能大家都有过这样的经历,水缸里有一缸水,我们要将其排出,那最简单的办法是什么呢?让其利用虹吸原理自动流出来,那最核心的步骤就是,我们首先需要在管子的一头用嘴巴吸出第一口水,给其一个初始动能。这就是初始状态的意义。

2 - 斐波那契数列和动态规划的渊源

    众所周知,斐波那契的定义是a[n] = a[n-1] + a[n-2],我们能很明显的发现其中的一项是由前两项的和组成的。那么这不久刚好匹配了动态规划的定义吗?我们来尝试使用动态规划的思想求解斐波那契数列,感受一下动态规划为什么高效。

public static int fib(int n) {
        // 边界检查
        if (n <= 0) throw new IllegalArgumentException("n 必须大于0");
        if (n <= 2) return n;

        // 确定状态 (开一个数组,明确数组的索引和值之间的对应关系  例如: 第i个元素表示fib(i))
        int[] dp = new int[n];

        // 初始状态
        dp[0] = 1;
        dp[1] = 1;

        // 状态转移
        for (int i = 2; i < n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n - 1];
    }

    在我们实践中发现,还有两个步骤是必不可少的,边界检查和确定状态,确定不了状态无法用程序表示出状态转移方程,没有边界检查,我们就无法保证程序正确运行。所以这四个步骤,就是动态规划的全部。用班主任的口吻来讲,动态规划是万变不离其宗!

3 - 实践出真知

    我们通过几个例子,来巩固一下动态规划的解题思路。

70. 爬楼梯

    印象非常深刻,这是美团曾经的一道笔试题,属于非常简单的入门题了。跟斐波那契数列一个接替思路,唯一要注意的是初始状态的界定。

    public int climbStairs(int n) {
        // 边界检查
        if(n <= 0) return 0;
        if(n > 0 && n <= 2) return n;

        // 确定状态
        int[] dp = new int[n];

        // 初始状态
        dp[0] = 1;
        dp[1] = 2; // FBI WARNING !!!

        for(int i = 2; i < n; i++){
            dp[i] = dp[i-1] + dp[i-2];
        }

        return dp[n - 1];
    }

300. 最长上升子序列

    这道题的难点在于如何找到状态转移方程,这也是绝大多数动态规划题目的难点,我们对此进行分析。首先要确定状态,我们的dp数组究竟表示什么?一般来讲,题目的结果是什么,dp数组的值就是什么,然后再去找索引对应的含义,这是个阅读理解题……。”dp[i]表示以nums[i]结尾的LIS的长度“,这就是我们这道题中状态的含义,我们待会以此推出状态转移方程。

public int longestCommonSubsequence(String text1, String text2) {

        int m = text1.length(), n = text2.length();
        // dp[i][j] 表示: 字符串 str1[0:i] 和字符串 str2[0:j] 的最大公共子序列
        int[][] dp = new int[m+1][n+1];
        // 进行状态转移
        for(int i = 1; i <= m; i++){         
            for(int j = 1; j <= n; j++){
                if(text1.charAt(i-1) == text2.charAt(j-1)){ // 若两个字符相等,必然可以构成子问题的最优解
                    // 这个字符存在于 lcs 之中
                    dp[i][j] = dp[i-1][j-1] + 1; 
                }else{
                    dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);  
                }
            }
        }
        return dp[m][n];
    }

动态规划的题目还有很多,例如求最值,博弈等,最大的难点就是找状态转移方程,只有多刷题才能够完全掌握,学得基本原理之后,接下来就是联系啦!

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