斐波那契和动态规划

在基础算法领域,动态规划算是比较难得一类算法了,而且在算法竞赛或者面试中会有大量动态规划的题目,因此下功夫理解这个算法还是很有必要的。

斐波那契

斐波那契数组算是一个比较常见的讨论算法的入门题,在leetcode中也包含这个题目

The Fibonacci numbers, commonly denoted F(n) form a sequence, called the Fibonacci sequence, such that each number is the sum of the two preceding ones, starting from 0 and 1. That is,
F(N)=\begin{cases} 0 & N=0 \\ 1 & N=1 \\ F(N-1) + F(N-2) & N > 1 \end{cases}
Given N, calculate F(N).
Example 1:

Input: 2
Output: 1
Explanation: F(2) = F(1) + F(0) = 1 + 0 = 1.

Example 2:

Input: 3
Output: 2
Explanation: F(3) = F(2) + F(1) = 1 + 1 = 2.

Example 3:

Input: 4
Output: 3
Explanation: F(4) = F(3) + F(2) = 2 + 1 = 3.

Note:0 ≤ N ≤ 30.

递归

显而易见的解法就是递归算法,不做解释,直接上代码:

func fib(N int) int {
  if N <= 1 {
    return N
  }
  return fib(N - 1) + fib(N - 2)
}

自上而下的优化解法

在递归算法中可以看到计算fib存在大量的重复计算,这些计算会占用很多时间。见下图:


15718946172954.jpg

图中对于F(2)F(3)存在多次重复计算,我们是否可以通过缓存的方式减少重复计算呢?显然是可以的。实现也比较简单,只需初始化一个数组记录计算过的数据即可。
代码如下:

func fib(N int) int {
    if N <= 1 {
        return N
    }
    if N == 2 {
        return 1
    }
    Mem := make([]int, N + 1)
    Mem[0] = 0
    Mem[1] = 1
    Mem[2] = 1
    return f(N, Mem)
}

func f(N int, Mem []int) int {
    if ans := Mem[N]; ans > 0 {
        return ans
    }
    Mem[N] = f(N - 1, Mem) + f(N - 2, Mem)
    return Mem[N]
}

将计算过的数记录在Mem中,下次计算时先判断Mem是否存在,如果存在直接返回,否则计算。

自下而上的优化解法

上述优化算法解决了重复计算的问题,但是显然还是用到了递归。是否有进一步的优化空间呢?还是存在的。
我们可以先计算出需要的序列,等到调用时直接返回。代码如下:

func fib(N int) int {
    if N <= 1 {
        return N
    }
    if N == 2 {
        return 1
    }
    f := make([]int, N + 1)
    f[0] = 0
    f[1] = 1
    f[2] = 1
    for i := 3; i <= N; i++ {
        f[i] = f[i - 1] + f[i - 2]
    }
    return f[N]
}

这个算法比较清晰明了,不做解释。

动态规划

描述了这么多斐波那契的问题,和动态规划有什么联系吗?其实上面的两种优化算法就是动态规划的两种形式。
什么是动态规划呢?我们可以从分析上述解法入手。两种优化解法都用到了一个思路,就是记录之前的问题,不做重复计算。也就是把一个问题拆分成一个个小的问题,对小问题进行计算并保存,而复杂问题就是这些小问题的组合。
这就是动态规划的核心思路: Those who cannot remember the past are condemned to repeat it.
在总结动态规划之前,先来几个例子:

钢条切割

在《算法导论》中有一个钢条切割的问题:

Description
The rod-cutting problem is the following. Given a rod of length n inches and a table of prices p_i for i = 1,2,…,n, determine the maximum revenue r_n obtainable by cutting up the rod and selling the pieces. Note that if the price p_n for a rod of length n is large enough, an optimal solution may require no cutting at all.
Example

length i 1 2 3 4 5 6 7 8 9 10
price p_i 1 5 8 9 10 17 17 20 24 30

下面进行一下分析:
长度为n的钢铁一共有2^{n+1}中切割方式。假设将钢铁切割为k段(1\le k \le n),则切割方案为n=i_1+i_2+...+i_k,收益为r_n=p_{i_1}+p_{i_2}+...+p_{i_k}
不难看出,长度为n的钢铁的最佳方案可以拆解为长度为i和n-i两段的和的最佳方案0 \le i \le n,即r_n = \max_{1 \le i \le n}(r_i + r_{n-i})。从而我们可以将切割问题按照上述逐层拆解,即:
r_n=\begin{cases} p_1 & n=1 \\ r_i + r_{n-i} & 1 < i \le n \end{cases}

源码:

func cutRod(price map[int]int, n int) int {
    var r = []int{0}
    for i := 1; i <= n; i ++ {
        p := price[i]
        for j := 1; j <= i; j++ {
            tmp := r[j - 1] + r[i - j]
            if tmp > p {
                p = tmp
            }
        }
        r = append(r, p)
    }
    return r[n]
}

Maximum Subarray

这是leetcode上的一个题目:

Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.
Example:

Input: [-2,1,-3,4,-1,2,1,-5,4],
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

分析:
假设以第i位0 \le i < len(nums)结尾的子数组的最大和为p_i,上述题目就是计算\max(p_i),0 \le i < len(nums)。而p_i可以表示为:
p_i=\begin{cases} nums[i] & p_{i-1}<=0 \ or \ i = 0 \\ p_{i-1} + nums[i] & p_{i-1} > 0 \end{cases}

源码:

func maxSubArray(nums []int) int {
    maxSum := []int{nums[0]}
    max := nums[0]
    for i := 1; i < len(nums); i++ {
        if maxSum[i - 1] < 0 {
            maxSum = append(maxSum, nums[i])
        } else {
            maxSum = append(maxSum, nums[i] + maxSum[i - 1])
        }
        if max < maxSum[i] {
            max = maxSum[i]
        }
    }
    return max
}

优化:
这个问题可以不对子问题进行存储,因此可以简化成:

func maxSubArray(nums []int) int {
    max, maxSum := nums[0], nums[0]
    for i := 1; i < len(nums); i++ {
        if max < 0 {
            max = 0
        }
        max += nums[i]
        if maxSum < max {
            maxSum = max
        }
    }
    return maxSum
}

总结

从上述例子我们可以看到,动态规划问题的基本思想与分治法类似,将待求解的问题分解为若干个子问题,按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供信息。动态规划解决的问题多数有重叠子问题这个特点。为降低反复计算。对每个子问题仅仅解一次,将其不同阶段的不同状态保存下来。适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的。
借用网上的总结,求解动态规划的问题的步骤如下:

  1. 划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
  2. 确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
  3. 确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
  4. 寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345