• 动态规划笔记


    原理:

    动态规划就是把一个大问题一步步降解成越来越小的子问题,直到子问题小到可以用确定的条件来解答。

    但是动态规划的本质不是递归,递归是完全自顶而下的,每次求解都需要重新计算所有的子问题。

    我觉得反映动态规划本质的解法是自底而上的解法,即按照顺序,从基元问题一步步扩大问题的规模,直到问题的规模覆盖了我要求解的问题。

    每一个规模的问题的解叫做一个状态,每个不同规模的问题的解的关系叫做状态转移方程。具体的可以看凑硬币问题的具体分析。


    1.凑硬币问题

    问题: 有1元、3元、5元面值的硬币若干,要凑到11元需要最少几个硬币?

    这是最简单的DP问题,记凑a元需要b个硬币为: n[a] = b。

    1)首先,如果凑0元 需要0个硬币表示为 n[0] = 0

    2)如果凑1元: 我们最近拿硬币的那次有三种可能 :拿了1元、3元、5元。

    于是问题表示为:n[1] = 1+n[0] 或者 1+n[-2]或者 1+n[-4]。是这三种情况的最小值。但是没有凑-2或者凑-4元的情况,所以n[1] = 1+n[0]

    3)凑2元:我们最近拿硬币的那次有三种可能 :拿了1元、3元、5元。

    n[2] = min{ 1+n[1], 1+n[-1],1+n[-3] } = 1+n[1],而n[1]已经从上一步知道了

    4)凑3元....

    凑4元.....

    .......

    凑11元: 我们最近拿硬币的那次有三种可能 :拿了1元、3元、5元。

    n[11] = min{ 1+n[10], 1+n[8], 1+n[6] } ,这里的n[10],n[8],n[6]在前几步就已经被算出来了,所以我们在这一步只要求个最小值就好了。

     

    每一个新的状态都是由 “最近一个可能的动作”+“做该动作的上一个状态”确定而转移来的,这就是最基本的状态转移。这个问题的状态转移方程就可以看为:

    n[m] = min{ 1+n[m-1], 1+n[m-3], 1+n[m-5] }

    #最像伪代码的写法
    def coins(M):
        n = [0]
        for N in range(1,M+1):
            l = []
            for i in [1,3,5]:
                if N-i >= 0:
                    l.append(n[N-i]+1)
            n.append(min(l))
        return n
       
    print(coins(11))
    #out:[0, 1, 2, 1, 2, 1, 2, 3, 2, 3, 2, 3]

    def coins(M):
        n = [0]
        for N in range(1,M+1):
            l = []
            print('%%%%%%',N)
            for i in [1,3,5]:
                if N-i >= 0:
                    l.append(n[N-i]+1)
            print('----',min(l))
            n.append(min(l))
            print('########',n)
        return n
       
    print(coins(11))
    %%%%%% 1
    ---- 1
    ######## [0, 1]
    %%%%%% 2
    ---- 2
    ######## [0, 1, 2]
    %%%%%% 3
    ---- 1
    ######## [0, 1, 2, 1]
    %%%%%% 4
    ---- 2
    ######## [0, 1, 2, 1, 2]
    %%%%%% 5
    ---- 1
    ######## [0, 1, 2, 1, 2, 1]
    %%%%%% 6
    ---- 2
    ######## [0, 1, 2, 1, 2, 1, 2]
    %%%%%% 7
    ---- 3
    ######## [0, 1, 2, 1, 2, 1, 2, 3]
    %%%%%% 8
    ---- 2
    ######## [0, 1, 2, 1, 2, 1, 2, 3, 2]
    %%%%%% 9
    ---- 3
    ######## [0, 1, 2, 1, 2, 1, 2, 3, 2, 3]
    %%%%%% 10
    ---- 2
    ######## [0, 1, 2, 1, 2, 1, 2, 3, 2, 3, 2]
    %%%%%% 11
    ---- 3
    ######## [0, 1, 2, 1, 2, 1, 2, 3, 2, 3, 2, 3]
    [0, 1, 2, 1, 2, 1, 2, 3, 2, 3, 2, 3]
     

    当然从递归的角度,底顶而下,一次性的解决这个问题也是可以的。不过,递归的解法在每次需要用到子问题的结果时,即使刚刚算过这个子问题,它也会忘掉结果,重新把子问题递归到最底层求解,所以复杂度比较高。

    def coins_rec(M):
        if M == 0:
            return 0
        l = []
        for i in [1,3,5]:
            if M - i >= 0:
                l.append( coins_rec(M-i)+1 )
        return min(l) 
    print(coins_rec(11))
    #out:3
    

    回头看刚刚的动态规划写法,其实这里用到了一个memo,即代码里面的n[] 数组,来从小到大的储存每一个已经解决的子问题的解,后面再需要用到该问题的解时,不需要重新算,读出来就行了。下面我们比较一下两种方法所用的时间。还是差距很大的。

    如果想要输出最小个数的同时输出组合的面值,就每次记录一下在 [1,3,5]这三个动作中,选取最小值时候使用的动作。

    def cal_memo(M):
        n = [0]
        change = [0]
        process = []
        for N in range(1,M+1):
            min_index = 100
            min_number = 100
            for i in [1,3,5]:
                if N-i >= 0:
                    if n[N-i]+1 < min_number:
                        min_index = i
                        min_number = n[N-i]+1   
            n.append(min_number)
            change.append(min_index)
        
        index = M
        while(index >0 ):
            process.append(change[index])
            index = index - change[index]
        
        return n[M],process
    
    print(cal_memo(11))
    #out:(3, [1, 5, 5])


    2.切刚条问题

    问题:有一段长长的刚条可以切断成若干节来卖,长度为0-10的刚条单独卖的价格为:p = [0,1,5,8,9,10,17,17,20,24,30]。 问长度为n的刚条,最好的切割方法是什么?

    可以看出,长度为2的刚条单独卖5元,切成2个长度为1的就只能卖2元了。而长度4的刚条9元,切成2个长度2的就是10元。所以贪心法是不对的。假设长度为n的刚条经过最优切割方案卖出的价格是 F[n]。可以把这个F[n]看成一个状态。其实我在做DP的时候,喜欢先找状态转移的条件,就是每一步可以有的动作,这里动作就是最近一次切下来长度为m的一部分,得到的新状态可以用旧状态表示为: F[n] = p[m]+F[n-m],当然我们的m需要遍历[1,2,....10] (n-m >= 0)。为什么不要0?因为和之前的凑硬币一样的,我们从F[0],F[1]一步步扩展的,多切一小段长度1出来肯定多卖一点钱。

    最像伪代码的代码,直接把扩展的过程print出来了。要输出长度的组合和凑硬币一样,记录一下每一步切割的长度就好

    def Cut(n):
        p = [0,1,5,8,9,10,17,17,20,24,30]
        F = [0]
        for i in range(1,n+1):
            l = []
            for m in range(1,11):
                if i-m >= 0:
                    l.append(p[m] + F[i-m])
            F.append(max(l))
            print(i,F)
        return F
    
    print(Cut(20))
    

    3.LCS问题

    最长公共子序列问题:有X Y两个序列,求最长的公共子序列的长度。举例:

    X: ABCBDAB

    Y: BDCABA

    最长的子序列为长度为4,有好几条,比如:BDAB BCAB。

    分析:基元的问题是x = X[:0], y = Y[:0]都是空序列,LCS(x,y) = 0。然后可以采取的动作是把X向后扩展一位或者把Y向后扩展一位。我们这里就X:求LCS(X[:1],Y[:0] ) = 0发现扩X没用的,Y[:0]是空的...所以LCS(X[:2],Y[:0] ) = 0....LCS[X,Y[:0]] = 0.我们这样一步步刷过去,相当于在一个 len(Y)+1 * len(X)+1的表格里面一行一行的从左到右算出每个A B部分对应的LCS。

    在中间我们发现要填写LCS( X[:i], Y[:j])时,自然想到

    如果X[i] = Y[j]那么LCS( X[:i], Y[:j]) = 1+LCS( X[:i-1], Y[:j-1]) 。而由于我们是一行行刷过来的,LCS( X[:i-1], Y[:j-1])求过了。

    如果X[i] != Y[j]那么LCS( X[:i], Y[:j]) = max{ LCS( X[:i], Y[:j-1]), LCS( X[:i-1], Y[:j]) }。这俩也都求出来了,所以这也就是一个填表的过程

    一行一行从左到有填过来的。可以回头找到好几组不同的最长子序列。箭头表示Y[i] = X[j]的状态转移。如果要print具体的子序列,就需要记录一下i,j

    最像伪代码的代码:

    import numpy as np
    def LCS(X,Y):
        table = np.zeros([len(Y)+1,len(X)+1])
        for i in range(1, len(Y)+1):
            for j in range(1,len(X)+1):
                if Y[i-1] == X[j-1]: # 这里-1是因为表格补了一行一列空元素,所以序号错开了
                    table[i,j] = table[i-1,j-1]+1
                else:
                    table[i,j] = max( table[i,j-1], table[i-1,j])
        return table
                           
    LCS("ABCBDAB","BDCABA")
    

    变一点点:如果要求连续的公共最长子序列。

    如果X[i] = Y[j]那么LCS( X[:i], Y[:j]) = LCS( X[:i-1], Y[:j-1])

    如果X[i] != Y[j]那么LCS( X[:i], Y[:j]) = 0,就相当于在这里重新开始了。

     
  • 相关阅读:
    Zabbix 3.2.1 安装 Graphtree3.0.4 或 Graphtree3.2.x
    jquery-1
    AngularJS (1)
    css-2 (Text Font)
    css
    Java经验
    js经验
    mysql经验
    MySQL 函数
    jquery 效果
  • 原文地址:https://www.cnblogs.com/the-wolf-sky/p/10353170.html
Copyright © 2020-2023  润新知