基础-9:一文搞定动态规划

1 概述

前面的课程中讲到了图的基本遍历算法和简单的应用,本来想接着往后面继续讲,后来有童鞋说讲讲动态规划吧,看书有些晕,再联想到图算法中也会用到动态规划和贪心算法,就先把这两个写了。

动态规划,英文名称为dynamic programming,语义为:在运行过程中找最优解。第一次接触,可能有些模糊,本文会通过三个例子对动态规划这种思想进行阐述,动态规划不是一个固定的算法,它代表的是一种与递归类似的思想,在中小学就接触过,它没有深奥的数学推导,仔细琢磨就能搞定。本文通过最少硬币数、矩阵乘法和最长公共子串三个最常用的例子对动态规划进行说明。

2 动态规划实践

2.1 最少硬币数

问题:假设有1分、2分、5分、9分、1角的硬币若干枚,给定任意的一个金额,如1角8分,最少的硬币数是多少?
答:直觉貌似是1个1角+1个5分+1个2分+1个1分,这样是4个硬币。其实,对这个问题,用2个9分的硬币就行了。

对于上面的问题,最终的答案并不重要,关键是解题的过程。下面讨论如何得到这个答案。

假设对于任意的金额n(分),其最小的硬币数为rn,则有:

  • r1 = 1
  • r2 = 2
  • r3 = 2
  • r4 = 2
  • r5 = 1
  • r6 = 2
  • r7 = 2
  • r8 = 3
  • r9 = 1
  • r10 = 1

显然,并不是随着金额数的增加,所需的硬币数简单增加。若用k1、k2、k5、k9、k10分别表示组合成n分的硬币数,则有:

k1 * 1 + k2 * 2 + k5 * 5 + k9 * 9 + k10 * 10 = n

要求:

min(k1 + k2 + k5 + k9 + k10)

分析到这一步,可以开始用暴力求解了,即找出k1、k2、k5、k9、k10所有的组合,具体的算法如下:

// r[i]表示金额为i时的最小硬币数, n为金额
r := make([]int, 0){1, 2, 2, 2, 1, 2, 2, 3, 1, 1}
func MinCoins(n int) int{ 
    if n == 0{
        return 0
    }

    if n <= 10{
        return r[n - 1]
    }
    q := n
    for i := 1; i < n; i++{
        // min是求最小数的函数
        q = min(q, MinCoins( i) + MinCoins(n - i))
    }
    return q
}

对于任何n,MinCoins都需要重新计算1~n的MinCoins,其时间复杂度为T(n) = 2n。仔细分析算法MinCoins,会发现一个问题:即计算MinCoins(k)时,需要计算MinCoins(m)(m < k),而当计算MinCoins(k+1)时,仍然需要重复计算MinCoins(m)(m < k),这会产生庞大的时间开销(注:递归算法的开销请查看关于递归)。

针对这个问题,很自然的一个想法是:如果将每次计算的MinCoins值保存起来,则其计算开销将显著减小。此时,算法变成:

back := make([]int, 0){1, 2, 2, 2, 1, 2, 2, 3, 1, 1}
func MinCoinBack(n int) int{
    if n == 0{
        return 0
    }
    // 如果已经保存相应的解,直接返回
    if n <= len(back) - 1{
        return back[n]
    }

    q := n
    for i := 1; i < n; i++{
        if v, ok := back[i]; !ok{
            back = append(back, MinCoinBack(i))
        }else{
            q = min(q, v + MinCoinBack(n - i))
        }
    }
    return q    
}

此时,虽然也用到了递归,但在back中保存了每次的MinCostsBack的值,每个值只需计算一次,其时间复杂度为O(n2).

分析:在求最小硬币数的过程中,MinCoins(m)依赖于MinCoins(i)和MinCoins(m - i),而MinCoins(i)又依赖于MinCoins(j)和MinCoins(i - j),即每个问题,都依赖于更小的具有相同结构的子问题,如果每次都重复求解子问题,则需要的时间开销与规模呈幂集增长。若在计算过程中,将子问题的解保存,则每个子问题恰好只需要计算一次,其时间复杂度降为O(n2)。这就是一个简单的动态规划问题,其核心为子问题。

动态规划思想通常用来解决最优化问题,如本例中的最小硬币数,其基本特征是:规模为n的问题可以划分为数量有限的若干类子问题求解,且与子问题之间存在直接或间接递归,其解决方法也很直接,以空间换时间,提升算法效率。

2.2 矩阵乘法

矩阵乘法是代数计算的核心基础,是matlab的计算核心,如何优化矩阵乘法是关键。先看矩阵乘法:

若有矩阵A、B,且row(A) = p, col(A) = row(B) = m, col(B) = q,这A、B可以相乘,若C=AB,则Cij = Ai1B1j + Ai2B2j + ... + AimBmj. 矩阵乘法的代价主要由各标量元素乘法的次数决定,C=AB的计算复杂度为:pmq

如果有矩阵A1, A2, A3三个矩阵,A1为10*100矩阵、A2为100*5矩阵、A3为5*50矩阵,若按照(A1*A2)*A3的顺序计算,则需要标量乘法次数为:10*100*5 + 10*5*50 = 7500;若按照A1*(A2*A3),则需要的标量乘法次数为:100*5*50+10*100*50=75000。显然,不同的乘法顺序意味着不同的计算效率,本例中,前者的计算效率是后者的10倍,这正是算法的魅力所在。由此,也引出了一个著名的矩阵链乘法问题

