逐步解决动态规划之01背包问题

[站外图片上传中...(image-fe2902-1583294261806)]

什么是动态规划?

动态规划(Dynamic programming,简称DP),是一种通过“大而化小”的思路解决问题的算法。

动态规划没有明确的算法模板,准确的说,它是一种思想。动态规划是一种解决问题的思想。

什么样的题目适用于动态规划

  1. 求最大值 / 最小值
  2. 求可不可行
  3. 求方案总数

以上三种问题基本上都是用动态规划来求解。

注意:如果问题是让你求出“所有的”方案和结果,则肯定不是使用动态规划。

动态规划问题的解决过程

主要以3个子目标来攻克此类问题:

  1. 建立状态转移方程。
  2. 缓存并复用以往结果。
  3. 按顺序从小往大算。

举个简单的例子,1个人有2条腿,2个人有4条腿,...,100 个人有多少条腿?

首先要建立状态转移方程: f(n) = 2n

这个太简单了不用缓存复用,直接计算即可。

嘴上说还是不够的,我们用实际的问题从浅到深,逐步来深入了解动态规划问题。

[Talk is cheap. Show me the code]

一、斐波那契数列

  • 斐波那契数列:0,1,1,2,3,5,8,13,21,34,55,89,144...

  • 图形如下:

[图片上传失败...(image-cea13e-1583294261806)]

  • 斐波那契数列规律计算公式如下:

[图片上传失败...(image-208a66-1583294261806)]

  1. 先看看暴力递归方法:
def fib(n):
    if n < 2:
        return n
    else:
        return fib(n - 1) + fib(n - 2)


if __name__ == "__main__":
    result = fib(100)
    print(result) # 别等了 根本执行不出来

简单递归的时间复杂度达到了O(n2)。 因为每次都要进行重复计算。

f(3) = f(2) + f(1)

f(4) = f(3) + f(2)

可以看出,f(2) 计算了两次,计算的n越大,循环的次数越多。

  1. 动态规划

    In [20]: def fib(n):
        ...:     result = list(range(n+1)) # 用于缓存以往结果,以便复用(目标2)
        ...:     for i in range(n + 1):   # 按顺序从小往大算(目标3)
        ...:         if i < 2:
        ...:             result[i] = i
        ...:         else:
                      # 使用状态转移方程(目标1),同时复用以往结果(目标2)
        ...:             result[i] = result[i - 1] + result[i - 2]
        ...:     return result[-1] # 输出列表中最后一个数值,也就是fib(n)的值
        ...:
    
    In [21]: result = fib(100)
    
    In [22]: result
    Out[22]: 354224848179261915075L
    
  1. (目标1)状态转移方程是什么: f(n) = f(n-1) + f(n-2)
  2. (目标2)缓存并复用以往的结果。在 result[i] = result[i - 1] +result[i - 2]中进行了复用。相当于你在计算了 + 1 + 1 + 1 + 1 + 1 + 1 + 1= 7 之后,再 + 1 你可以很快的计算出是 8 ,不必再重新将 8 个 1 相加。
  3. (目标3)从小到大计算。这样以来,时间复杂度降为O(n)。我们只需要计算红圈圈出的部分即可。节省了大量的时间。
1583152807162.png

二、不同路径问题

参照LeetCode上的62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不同的路径?

上图是一个7 x 3 的网格。有多少可能的路径?

太多了,我简化一个3 X 2 的。

解这道题的时候,我们也要从三个目标开始

  1. (目标1)状态转移方程是什么:

    这道题你必须要知道转移方程是什么。不然的话下面都不用做了...

    f(i, j) = f(i-1, j) + f(i, j - 1)

  2. (目标2)缓存并复用以往的结果。这里是一个二维的行列问题。我在这里用二维的数组解决这个问题。

  3. (目标3)同样也是从小到大计算。我们需要双重循环,逐行逐列进行计算。

# encoding:utf8
class Solution(object):
    def uniquePaths(self, m, n):
        """
        :type m: int m为行数
        :type n: int n为列数
        :rtype: int
        """
        result = [[1] * n] * m
        for i in range(1, m):
            for j in range(1, n):
                result[i][j] = result[i][j - 1] + result[i - 1][j]
        return result[-1][-1]


