从[Leetcode 790. Domino and Tromino Tiling]谈DP

前言

写本文的动机是想谈谈近期学习动态规划的一些心得体会,记录一下学习路径顺便推荐一些优秀的博文,抛砖引玉,为之后想要学习的朋友做一点简单的总结指引。本文原计划从一个简单的DP问题入手来谈谈近期的学习心得,然后引申到背包问题、阶梯问题并做一个简单的入门讲解,但是在行文过程中发现笔者写作水平有限,很多想法表达不够直白,相较之后推荐的博文,难免有画蛇添足之嫌,贻笑大方,于是在解题过程中穿插一些想法。错误在所难免,望大家指正。接下来进入正题。

题目:Leetcode 790. Domino and Tromino Tiling

图1

题意:
给定 N,有多少种方式可以铺满 2xN 的格子?将结果模 10^9+7 返回。
分别有两种铺法:L 型和 - 型。

分析

这是动态规划题目中典型题目里的一种——铺砖问题。此外还有背包问题、阶梯问题以及衍生的切割问题等。

DP问题的求解思路是比较明确的,通过审题总结出递推公式,然后根据初始条件通过迭代计算得出正解,即求解DP问题的通用范式:最优子结构 + 状态转移方程 + 边界条件

回到本文主题,对于铺砖问题,说难也难说简单也简单,难的是找出最优子结构,因为如果对DP问题了解的不多的话,列举各个阶段的解并找出之间的规律经常让人无从下手;简单的是对于此类问题的解法比较固定,规律找多了也就有经验了,熟悉之后对于大部分中难度的DP问题可以说是一马平川。

对于本文所要求解的问题,先列举出前5个阶段来总结规律, 如图2:(图中隐含了递推规律,可对照图示先行推导)

图2

如题所述,对于-型和L型铺法,各有两种铺法,其中-型分为 -|分别占1列和2列,而L型的两种铺法对称,都占两列,如图3:

图3

所以不难看出,要铺满 2xN 的格子,我们有三种方式:

  1. 铺满 2x(N-1) 的格子然后用|铺满最后一列;
  2. 先铺满 2x(N-2) 的格子然后用-铺满最后两列;
  3. L型刚好铺满。

如果是前两种情况,那么子问题也是完整的被铺满,而第三种情况则不是,因为只有两个对称的L型才能完整的契合。如图4:(黑色表示所有可铺满的情况)

图4

这样我们就找到了最优子结构,因为要区分子问题是否已经铺满,所以用一个二维数组来表示:

  • 定义 f[i][j] 表示铺满 2xi 格子时还有 j 个格子被铺满的解,那么 j 只有两种取值 01,即 f[i][0] 表示刚好铺满 2xi 格子的解,f[i][1]表示铺满 2xi 格子之后又多铺了 i+1 列中的一个格子,也就是以L型结尾的情形。

那么 f[i][0] 将由三部分子解组成,写出递推公式:

f[i][0] = f[i-1][0] + f[i-2][0] + f[i-2][1]

加上边界条件

容易理解,对于 N=0,铺满 2x0 格子的方案只有一种就是不铺。不存在铺满 2x0 格子且多铺1格子的方案,所以为 0 ;对于 N=1,铺满 2x1 格子的方案只有一种,用|即可铺满,而用L型即可得到两种铺满 2x1 格子且多铺一格的方案。

f[0][0] = 1, f[0][1] = 0
f[1][0] = 1, f[1][1] = 2

观察递推公式:f[i][0] = f[i-1][0] + f[i-2][0] + f[i-2][1]

其中f[i-1][0] + f[i-2][0]便是所求解的子解,有了初始的边界条件便能容易得推出,所以问题主要在 f[i][1] 的推导,很容易便能想到这两种对称的情形,即f[i][1] = f[i-1][0]*2,也就是说 f[i][1] 可由 f[i-1][0] 和两个对称的L型拼成,如图5:

图5

到这里,问题似乎得到了解决,剩下所要做的无非是将推导过程翻译成相应的代码,可当我们对推导出的状态转移方程做以简单的验证便却发现结果并不符合预期。

回到递推式:f[i][0] = f[i-1][0] + f[i-2][0] + f[i-2][1]

思考: 对于-型铺法,要么用只占一列的|铺设,要么用占两行两列的-铺设,这点毫无疑问,那么问题定是出在了f[i][1]的求解上。继续观察之前 [图2] 所例举的 N <= 4 的所有情形,由于用黑色填充省略了子问题的布局方式,可能导致我们遗漏了一些细节,将其展开后可以发现在 N=4 的时候多出了一种没有考虑到的L型,如图6:

图6

从上图可以看出,L型与-型也可以组成L型,此时f[i][1] = f[i-1][1],可以解释为 f[i][1]f[i-1][1] 加上-型拼成。需要注意的是,f[i-1][1]中隐含对称的情形,因此不需要乘 2
所以最终的状态转移方程为:

f[0][0] = 1
f[0][1] = 0
f[i][0] = f[i-1][0] + f[i-2][0] + f[i-2][1]
f[i][1] = f[i-1][0]*2 + f[i-1][1]

由递推公式写出部分递推过程:

