数据结构之动态规划问题

数据结构中动态规划应该算得上是你避不开的一道槛了吧!其重要性不言而喻,今天就整理下学习笔记分享出来。希望对读者朋友也能有帮助,文章基本框架如下:

  • 什么是动态规划
  • 小偷的背包问题
  • LeetCode刷题

什么是动态规划

定义

动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

上述是*的解释。能够看的出来最为关键的点有这样3个。

  • 最优子结构
  • 最小子问题边界
  • 状态转移函数(拆分转化方法)
    最优子结构指的是某一阶段复杂问题可以拆解成之前某些阶段的子问题;边界则是指对于最小子问题的解;状态转移方法指的是复杂某阶段复杂问题和之前阶段的转化关系。

举例解释

我们可以以斐波那契数列(Fibonacci)为例!

1, 1, 2, 3, 5, 8, 13 ,21 …

如果把第n个斐波拉契数记作 F(n),那么怎样利用动态规划来解释这个问题呢?

先谈最小子问题边界,当然是最前边的两个数F(1)和F(2)。

F(1)=F(2)=1。

最优子结构则是当前阶段斐波拉契数可以由之前斐波拉契数计算而出,计算关系则是我们说的状态转移方法。这里的状态转移函数为:

F(n)=F(n-1)+F(n-2),n>2

小偷的背包问题

上述斐波拉契数的例子只是用来举例便于理解动态规划的3个特点。真正经典的动态规划题目是小偷的背包问题:

一个小偷闯进一家珠宝店,他只背了一个可承重W的背包,珠宝店里有N件物品,其重量分别为w1,w2……wN,其价值分别为v1,v2……vN。那么问题来了,小偷该选择偷哪些东西用背包带走呢?(假设都为整数单位)

用动态规划的思路来解决这个题目,可以把物品的种类和书包承重力当成状态,绘制出一个二维表格如下,行表示种类从1到N,列表示承重从0到W。


0 1 …… W
1



2



……



N



再定义2个列表存储N类物品各自的重量和价值:

w=[w1,w2,……wN]

v=[v1,v2,^vN]

先思考整体过程

首先假设只有一件物品重量w[0]价值v[0],假设背包承重能力从0到W,被偷的最大价值为res,也就是第一行从左到右的过程。

不难想象,这时只用判断背包承重是否能够承受第一行对应物品,不能则res=0,能则被偷走,res=v[0]。


0 1 …… W
1 0 v[0] …… v[0]
2



……



N



确定第一行(边界)不难,关键在于如何找到状态转移函数,即res[i][j]是如何从之前状态获取计算的。res[i][j]表示共i+1件物品,j承重的时候,能偷走的最大价值。(加1是因为列表索引从0开始)

选择边界并确定状态转移函数

重点来了!当已知res[i-1][:]所有状态,现在有第i+1件物品可以选择,我们该怎么判断?

首先判断第i+1件物品是否超出背包当前承重(即w[i]>j),超出则肯定放不进去,最大价值和有无第i+1件物品无关,即res[i][j]=res[i-1][j]。

当可以放进去的时候,有两个选择,放或不放,选择不放该物品,则依旧是res[i][j]=res[i-1][j];选择放该物体则最大价值是res[i][j]=v[i]+res[i-1][j-w[i]],表示放了之后剩余载重可带走的最大价值。

两种选择取其大者即可!上述逻辑代码如下,也是核心代码。

for i in range(1,N):#从第二行开始
    for j in range(C+1):
        #这里是关键的状态转移函数
        if j < w[i]:
            res[i][j] = res[i-1][j]
        else:
            res[i][j] = max(res[i-1][j], v[i]+res[i-1][j-w[i]])#注意这里,应当设置承重为0的情况(即j从0到C+1);可能存在j=w[i]
return res

当返回了res后,其实res[N-1][C]即为所求的最大价值。

但是!如果我要你告诉我你选择哪几件物品的时候能取得上述最大值呢?思考一分钟再往后看!

其实,这会我们反过来看就比较简单了,因为上述核心就是对比res[i][j]和res[i-1][j],能够想到如果二者不相等的时候,必然说明res[i][j]对应的物品偷走了(没偷走二者应当相等),若取走了该件物品,背包载重则减少了对应的承重。所以这样就能够判断出小偷偷走的是那些物品了,需要注意的是当i=1的时候无法判断res[0][j]对应行是否偷走,这里应当利用剩余承重是否为0了。

有点绕口,基本上是逐行把自己代码的思路讲解出来的,而不是直接给代码,希望对小白友好些。

代码实现

talk is cheap,show me the code:

# Author:小詹
#Date:2019-4-18
#动态规划的背包类问题,Python实现
def bag(N,C,w,v):
    '''
    N:物品种类
    C:背包承重
    w:物品种类对应的重量,列表
    v:物品种类对应的价值,列表
    '''
    res = [[0 for j in range(C+1)] for i in range(N)] #全0表格,1-N件物品,0-C承重
    for j in range(C+1):#初始化第一行边界
        if j < w[0]:
            res[0][j] = 0
        else:
            res[0][j] = v[0]
#     print(res)
    for i in range(1,N):#从第二行开始
        for j in range(C+1):
            #这里是关键的状态转移函数
            if j < w[i]:
                res[i][j] = res[i-1][j]
            else:
                res[i][j] = max(res[i-1][j], v[i]+res[i-1][j-w[i]])#注意这里,应当设置承重为0的情况(即j从0到C+1);可能存在j=w[i]
    return res
def show(N,C,w,res):
    '''
    通过比较res[i][j]和res[i-1][j]是否相等判断是否装了第i+1件物品;
    小詹这里没有充分发挥列表下标从0开始的特点,导致要做+1操作,能理解就好
    '''
    print('背包能放下物体最大价值为:',res[N-1][C])
    print(res)
    j = C
    pick = []
    for i in range(N-1,0,-1):
#         print(i)
        if res[i][j] > res[i-1][j]:
        #因为res中从0开始是第1行,这里i-1对应第i行;将第i行和i-1行对比,如果不一样则表示背包装了第i行对应的物品
            pick.append(i+1)
            j-=w[i-1]               
    if j > 0:
        pick.append(1)
    print('背包依次装了第',pick,'件物品')
def main():
    N = 5
    C = 10
    w = [2, 2, 6, 5, 4]
    v = [6, 3, 5, 4, 6]
    res = bag(N,C,w,v)
    show(N,C,w,res)
if __name__ =='__main__':
    main()

上述就是完整代码。(码字不易,转用请注明出处)


LeetCode刷题

嗯哼,如果你把前两部分彻底搞懂了,对动态规划至少也算是有个初步了解了吧!

熟能生巧,当然得在力扣上找点题目练练手吧?这里有3道题可以去尝试一下噢!

1.LeetCode第5题————最长回文字串

2.LeetCode第10题————正则表达式匹配

3.LeetCode第32题————最长有效括号

4.Leetcode第62题————不同路径

5.Leetcode第63题————不同路径2

上一篇:[usaco]3.4.4 变形的动态规划问题Electric Fence


下一篇:CYQ.Data 轻量数据层之路 常见问题QA(三十)