动态规划思想

1. 动态规划与分治

动态规划(Dynamic Programming, DP)方法常常被用来寻找最优解。类似于分治策略,将原问题分解为子问题,然后对子问题进行求解,再子问题的解综合得到原问题的解。但是用递归的算法实现分治策略时,往往和大量重复求解子问题,导致了指数级的时间复杂度。有两种方法可以避免重复求解子问题,一是在递归求解的过程中,将子问题的解记录下来,后续遇到相同子问题时,在常数时间内取出该子问题的解即可;二是采用自底而上的求解,先求解最底层的子问题,逐步向上最后综合得到原问题的解答。两种方法都是以空间换时间(time memory trade-off)

斐波那契数列问题

f ( n ) = f ( n − 1 ) + f ( n − 2 ) f ( 0 ) = 1 ,   f ( 1 ) = 1 \begin{matrix} f(n)=f(n-1)+f(n-2) \\ f(0)= 1,~f(1)=1 \\ \end{matrix} f(n)=f(n1)+f(n2)f(0)=1, f(1)=1

2. 递归求解

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

如图中所示阴影部分节点实质是被重复计算了,如果n越大,那么重复计算的节点将更多,这也就是n较大时,程序耗时长的重要原因。算法的时间复杂度是指数级的 O ( c n ) O(c^{n}) O(cn).

3. 带记忆的递归

如果能在递归的过程中,将那些业已计算过的节点值记录下来,后面遇到时在 O ( 1 ) O(1) O(1)的时间内进行调用。这样虽然牺牲了空间,但是能将时间复杂度大为降低。

int fibonacci(int n, std::unordered_map<int, int>& record) {
    if (record.find(n) != record.empty()) return record[n];
    if (n == 0 || n == 1) {
        record[n] = 1;
        return 1;
    }
    int ans = fibonacci(n-1, record) + fibonacco(n-2, record);
    record[n] = ans;
    return ans;
}

上面程序中利用哈希表记录下了已经计算得到的fibonacci数列的结果,后面遇到时能在 O ( 1 ) O(1) O(1)的时间内得到该结果,实际的时间复杂度为 O ( n ) O(n) O(n).

4. 动态规划

递归的方法求解实质上一种自顶而下的思考方式,计算f(n)需要计算f(n-1)和f(n-2),是一种直接的思考方法。而动态规划则是自下而上的思考方式,先计算f(0),f(1)进而得到f(2),再由f(1)和f(2)得到f(3)最后得到f(n)。

如果运用动态规划解决问题,那么必须具备两个特征。

动态规划两个特征:

  • 最优子结构:所有子结构的最优解重合得到原问题的解一定是原问题的最优解
  • 重复子问题:需要求解的子问题中存在大量的重复,动态规划记录了子问题的解,避免重复求解子问题

最优子结构是保证了对子问题求解之后再由子问题导出原问题的解是原问题的最优解,保证了算法正确性。而重复子问题是动态规划算法比递归方法要快的本质原因,如果不存在重复子问题,那么动态规划在效率上则没有优势。

int fibonacci(int n) {
    assert(n >= 0);
    if (n == 0 || n == 1) return 1;
    std::vector<int> f(n+1, 0);
    f[0] = 1, f[1] = 1;
    int i = 2;
    while (i < n + 1) {
        f[i] = f[i-1] + f[i-2];
        i++;
    }
    return f.back();
}

上面的程序用一个一维数组记录了所有fibonacci数列的结果,从0一直计算到n,没有重复计算,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n),是一种动态规划的做法。实质上,dynamic programming中的programming的意思不应该是程序的意思,应该是表格法,就是用表格把结果动态地记录下来。

实际上不需要保持所有的计算结果,仅保存当前计算的前两步的计算结果即可,如下面程序中用a保存(i-2),用b保存f(i-1),这样的空间复杂度降为 O ( 1 ) O(1) O(1)

int fibonacci(int n) {
    assert(n >= 0);
    int a = 1, b = 1;
    while (n--) {
        b = a + b;
        a = b - a;
    }
    return a;
}

判断是否用动态规划解决问题,考察该问题是否具有最优子结构重复子问题的两个特征即可。

使用动态规划方法解决问题时,思考问题的方式应当是自上而下地递归方法,尝试写出问题的递归表达式,然后自下而上地去解决问题、代码实现。最后,可以尝试能不能降低空间复杂度。

你可能感兴趣的:(算法,动态规划,递归,带记忆递归,Fibonacci)