给定n的矩阵A1, A2, ..., An,矩阵Ai的规模为pi * q i,求解A1*A2* ...*An的计算次序(矩阵乘法满足结合律),使其所需的标量乘法最少。

这里有两个地方需要注意:

  • 计算次序就是给矩阵加括号,确定先算哪两个矩阵的乘积,再算哪两个矩阵的成绩,通常称之为括号化方案;
  • 这里要确定括号化方案,并不是真正对矩阵实施乘法,而是确定以那种括号化方案计算最为高效。

分析:对于这类上来就带n的规模的问题,通常的思路是:先分析n=1、2、3、4时,再分析n=k时的问题,总结出一般的规律,最后对一般规律求解,下面分情况讨论:

  • n = 1, 2时,括号化方案是唯一的
  • n = 3时,有两种方案:(A1*A2)*A3和A1*(A2*A3)
  • n = 4时,有((A1*A2)*A3)*A4, (A1*(A2*A3))*A4, (A1*A2)*(A3*A4), A1*(A2*(A3*A4)), A1*((A2*A3)*A4)

当分析到n=4的时候,可以观察出规律了,可以将A1*A2*A3*A4看成:

  • A1*A2*A3与A4两部分,前者即是n=3的情形;
  • A1*A2 与A3*A4两部分,两部分均是n=2的情形;
  • A1与A2*A3*A4两部分,后者为n=3的情形。

这三种情形中,n=4可以分解为n=2, 3时的问题,即找到了这种递推规律:


矩阵链乘法递推规律

此时,剩下的问题时如何求括号化方案了。显然,这一问题要比最小硬币数要困难,虽然找到了规模为n的问题与规模小于n之间的关系,但是并未找到解之间的关系(这里的解是标量乘法次数)。

若有M=Ai*Ai+1* ...*Aj,row(Ai) = pi - 1, col(Ai) = pi不妨取M中标量的乘法次数为m[i, j],则有:m[i, j] = m[i, k] + m[k + 1, j] + pi-1pkpj,最小括号化方案为:

最小括号化方案

m[1, n]即为最小化括号方案的解。到这里,问题的本质就变成和最少硬币数一样的问题了,即需要将在求解m[i, j]过程中所求取的m[i, k]和m[k+1, j]的值暂存,规避重复求解,其算法如下所示(摘自算法导论):

矩阵最小括号化算法

同样,其算法规模由递归的O(2n)降为O(n2),感兴趣的童鞋可以深入分析。

2.3 最长公共子序列LCS

这个问题是企业面试中很常见的一个问题,本人帮朋友面试的时候,也经常会问这个问题,很少见童鞋能直接提到动态规划四个字。在具体的工程实践中,需要码农们分析归结到这个问题,才可以高效地实现算法。

先看子序列的定义:

若有序列X=< x1, x2, ..., xm >,存在另一个序列Z=< z1, z2, ..., zk >,且存在严格递增的X的下标序列< i1, i2, ..., ik >,满足(xi)j = zj (1 <= j <= k).

如果从数学分析中数列的角度看,Z就是X的子列。最长公共子序列就是求两个序列的最长公共子序列。如何求解最长公共子序列呢?

分析:假设有X,Y两个序列,X、Y长度分别为m、n,不妨取X,Y的最长公共子序列为LCS(X, Y), 可以按照下面的思路分析:

  • 当m = 0或n = 0时,没有公共子序列
  • 当m = i、 n = j时, 如果X[i]=Y[j],则有LCS(X, Y) = LCS(X1..i-1, Y1..j-1) + X[i],否则,LCS(X, Y) =max{ LCS(X1..i-1, Y), LCS(X, Y1..j-1)}

由上分析知,最长公共子序列变成了和最少硬币数、矩阵链乘法一样的问题。在实现时,需要逐元素比较两个序列,其复杂度为O(mn),算法描述如下:

// lcs表示最长公共子序列,用于暂存计算中的最长公共子序列,伪代码,需要修改才能运行,后续将在github上列出
lcs := make([][]string, 0) 
func LCS(X, Y string) string{
    for i := 0; i < len(X) ; i++{
        lcs[i][0] = ""
    }
    for j := 0; j < len(Y) ; j++{
        lcs[0][j] = ""
    }
    for i := 1; i <= len(X); i++{
        for j := 1; j <= len(Y); j++{
            if X[i] == Y[j]{
                lcs[i][j] = lcs[i-1][j-1] + X[i]
            }else if len(lcs[i - 1][j]) > len(lcs[i][j - 1]){
                lcs[i][j] = lcs[i - 1][j]
            }else{
                lcs[i][j] = lcs[i][j - 1]
            }
        }
    }
}

3 小结

最少硬币数、矩阵乘法、和LCS是三个典型的动态规划问题,它代表的是一种思考方式,应对的问题通常是优化问题,这三个例子中分别求解的问题是:最少的硬币数、最少的标量乘法数、最长的公共子序列,大部分情况下,是规模为n的问题通过类似递归演变为规模更小的子问题,有些书上称之为最优解结构。用动态规划时,一定要保证最终的子问题种类是有限的,而不能不停产生新的子问题

总体而言,这三个例子很好地代表了动态规划问题以及其解决方案,其本质都递归为有限的子问题,并且用空间换时间对其进行高效处理。

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

推荐阅读更多精彩内容