動態規劃

1、從斐波那契數列看動態規劃

  斐波那契數列:Fn = Fn-1 + Fn-2python

  Fibonacci:除第一個和第二個數外,任意一個數可由前兩個數相加獲得。算法

一、練習:遞歸和非遞歸的方法來求解

  使用遞歸非遞歸的方法來求解斐波那契數列的第n項。app

def fibnacci(n):
    """
    遞歸版本——斐波那契函數
    :param n:
    :return:
    """
    if n == 1 or n == 2:
        return 1
    else:
        return fibnacci(n-1) + fibnacci(n-2)

# 動態規劃(DP)的思想 = 最優子結構 = 遞推式
def fibnacci_no_recurision(n):
    """
    非遞歸版本——斐波那契
    :param n:
    :return:
    """
    f = [0, 1, 1]
    if n > 2:
        for i in range(n-2):
            num = f[-1] + f[-2]
            f.append(num)
    return f[n]


# print(fibnacci(100))  # 須要計算好久
print(fibnacci_no_recurision(100))   # 354224848179261915075

二、遞歸速度問題——子問題重複計算

  經過上例能夠發現遞歸版原本計算斐波那契速度比非遞歸慢不少不少。函數

  這是因爲相同的問題算了不少遍,致使速度很慢。spa

爲何遞歸很慢:子問題的重複計算
f(5) = f(4)+f(3)
f(4) = f(3)+f(2)
f(3) = f(2)+f(1)
f(2) = 1
f(1) = 1

  而非遞歸版本就用到了  動態規劃(DP)的思想 = 最優子結構 = 遞推式 + 重複子問題3d

2、鋼條切割問題

  某公司出售鋼條,出售價格與鋼條長度之間的關係以下表:blog

  

  問題:現有一段長度爲n的鋼條和上面的價格表,求切割鋼條方案,使得總收益最大。遞歸

一、長度是n的鋼條切割方案

(1)長度爲4的鋼條全部的切割方案以下所示:(c方案最優)

  

(2)思考:長度爲n的鋼條的不一樣切割方案有幾種?

  答:長度爲n的鋼條有n-1個能夠切割的位置。每一個能夠切割的位置,都有切和不切兩種選擇。所以切割方案有2n-1個。ci

    但若是過b\d這樣的狀況看作是一種方案,這就比較難了,在組合數學裏的叫整數分割問題。字符串

(3)鋼條長度和能賣出的最高價格關係以下所示:

  

  好比鋼條長度是4,總體賣的話價格是9,但若是切成2+2,則能夠賣出10的價格;若是鋼條長度是9,總體賣出價格是24,分拆爲6+3,6最高可賣17,3最高可賣8,所以9能夠賣出25的價格。

二、遞推式

  設長度爲n的鋼條切割後最優收益值爲rn,能夠得出遞推式: 

  rn = max(pn, r1 + rn-1, r2 + rn-2, ..., rn-1 + r1

  • 第一個參數pn表示不切割的價格。
  • 其餘n-1個參數分別表示另外n-1種不一樣切割方案,對方案i=1,2,...,n-1
    • 將鋼條切割爲長度爲i和n-i兩段
    • 方案i的收益爲切割兩段的最優收益之和
  • 考察全部的i,選擇其中收益最大的方案。

三、最優子結構

  能夠將求解規模爲n的原問題,劃分爲規模更小的子問題:完成一次切割後,能夠將產生的兩段鋼條當作兩個獨立的鋼條切割問題

  組合兩個子問題的最優解,並在全部可能的兩段切割方案中選取組合收益最大的,構成原問題的最優解。

  鋼條切割知足最優子結構:問題的最優解由相關子問題的最優解組合而成,這些子問題能夠獨立求解。

四、遞歸求解簡化

  鋼條切割問題還存在更簡單的遞歸求解方法:

  • 從鋼條的左邊切割下長度爲i的一段,只對右邊剩下的一段繼續進行切割,左邊的再也不切割。
  • 遞推式簡化爲
  • 不作切割的方案就能夠描述爲:左邊一段長度爲n,收益爲pn,剩餘一段長度爲0,收益爲r0=0。

五、鋼條切割代碼實現

(1)自頂向下實現 

import time


def cal_time(func):
    def wrapper(*args, **kwargs):
        t1 = time.time()
        result = func(*args, **kwargs)
        t2 = time.time()
        print("%s running time: %s secs." % (func.__name__, t2 - t1))
        return result

    return wrapper


# 鋼條長度對應的價格
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40]