if __name__ == '__main__':
    s = Solution()
    print s.uniquePaths(3, 7)

输出结果为28,证明我们的计算方法是正确的。

三、01背包问题

有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?

为方便讲解和理解,下面讲述的例子均先用具体的数字代入,即:eg:number=4,capacity=6

i(物品编号) 1 2 3 4
w(体积) 2 3 4 5
v(价值) 3 4 5 6

背包问题(0—1背包)

有N件物品,背包的最大承重为M,每件物品的数量均为1,价值集合为V,重量集合为W,计算该背包可以承载的物品的最大价值。
动态规划思想:

  • 动态规划解题步骤:问题抽象化、建立模型、寻找约束条件、判断是否满足最优性原理、找大问题与小问题的递推关系式、填表、寻找解组成

  • 动态规划的原理

    • 动态规划与分治法类似,都是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。
    • 但不同的是:
      • 分治法在子问题和子子问题等上被重复计算了很多次,
      • 而动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。
  • 背包问题的解决过程
    在解决问题之前,为描述方便,首先定义一些变量:Vi表示第i个物品的价值,Wi表示第i个物品的体积,定义V(i,j):当前背包容量j,前i个物品最佳组合对应的价值,同时背包问题抽象化(X1X2,…,Xn,其中Xi 取0或1,表示第i 个物品选或不选)。

    此处要注意各个变量代表的含义,尤其是V(i,j):当前背包容量j,前i个物品最佳组合对应的价值。

1、建立模型,即求max(V1X1+V2X2+…+VnXn)

2、寻找约束条件,W1X1+W2X2+…+WnXn < capacity

3、寻找递推关系式,面对当前商品有两种可能性:

图示:

capacity 0 1 2 3 4 5 6
0 weight value 0 0 0 0 0 0 0
1 2 3 0 0 3 3 3 3 3
2 3 4 0 0 3 4 4 7 7
3 4 5 0 0 3 4 5 7 8
4 5 6 0 0 3 4 5 7 8
# encoding:utf8
class Solution(object):
    def bag01(self, weights, values, capacity):
        # 动态规划
        n = len(weights)
        result = [[0 for j in range(capacity + 1)] for i in range(n + 1)]
        for i in range(1, n + 1):
            for j in range(1, capacity + 1):
                result[i][j] = result[i - 1][j]
                # 背包总容量够放当前物体,遍历前一个状态考虑是否置换
                if j >= weights[i - 1] and result[i][j] < result[i - 1][j - weights[i - 1]] + values[i - 1]:
                    result[i][j] = result[i - 1][j - weights[i - 1]] + values[i - 1]
        for x in result:
            print(x)
        return result

    def show(self, weights, capacity, result):
        n = len(weights)
        print ('最大解为:{}'.format(result[n][capacity]))
        x = [False for i in range(n + 1)]
        j = capacity
        for i in range(n, 0, -1):
            if result[i][j] > result[i - 1][j]:
                # i代表第i个物品,如果放入第i个物品的价值大于同等重量放入i-1物品的重量,说明选择了物品i
                x[i] = True
                j -= weights[i - 1]
        print('items:')
        for i in range(n + 1):
            if x[i]:
                print('no:{}'.format(i))


if __name__ == '__main__':
    s = Solution()
    weights = [2, 2, 3, 1, 5, 2]
    values = [2, 3, 1, 5, 4, 3]
    capacity = 10
    result = s.bag01(weights, values, capacity)
    s.show(weights, capacity, result)

输出结果:

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2]
[0, 0, 3, 3, 5, 5, 5, 5, 5, 5, 5]
[0, 0, 3, 3, 5, 5, 5, 6, 6, 6, 6]
[0, 5, 5, 8, 8, 10, 10, 10, 11, 11, 11]
[0, 5, 5, 8, 8, 10, 10, 10, 12, 12, 14]
[0, 5, 5, 8, 8, 11, 11, 13, 13, 13, 15]
最大解为:15
items:
no:2
no:4
no:5
no:6

Process finished with exit code 0

以上。

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

推荐阅读更多精彩内容