动态规划_实战1

2018-08-29  本文已影响0人  小碧小琳

参考剑指offer

一、前言——递归与循环

即使同一个算法用循环和递归两种思路实现的时间效率可能会大不一样。

1.1、递归

1.1.1、递归的特点:

1.2、数组 + 循环 —— 取其精华,去其糟粕

1.3 引入动态规划

绝大部分动态规划算法的分析和代码实现都是分这两个步骤完成的。
步骤1:用递归的思路分析问题
步骤2:用数组保存中间结果,并基于循环实现。

所以,接下来在实战的时候,会按照如下的思路,分析问题,并写出代码

问题建模:
算法实现

需要注意的地方:
一般,问题中有k个参数,可能DP的数组就是k-1维的。
比如,挖金矿问题中,有金矿数量和矿工人数两个参数,那么需要维护一个一维数组。
在合唱团问题中,有n,k,d三个参数,就需要维护二维数组。

二、实战

根据上面所说思路,分别用先建模,后递归,最后DP的算法解决问题。

2.1、爬楼梯问题

参考什么是动态规划

2.1.1问题建模

def climb(n):
    if n == 1:
        return 1
    elif n == 2:
        return 2
    else:
        return climb(n-1) + climb(n-2)

由前面递归可知,若是用DP自底向上的实现,那么每次需要记录当前n的前两个结果即可。
因此1、用一个数组把最优子结构的中间结果保存起来 2、在循环中不断地更新该数组。

def climb_dp(n):
    if n == 1:
        return 1
    elif n == 2:
        return 2
    #用一个数组把最优子结构的中间结果保存起来,在循环中不断地更新该数组
    pre_list = [1,2]
    for i in range(3,n+1):
        f_i = pre_list[0] + pre_list[1]
        pre_list[0] = pre_list[1]
        pre_list[1] = f_i
    return f_i

2.2、找零问题

2.2.1、问题建模

1、定义目标变量
设最少找零硬币数为F(N)
2、dp三要素
最优子结构:根据零钱与硬币面额的大小,最多有四个最优子结构
动态转移方程:F(N)等于最优子结构中的最小值,再加1
边界条件:N等于硬币面额
3、用递归写代码

#这个递归,运行起来真的会占用很长时间!
def makechange(n,coinValueList):
    if n in coinValueList:
        return 1
    min_res = []
    for i in coinValueList:
        if n - i > 0:
            min_res.append(makechange(n-i,coinValueList)+1)
            print(min_res)
    return min(min_res)

print(makechange(63,[1,5,10,25]))

4、从n=1开始一直到N,用一个数组保存最优子结构的值,一直循环到N
因为最优子结构有m个,所以中间会需要m此循环,用来计算每个最优子结构,用以在循环结束后选择最好得作为当前值的值。

def makechange_dp(n,coinValueList):
    if n == 1:
        return 1
    #将中间会用到的每个零钱数的最少硬币数,用字典存起来
    result = {}
    result[1] = 1
    for i in range(2,n+1):
        min_coins = []
        #利用状态转移方程计算每一个最优子结构需要的最少的硬币数,并选出最小值
        for j in coinValueList:
            if i > j:
                min_coins.append(result[i-j] + 1)
        result[i] = min(min_coins)
    return result[n]

print(makechange_dp(63,[1,5,10,25]))

2.3、国王与金矿问题

建模参考进一步理解动态规划

递归实现:

def getMostGold(m,n,gold_list,miner_list):
    #边界条件
    if m == 1 and n < miner_list[0]:
        return 0
    elif m == 1 and n >= miner_list[0]:
        return gold_list[0]
    
    #这个情况也是需要考虑的,如果最后一个矿需要的工人数大于总工人数n,
    # 那么绝对不会动最后一个矿
    elif m > 1 and n < miner_list[m-1]:
        return getMostGold(m-1,n,gold_list,miner_list)
    
    #状态转移方程,在人数充足,并且n大于最后一个矿工人数时,可以用状态转移方程
    else:
        return max(getMostGold(m-1,n,gold_list,miner_list),
                   getMostGold(m-1,n-miner_list[m-1],gold_list,miner_list) + gold_list[m-1])

print(getMostGold(5,10,[500,200,300,350,400],[5,3,4,3,5]))

动态规划

怎么确定需要保存的数组?
递归的进一步算法是备忘录算法,可以通过备忘录算法,来找到比DP保存的中间数组稍微大的数组。

自底向上的实现,根据网页中画的表格可以帮助理解。
也可以从另一方面思考。
1、循环数:
有两个参数维度,就用两个循环实现。最外层循环为矿数m,内层循环为工人数n。
2、需要保存的数组:
由状态转移方程可知,当有m个矿时,F(m,n)是由m-1时的两个状态决定的
因此只需要保存矿数为m-1的n个状态即可,因此需要构造一个长度为n的list用来保存中间结果

怎样根据第m-1个list中的每个格子,推断出第m个list中的每个格子的数值。。。不能确定状态转移方程的每个具体参数时,可以举例子来直观理解。

代码实现:

def getMostGold_dp(m,n,gold_list,miner_list):
    #根据边界条件,在金矿数为1时,得到初始list
    mid_res = [ ]
    #注意,中间结果保存的是矿工人数为1到10人的情况,如果只是range(n),那么就是0-9的情况
    for i in range(1,n+1):
        if i < miner_list[0]:
            mid_res.append(0)
        else:
            mid_res.append(gold_list[0])
    print(mid_res)


    for i in range(1,m):
    #根据状态转移方程,得到矿数一定为m-1时,人数不同对应的各种状态
        m_result = []
        for j in range(n):
            #若是人数小于最后一个矿所需 矿工数,那就直接按上一个来
            if j - miner_list[i] < 0:
                m_result.append(mid_res[j])
            else:
                m_result.append(max(gold_list[i] + mid_res[j-miner_list[i]],mid_res[j]))
        mid_res = m_result
        print(mid_res)
    return mid_res

getMostGold_dp(5,10,[500,200,300,350,400],[5,3,4,3,5])
上一篇下一篇

猜你喜欢

热点阅读