def cut_rod_recurision_1(p, n):
    """
    鋼條切割——遞歸版(兩邊切割)
    :param p: 鋼條價格
    :param n: 鋼條長度
    :return:
    """
    if n == 0:   # 鋼條不存在
        return 0
    else:
        res = p[n]
        for i in range(1, n):
            res = max(res, cut_rod_recurision_1(p, i) + cut_rod_recurision_1(p, n-i))
        return res


@cal_time
def c1(p, n):
    return cut_rod_recurision_1(p, n)


def cut_rod_recurision_2(p, n):
    """
    鋼條切割——遞歸版(只一邊切割)
    :param p:鋼條價格
    :param n:鋼條長度
    :return:
    """
    if n == 0:
        return 0
    else:
        res = 0
        for i in range(1, n+1):   # 從1到n,即1~n+i
            res = max(res, p[i] + cut_rod_recurision_2(p, n-i))
        return res


@cal_time
def c2(p, n):
    return cut_rod_recurision_2(p, n)


print(cut_rod_recurision_1(p, 9))
# 25

print(c1(p, 10))
print(c2(p, 10))  # 因爲每次都少遞歸一次,效率高了不少
"""
c1 running time: 0.013891935348510742 secs.
27
c2 running time: 0.0005898475646972656 secs.
27
"""

  自頂向下遞歸實現的效率這麼差,緣由分析:

  

  僅分析n=4的狀況就發現有大量相同子問題重複求解。

  遞歸算法因爲重複求解相同子問題,效率極低。時間複雜度:O(2n)

(2)自底向上實現

  動態規劃的思想:

  • 每一個子問題只求解一次,保存求解結果
  • 以後須要此問題時,只需查找保存的結果
@cal_time
def cut_rod_dp(p, n):
    """
    鋼條切割——動態規劃
    :param p:鋼條價格
    :param n:鋼條長度
    :return:
    """
    r = [0]   # 長度爲0時,最優收益爲0
    for i in range(1, n+1):  # 計算出r1,r2,...,rn的最優收益
        res = 0   # ri最小值默認是0
        for j in range(1, i+1):  # ri的i種方案
            res = max(res, p[j] + r[i-j])
        r.append(res)  # 添加最優值
    return r[n]


print(cut_rod_dp(p, 10))
print(cut_rod_dp(p, 20))
"""
cut_rod_dp running time: 2.7894973754882812e-05 secs.
27
cut_rod_dp running time: 7.104873657226562e-05 secs.
56
"""

  時間複雜度:O(n2),這是因爲每一個都是直接去取以前存好的值,而不是把值再算一遍,以下圖所示:

  

六、重構解

  如何修改動態規劃算法,使其不只輸出最優解,還輸出最優切割方案?

  針對每一個子問題,保存切割一次時左邊切下的長度。

  

  上表中,長度爲i,r[i]是對應長度時最優收益,s[i]是左邊切下的長度。

# 鋼條長度對應的價格
# p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]

def cut_rod_extent(p, n):
    """重構解——輸出最優切割方案"""
    r = [0]  # 最優解
    s = [0]  # 左邊切下的長度
    for i in range(1, n + 1):
        res_r = 0  # 價格最大值
        res_s = 0  # 價格最大值對應方案左邊不切割部分長度
        for j in range(1, i + 1):
            if p[j] + r[i - j] > res_r:
                res_r = p[j] + r[i - j]
                res_s = j
        r.append(res_r)  # 添加最優值
        s.append(res_s)
    return r[n], s

r, s = cut_rod_extent(p, 10)
print(s)   # 輸出左邊切下的長度
"""
[0, 1, 2, 3, 2, 2, 6, 1, 2, 3, 10]
"""

def cut_rod_solution(p, n):
    """重構解——輸出結果"""
    r, s = cut_rod_extent(p, n)
    ans = []   # 保存最後切成的樣子
    while n > 0:
        ans.append(s[n])  # 保存左邊切下的長度
        n -= s[n]    # 剩下的長度
    return ans

print(cut_rod_solution(p, 9))  # [3, 6]

  若是使用以前的鋼條長度價格表:p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40]

print(cut_rod_dp(p, 20))       # 56
print(cut_rod_solution(p, 20))  # [2, 6, 6, 6]
r, s = cut_rod_extent(p, 20)
print(s)   # [0, 1, 2, 3, 2, 2, 6, 1, 2, 3, 2, 2, 6, 1, 2, 3, 2, 2, 6, 1, 2]

3、動態規劃問題關鍵特徵

  什麼問題可使用動態規劃方法?

(1)最優子結構

  • 原問題的最優解中涉及多少個子問題
  • 在肯定最優解使用哪些子問題時,須要考慮多少種選擇

(2)重疊子問題

