一道算法题看懂动态规划

动态规划简介

动态规划(Dynamic Programming)即 DP 算法,把多个阶段过程转化为一系列阶段问题,并利用各阶段之间的关系,逐个求解

一个楼梯有 n 层,你从楼底开始上到第 n 层,每次只能上 1 层或者上 2 层,你一共有多少种可能到达 n 层?

递归算法解法

我们可以这么想,一层楼有 n 层,走到第 n 层的前面一层要么是 n-1 层,要么是 n-2 层,因为我只能跨 1 层或者跨 2 层上到第 n 层,然后 n-1 层和 n-2 层又满足这种思想,这不就是递归的思想吗?!最后出口是 1 层的一种可能和 2 层的两种可能。这就写代码

// n 层阶梯,每次上 1 层或者 2 层,共多少种上法(递归实现)
public int upstairsRecursion(int n) {
    if (n == 1)
        return 1;
    if (n == 2)
        return 2;
    return upstairsRecursion(n - 1) + upstairsRecursion(n - 2);
}

我们掐指一算,这种结构不就是很类似二叉树的结构吗,每次递归,都要 n-1 一次,这样来看,深度不就是 n 吗,那就简单了,时间复杂度就是 O(2^n),但是效率好低呀!

备忘录算法解法

我们仔细斟酌代码,会发现一个不能忽视的点,那就是return upstairs(n - 1) + upstairs(n - 2);这个代码,我们执行到这里肯定是要深处递归的,当然先从upstairs(n - 1)这里往深处递归,一直递归到最深处,在往上层层返回一直到最上层,注意往上层进行层层返回的过程中还要对upstairs(n - 2)进行递归才行,这种思想很想二叉树的后序遍历是不是,确实如此

看下图,我们求 F(N-1) + F(N-2) 的值,我们必须递归到最深处拿到其左结点的值,如下图假如是 F(N-3) 的值,然后 F(N-4) 的值我们也拿到,这样我们就有 F(N-2) 的值,再然后我们如果要计算 F(N-2) + F(N-3) 的结果,又需要对 F(N-3) 进行递归,然后我们拿到 F(N-4) 和 F(N-5) 的值,再然后我们拿到 F(N-3) 的值,这样来看我们遍历结点的顺序是:F(N-3) => F(N-4) => F(N-2) => F(N-4) => F(N-5) => F(N-3) => F(N-1)……这不就是二叉树的后续遍历嘛

算法-动态规划DP_算法

谈到后序遍历这棵二叉树,我们又会发现根结点为 F(N) 的右子树就是 F(N) 左子树的子树嘛,而且结构和数据完全一致(上图中没有写全,写全的话左边要比右边深一层),那我要是通过后续遍历把左子树率先全都遍历完了,我还遍历右子树干嘛,这不浪费哥时间吗?所以怎么办呢?那我在遍历左边的时候,从最底层我每知道当前层的种类数,我就通过键值对放到 map 中吧,之后遇到同样层的就直接取 map,不用往深的遍历了

// n 层阶梯,每次上 1 层或者 2 层,共多少种上法(备忘录实现)
public int upstairsMemo(int n, HashMap<Interger, Interger> map) {
    if (n == 1) {
        map.put(1, 1);
        return 1;
    }
    if (n == 2) {
        map.put(2, 2);
        return 2;
    }
    if (map.contains(n))
        return map.get(n);
    else {
        int value = upstairsMemo(n - 1, map) + upstairsMemo(n - 2, map);
        map.put(n, value);
        return value;
    }
}

搞定了!这种方法应该很快吧!是的,我们算了一下,int value = upstairsMemo(n - 1) + upstairsMemo(n - 2);这个代码总是最左边值得计算,右边不用再往深处递归可以直接拿到值,因为右边总是左边的子集嘛,这个很好理解吧,再不行我们再画个二叉树的图或者直接看上面的图,这个递归的二叉树的最左边一串下来的结点,包含了整棵二叉树所有的信息,这样该能理解了吧!所以我们没必要算每个结点,我们找到最左边的所有结点即可(实际上面那个代码中右边也有计算一次,只不过右边不用递归可以直接通过 map 取值罢了),所以时间复杂度应该是 O(2n),大约就是 O(n),空间复杂度就是 map 存储了 n 层楼的键值对罢了,也是 O(n)

动态规划解法

我们仅仅是满足于此吗?好像不能再化简了吧!我想破了脑袋也没想出更好的思路了,这时候我想换个路子想一下,我要是不用递归呢?递归大家都知道,是一种可读性强但是一般效率不高的算法,我要是不用递归我用迭代试试呢?我们来看看效果:

层数 1 2 3 4
种类数 1 2 1+2=3 2+3=5

第一层肯定是 1 种,第二层肯定是 2 种,第三层就是前两层种数之后,第四层也是如此,这样乍一看我是不是空间复杂度可以不用是 O(n) 了呢?因为我要保存前两层两个变量即可啊!我很兴奋,现在来实现一下吧!

public int upstairsDP(int n) {
    // 第一层走法
    int a = 1;
    // 第二层走法
    int b = 2;
    // 第几层
    int i = 3;
    // 临时变量
    int temp;
    while(i <= n){
        temp = a + b;
        a = b;
        b = temp;
        i++;
    }
    return temp;
}

时间复杂度老夫掐指一算 O(n),空间复杂度确实是 O(1),很 nice,有木有!利用了层层向上的递推的方式完成了时间空间的最优算法