f[0][0] = 1, f[0][1] = 0
f[1][0] = 1, f[1][1] = 2
f[2][0] = f[1][0] + f[0][0] + f[0][1] = 2,   f[2][1] = 4
f[3][0] = f[2][0] + f[1][0] + f[1][1] = 5,   f[3][1] = 8
f[4][0] = f[3][0] + f[2][0] + f[2][1] = 11,  f[4][1] = 18
f[5][0] = f[4][0] + f[3][0] + f[3][1] = 24,  f[5][1] = 40
f[6][0] = f[5][0] + f[4][0] + f[4][1] = 53,  f[6][1] = 88
f[7][0] = f[6][0] + f[5][0] + f[5][1] = 117, f[7][1] = 194
... ...

至此,完成求解。


Python 代码:

class Solution(object):
    def numTilings(self, N):
        """
        :type N: int
        :rtype: int
        """
        if N < 1:
            return 0
        N += 1
        module = pow(10, 9)+7   
        dp = [[0, 0]*N for _ in range(N)]
        dp[0][0], dp[1][0], dp[1][1] = 1, 1, 2
        for i in range(2, N):
            dp[i][0] = dp[i-1][0] + dp[i-2][0] + dp[i-2][1]
            dp[i][1] = dp[i-1][0]*2 + dp[i-1][1]
        return dp[N-1][0] % module

注意⚠️:可以看到最后的结果模 10^9 + 7返回,也就是题目中加粗提到的Return your answer modulo 10^9 + 7.以防止整数溢出。本人由于没有认真审题,第一次提交没有AC,在排查的时候花费了不必要的功夫,因此也在这里提醒各位粗心的朋友,解题的前提是理解题目,本人在以往的工作学习中吃过很多亏,谨以为诫。

在本题的讨论区下主流的解题思路与本文不同,在效率上本文的求解方式也只打败了可怜的8.5%的提交,然而本文的初衷是通过本题对DP建立一个初级的思考过程,效率问题不在讨论范畴之内,有兴趣的朋友可以自行学习。

后记

其实对于铺砖问题乃至大部分的DP问题,主要的难点就是对问题的抽象以及状态的思考上,对于本文所求解的问题,按照直觉,用 f(n) 表示铺满 2xn 格子的解,只需要找出 f(n)f(n-1),f(n-2)... 的关系便可,而在实际思考过程中却发现需要多加一个纬度。当然对于本文所论述的题目来说这种改动相对而言比较自然直观,而对于一些较难的题目可能就没有那么显而易见。直接的翻译题目去求解很可能走进死胡同。这时候很可能需要换一种思路来曲线救国,让看起来纷繁复杂的问题变得直观简单。

对于本文所论述的问题,只需列举 N<=4 的情形便能找到规律,但这毕竟是后知后觉的做法,也就是说是在知道了答案再去论述求解过程。其实笔者在第一次思考这个问题的时候列举到了 N<=6,此时 f[6][0]=53,对于人肉枚举来说已经让人望而生畏。

通常,对未知状态恐惧的心理障碍是阻碍解题的第一道门槛。笔者认为归根到底是因为缺乏逻辑训练,对于问题的分析求解没有一个成体系的流程,所以希望大家在日常学习中多加思考,逐步形成自己的思维模式。

一些推荐

本文求解的思路主要来源于 夜深人静写算法(二) - 动态规划[1] 这篇博文,其作者对DP问题的论述不论在深度还是广度上都有很深入的见解,有兴趣的朋友可以学习一下。

通过对大量DP问题学习总结可以发现,DP是一种带备忘录的自底而上的递归求解思想。当时理解这一点也是来自于这篇博文:漫画:什么是动态规划?[2]。作者讲的非常有趣,通过最简单的爬楼梯将递归求解一步步上升到DP问题,对于初学者有很大的帮助。

笔者在刚接触DP问题时,通常无从下手,茫然不知所措,在先后了解了背包问题、阶梯问题以及铺砖问题,才慢慢对动态规划有了一个感性的认识。尽管目前在做新题时也会纠结好久,但好在有了指导思想,大概知道前进的方向。在这里笔者推荐背包九讲[3]这篇在网络上广泛流传的博文,原作者对背包问题进行了一个全面的总结,对不同背包问题的讲解简单直接,各个章节篇幅不长,详细的理解前三章会让读者受益匪浅。读完后可通过leetcode 474. Ones and Zeroes进行练习。

需要注意的是在背包九讲中作者在 1.5 一个常数优化 一节中存在错误,本想联系下作者勘误,但是发现在github上有人已经提了issue,感兴趣的朋友也可以找找具体位置以验证是否真的理解了作者所要传达的思想。

索引


  1. 夜深人静写算法(二) - 动态规划

  2. 漫画:什么是动态规划?

  3. 背包九讲

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

推荐阅读更多精彩内容

  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,658评论 0 33
  • 各校历年复试机试试题 清华、北大、华科试题详细笔记部分,少笔记部分与少数leetcode【含个人整理笔记】 一、详...
    十里江城阅读 1,130评论 0 1
  • 一.时间周期 2018年7月16日---2018年7月24日 二.互评分组 .A组:就走偏锋,傲娇一只羊,嗨_四月...
    失落的羊阅读 2,444评论 68 2
  • 春天晨练以其独特的魅力吸引成千上万的群众,特别中老年人是晨练活动的主力军,他们追索着青春,恢复着活力,增进着...
    太阳花_801e阅读 372评论 2 13
  • 《山鬼》目录 上一章:青蔓 下一章:灵芝 上章末段:山鬼庙外,忽然传来吵哄哄的声音,敲锣打鼓,还有众多汉子们骂骂咧...
    车钱草阅读 745评论 4 7