4、最長公共子序列(LCS)

  一個序列的子序列是在該序列中刪去若干元素後獲得的序列。例如:「ABCD」和「BDF」都是「ABCDEFG」的子序列。

  最長公共子序列(Longest Common Subsequence,簡寫LCS)問題:給定兩個序列X和Y,求X和Y長度最大的公共子序列。例如:X=「ABBCBDE」, Y="DBBCDB", LCS(X,Y)="BBCD"

  應用場景:字符串類似度比對、基因比對

一、LCS的最優子結構原理

  令X=<x1,x2,...,xm>和Y=<y1,y2,...,yn>爲兩個序列,Z=<z1,z2,...,zk>爲X和Y的任意LCS。

  1.若是xm=yn,則zk=xm=yn且Zk-1是Xm-1和Yn-1的一個LCS。

  2.若是xm≠yn,那麼zk≠xm意味着Z是Xm-1和Y的一個LCS。

  3.若是xm≠yn,那麼zk≠yn意味着Z是X和Yn-1的一個LCS。

二、最優解的遞推式

  

  c[i,j]表示Xi和Yj的LCS長度。

 三、示例及解析

  例如:要求a="ABCBDAB"與b="BDCABA"的LCS:

  因爲最後一位"B"≠"A",所以LCS(a,b)應該來源於LCS(a[:-1],b)與LCS(a,b[:-1])中的一個。意思就是最後一位不同,要不去除a的最後一位,要不去除b的最後一位。

  

 四、代碼實現

def lcs_length(x, y):  # x,y是列表或字符串
    # 查看x,y的長度
    m = len(x)
    n = len(y)

    # 用二維列表生成式生成一個m+1行,n+1列的二維列表
    c = [[0 for _ in range(n+1)] for _ in range(m+1)]

    for i in range(1, m+1):
        for j in range(1, n+1):
            if x[i-1] == y[j-1]:    # i,j位置上的字符匹配的時候,來自於左上方+1
                c[i][j] = c[i-1][j-1] + 1             # 若i,j>0,且xi==yi
            else:
                c[i][j] = max(c[i-1][j], c[i][j-1])   # 若i,j>0,且xi!=yi

    for _ in c:   # 將列表c逐行打印
        print(_)

    return c[m][n]


# print(lcs_length("ABCBDAB", "BDCABA"))   # 4
"""
[0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 1, 1, 1]
[0, 1, 1, 1, 1, 2, 2]
[0, 1, 1, 2, 2, 2, 2]
[0, 1, 1, 2, 2, 3, 3]
[0, 1, 2, 2, 2, 3, 3]
[0, 1, 2, 2, 3, 3, 4]
[0, 1, 2, 2, 3, 4, 4]
"""


def lcs(x, y):
    """添加方向"""
    m = len(x)
    n = len(y)
    c = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
    b = [[0 for _ in range(n + 1)] for _ in range(m + 1)]   # 1:左上方  2:上方  3:左方

    for i in range(1, m+1):
        for j in range(1, n+1):
            if x[i-1] == y[j-1]:   # i,j位置上的字符匹配的時候,來自於左上方+1
                c[i][j] = c[i-1][j-1] + 1
                b[i][j] = 1
            elif c[i-1][j] >= c[i][j-1]:   # 來自於上方(這裏把等於也偏向上)
                c[i][j] = c[i-1][j]
                b[i][j] = 2
            else:    # 來自於左方
                c[i][j] = c[i][j-1]
                b[i][j] = 3
    return c[m][n], b


c, b = lcs("ABCBDAB", "BDCABA")
for _ in b:
    print(_)
"""
[0, 0, 0, 0, 0, 0, 0]
[0, 2, 2, 2, 1, 3, 1]
[0, 1, 3, 3, 2, 1, 3]
[0, 2, 2, 1, 3, 2, 2]
[0, 1, 2, 2, 2, 1, 3]
[0, 2, 1, 2, 2, 2, 2]
[0, 2, 2, 2, 1, 2, 1]
[0, 1, 2, 2, 2, 1, 2]
"""


def lcs_trackback(x,y):
    """回溯"""
    c, b = lcs(x, y)
    i = len(x)
    j = len(y)
    res = []
    while i > 0 and j > 0:
        if b[i][j] == 1:   # 來自左上方=》匹配
            res.append(x[i-1])
            i -= 1
            j -= 1
        elif b[i][j] == 2:   # 來自於上方=》不匹配
            i -= 1
        else:       # ==3,來自左方=》不匹配
            j -= 1
    print(res)
    return "".join(reversed(res))


print(lcs_trackback("ABCBDAB", "BDCABA"))   # BCBA
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息