轻松明白-DP(动态规划)入门

一、动态规划解决什么问题

将复杂问题分解为子问题,子问题会多次重复出现,存储子问题结果以避免重复计算的优化方法。

二、从斐波那契额数列开始

斐波那契数列是指这样一个数列:0,1,1,2,3,5,8,13,21,34,55,89……这个数列从第3项开始 ,每一项都等于前两项之和。

可以记作:

F(0) = 0

F(1) = 1

F(n) = F(n-1) + F(n-2)(n ≥ 2)

需求:写一个函数 Fib(n) 返回第 n 个斐波那契数。

1.1 暴力计算

比如算F(5)需要计算F(4...0)。

算F(4)的时候计算F(3...0)。

算F(3)的时候计算F(2...0)。

算F(2)的时候需要计算F(1)+F(0).

其中有大量的过程被重复了,比如算F(5)的所有过程中F(3)就同时被F(5)和F(4)需求了。

下面是暴力算法,完全按照斐波那契的计算公式来写:

using System;

class Program
{
    static int FibNaive(int n)
    {
        if (n == 0) return 0;
        if (n == 1) return 1;

        // 完全照公式写:F(n) = F(n-1) + F(n-2)
        return FibNaive(n - 1) + FibNaive(n - 2);
    }

    static void Main()
    {
        Console.WriteLine(FibNaive(10));
    }
}

1.2 优化

我们会发现Fib(0)、Fib(1)...Fib(n-1)每一个结果都会被多次的使用,如果我们将Fib(x)的计算结果存储在一个数据结构中,如果有就从这个数据结构中拿,就会减少调用Fib。比如:

using System;

class Program
{
    static int[] memo;

    static int FibMemo(int n)
    {
        if (n == 0) return 0;
        if (n == 1) return 1;

        if (memo[n] != -1)  // 之前算过了,直接用
            return memo[n];

        memo[n] = FibMemo(n - 1) + FibMemo(n - 2);
        return memo[n];
    }

    static void Main()
    {
        int n = 40;
        memo = new int[n + 1];

        // 用 -1 表示“还没计算过”
        for (int i = 0; i <= n; i++)
            memo[i] = -1;

        Console.WriteLine(FibMemo(n));
    }
}

我们从参数N开始递归一直递归到N=2,称之为从上到下。过程中我们使用了一个int[] memo数组来存储Fib(x)的结果,如果再次遇到就直接从数组拿。即memo[i] 表示 F(i) 的值。

当然我们也可以从下到上,即从下标2开始计算到N:

using System;

class Program
{
    static long FibDP(int n)
    {
        if (n == 0) return 0;
        if (n == 1) return 1;

        long[] dp = new long[n + 1];
        dp[0] = 0;
        dp[1] = 1;

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

        return dp[n];
    }

    static void Main()
    {
        int n = 50;
        Console.WriteLine(FibDP(n));
    }
}

我们发现,当long[] dp填满的时候,dp[n]就是结果。

1.3 思想总结

DP能解决的问题都有这些基本特征

  1. 大问题可以拆成小问题(最优子结构

  2. 小问题会反复出现(子问题重叠

  3. 无后效性,或称为【有序无环图】。

如果我们每次都“重新算一遍小问题”,就会很浪费(典型:斐波那契数)。
DP 的核心就是:小问题的答案算一次就存起来,之后直接用。

重点说一下无后效性,这个词很抽象,有序无环图更好理解:

通过箭头我们就可以直观的感受到【有序】。

如果有环呢?比如下面:

F(2) = F(1)+F(0)+F(5), 那么我们发现F(2)、F(3)、F(4)、F(5)成环了,问题将无法求解,所以需要无环

无后效性是动态规划能成立的基础。如果一个问题存在环路(如动态依赖关系),那就无法使用动态规划来优化,因为子问题的解无法按顺序进行计算

三、再抽象一点

三个词:状态、状态转移、边界。

3.1 边界

边界最好理解,比如斐波那契数列中,F(0)和F(1)就是边界。

3.2 状态

比如斐波那契数列中每个节点都是一个状态。

状态是子问题的集合,就像斐波那契数列中F(5)此时的状态就是子问题F(4...0)子问题的集合。

3.3 状态转移

状态转移也好理解,比如计算F(5) = F(4) + F(3)。F(5)的状态来自于F(4)状态结果和F(3)状态结果的和,这个过程就是状态转移。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值