Dynamic Programming学习笔记 (1) - 简介

505 阅读4分钟

Dynamic Programming的基本概念

Dynamic Programming, 简称 DP,中文一般翻译为动态规划,是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法,在计算机中算法属于高级算法的一种。

DP 适用范围

DP很高级,都并不包治百病, 只有符合以下条件的特定问题才有其用武之处。

1)要求解的问题可以分拆成若干个且有限数量的子问题

2)要求解的问题与子问题之间,以及子问题与子问题之间存在层层递归的关系,也就是说在递归过程较高层的问题的解可以从位于较低层问题的解获得

3)一个子问题的解在整个计算过程中被反复多次使用

可应用DP的问题多为优化问题(最大,最小,最多,最少等等),有效使用DP算法可以大幅减少计算时间复杂度。

斐波那契数列(Fibonacci Sequence) 可以说是一个最简单但又充分说明DP解题思路的一个问题。

斐波那契数列通常用 F(n) 表示,该数列由 01 开始,
后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定一个正整数 n ,计算 F(n) 

以 n=5 为例,要计算 F(5),我们必须先得到F(4)和F(3), 而计算F(4)则需要F(3)和F(2),F(3)需要F(2)和F(1),F(2)需要F(1)和F(0)。整个求解过程可以用下图表示,我们不难看出这个问题完全符合上面提及的3个条件, 因此可以使用DP算法。

001.png

DP 基本方法

DP 解题方法可以归为两类,第一类称为"自上而下" (top-down), 另一类称为"自下而上" (bottom-up).

使用"自上而下"的方法,需要使用一个递归的方法来实现的子问题的求解,同时将每一个子问题的结果保存在一个缓存之中,当该子问题被再次递归调用时,我们直接从DP缓存中获取该子问题的结果,而无需再次计算,这种方法有一个专用术语,称为Memoization. 以下的代码给出了一个使用DP"自上而下"法来计算斐波那契数列的Java程序。

class TopDown {
    //DP缓存
    int[] dp;

    public int fib(int n) {
        //冿始化DP缓存
        dp = new int[n + 1];
        
        //递归调用
        return solve(n);
    }

    /*
    * 计算政波那契数的递归方法
    */
    private int solve(int n) {
        //F(0) = 0
        if (n == 0) {
            return 0;
        }

        //F(1) = 1
        if (n == 1) {
            return 1;
        }

        //检查DP缓存中是㿦已有该问题的结果
        if (dp[n] > 0) {
            //返回缓存中的结果
            return dp[n];
        }

        //递归调用并将结果使存到DP缓存中㿎返回
        return dp[n] = solve(n - 1) + solve(n - 2);
    }
}

使用"自下而上"的方法,我们不再使用递归方法,而是同最小的那个开始对各个子问题逐一求解,并将获得的结果保存在DP缓存中,直到最终的问题得到结果。这种方法又被称为Tabulation. 以下的代码给出了一个使用DP"自下而上"法来计算斐波那契数列的Java程序。

class BottomUp {
    public int fib(int n) {
        //F(0) = 0
        if (n == 0) {
            return 0;
        }

        //F(1) = 1
        if (n == 1) {
            return 1;
        }

        //初始化DP缓存
        int[] dp = new int[n + 1];

        //设置初始值
        dp[0]= 0;
        dp[1]= 1;

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

        //dp[n]给出了问题的最终解
        return dp[n];
    }
}

对于同一个问题,一般而言,这两类解法的计算复杂度是相当的,对刚接触DP的同学而言,基于递归的"自上而下"法相对容易理解,但要想用好DP的话,则必须掌握"自下而上"法的思维方式,不仅仅是由于省下了递归调用的系统开销,而且对某些特点问题而言,我们还可以进一步对算法进行优化以减少计算时间和存储的复杂度,如以下代码所示,我们只需使用三个变量做为DP缓存,通过对它们的依次更新就可以计算F(n)。

class SpaceOptimised {

    public int fib(int n) {
        //F(0) = 0
        if (n == 0) {
            return 0;
        }

        //F(1) = 1
        if (n == 1) {
            return 1;
        }

        //初始化x1为F(1)
        //初始化x2为F(0)
        int x1 = 0, x2 = 1;

        //初始化x为F(2)
        int x = x1 + x2;
        for (int i=3; i<=n; i ++) {
            //依次更新x1,x2和x 
            x1 = x2;
            x2 = x;
            
            //F(i) = F(i - 1) + F(i - 2)
            x = x1 + x2;
        }

        //x的值就是F(n)
        return x;
    }
}