算法:動態規劃(詳解及例題)

        最近在刷LeetCode時看到了一道動態規劃的題,一些大神的題解很巧妙,這裏複習一下動態規劃和經典問題,順便分享給大家這道題。


1. 簡介

        動態規劃(Dynamic Programming)算法是五種常見的算法之一,通常用於求解具有某種最優性質的問題。動態規劃算法與分治法類似,其基本思想也是將待求解問題分解成若干個子問題。與分治法不同的是,適合於用動態規劃求解的問題,經分解得到子問題往往不是互相獨立的。若用分治法來解這類問題,則分解得到的子問題數目太多,有些子問題被重複計算了很多次。

       動態規劃主要用於求解以時間劃分階段的動態過程的優化問題,動態規劃程序設計是對解最優化問題的一種途徑、一種方法,而不是一種特殊算法。它往往是針對一種最優化問題,由於各種問題的性質不同,確定最優解的條件也互不相同,因而動態規劃的設計方法對不同的問題,有各具特色的解題方法,而不存在一種萬能的動態規劃算法,可以解決各類最優化問題。

2. 適用條件

       任何思想方法都有一定的侷限性,超出了特定條件,它就失去了作用。,適用動態規劃的問題必須滿足最優化原理無後效性

  • 最優化原理
           即最優子結構性質,最優化原理可這樣闡述:一個最優化策略具有這樣的性質,不論過去狀態和決策如何,對前面的決策所形成的狀態而言,餘下的諸決策必須構成最優策略。簡而言之,一個最優化策略的子策略總是最優的。一個問題滿足最優化原理又稱其具有最優子結構性質。
  • 無後效性
           將各階段按照一定的次序排列好之後,對於某個給定的階段狀態,它以前各階段的狀態無法直接影響它未來的決策,而只能通過當前的這個狀態。換句話說,每個狀態都是過去歷史的一個完整總結。這就是無後向性,又稱爲無後效性。

       動態規劃將原來具有指數級時間複雜度的搜索算法改進成了具有多項式時間複雜度的算法。其中的關鍵在於解決冗餘,這是動態規劃算法的根本目的。動態規劃實質上是一種以空間換時間的技術,它在實現的過程中,不得不存儲產生過程中的各種狀態,所以它的空間複雜度要大於其它的算法。

3. 動態規劃算法的設計

動態規劃算法的設計主要有兩種方法:

  1. 自頂向下(又稱記憶化搜索、備忘錄):基本上對應着遞歸函數實現,從大範圍開始計算,要注意不斷保存中間結果,避免重複計算

  2. 自底向上(遞推):從小範圍遞推計算到大範圍

4. 經典例題

       動態規劃在編程中常用解決最長公共子序列問題、矩陣連乘問題、凸多邊形最優三角剖分問題、電路佈線等問題。

4.1 最長公共子序列問題(LCS)

       最長公共子序列問題即LCS問題,是一個十分實用的問題,它可以描述兩段文字之間的“相似度”,即它們的雷同程度,從而能夠用來辨別抄襲。對一段文字進行修改之後,計算改動前後文字的最長公共子序列,將除此子序列外的部分提取出來,這種方法判斷修改的部分,往往十分準確。

       如給定兩個字符串A和B,長度分別爲m和n,要求找出它們最長的公共子序列,並返回其長度。例:
         A = “HelloWorld”
         B = “loop”
則A與B的最長公共子序列爲 “loo”,返回的長度爲3。 
       下面是從乖乖的函數 處拿的圖很清晰的概括了算法流程:
在這裏插入圖片描述

''''最長公共子序列'''
def LCS(string1,string2):
    len1 = len(string1)
    len2 = len(string2)
    # 構建一個len1 X len2 的 0 列表
    res = [[0 for i in range(len1+1)] for j in range(len2+1)]
    for i in range(1,len2+1):
        for j in range(1,len1+1):
            # 若當前string2[i-1]處值爲string1[j-1]
            if string2[i-1] == string1[j-1]:
                res[i][j] = res[i-1][j-1]+1
            else:
                # 若當前string2[i-1]處值不爲string1[j-1]
                res[i][j] = max(res[i-1][j],res[i][j-1])
    return res,res[-1][-1]
print(LCS("helloworld","loop"))

輸出:

([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 
  [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], 
  [0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2], 
  [0, 0, 0, 1, 1, 2, 2, 3, 3, 3, 3], 
  [0, 0, 0, 1, 1, 2, 2, 3, 3, 3, 3]], 3)

4.2 爬樓梯(LeetCode題)

        這道題比較淺顯易懂,能夠更好地理解動態規劃的精髓:

        假設你正在爬樓梯。需要 n 階你才能到達樓頂。每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?注意:給定 n 是一個正整數。

  • 示例 1:
    輸入: 2
    輸出: 2
    解釋: 有兩種方法可以爬到樓頂。
                    1. 1 階 + 1 階
                    2. 2 階
  • 示例 2:
    輸入: 3
    輸出: 3
    解釋: 有三種方法可以爬到樓頂。
                    1. 1 階 + 1 階 + 1 階
                    2. 1 階 + 2 階
                    3. 2 階 + 1 階

動態規劃解題思路:

第 i 階可以由以下兩種方法得到:
       在第 (i-1) 階後向上爬 1 階。
       在第 (i-2) 階後向上爬 2 階。
所以到達第 i 階的方法總數就是到第 (i−1) 階和第 (i−2) 階的方法數之和。
令 dp[i] 表示能到達第 i 階的方法總數:
       dp[i]=dp[i-1]+dp[i-2]

class Solution(object):
    def climbStairs(self, n):
        """
        :type n: int
        :rtype: int
        """
        if n==1:return 1
        elif n==2:return 2
        else:
            dp = [0 for _ in range(n)]
            dp[0], dp[1]= 1, 2
            for i in range (2,n):
                dp[i] = dp[i-1] + dp[i-2]
            return dp[n-1]

4.3 最低票價問題(LeetCode例題)

  1. 最低票價問題:

       在一個火車旅行很受歡迎的國度,你提前一年計劃了一些火車旅行。在接下來的一年裏,你要旅行的日子將以一個名爲 days 的數組給出。每一項是一個從 1 到 365 的整數。

火車票有三種不同的銷售方式:

       一張爲期一天的通行證售價爲 costs[0] 美元;
       一張爲期七天的通行證售價爲 costs[1] 美元;
       一張爲期三十天的通行證售價爲 costs[2] 美元。
       通行證允許數天無限制的旅行。 例如,如果我們在第 2 天獲得一張爲期 7 天的通行證,那麼我們可以連着旅行 7 天:第 2 天、第 3 天、第 4 天、第 5 天、第 6 天、第 7 天和第 8 天。

返回你想要完成在給定的列表 days 中列出的每一天的旅行所需要的最低消費。

  • 示例 1:
    輸入:days = [1,4,6,7,8,20], costs = [2,7,15]
    輸出:11
    解釋:
    例如,這裏有一種購買通行證的方法,可以讓你完成你的旅行計劃:
    在第 1 天,你花了 costs[0] = $2 買了一張爲期 1 天的通行證,它將在第 1 天生效。
    在第 3 天,你花了 costs[1] = $7 買了一張爲期 7 天的通行證,它將在第 3, 4, …, 9 天生效。
    在第 20 天,你花了 costs[0] = $2 買了一張爲期 1 天的通行證,它將在第 20 天生效。
    你總共花了 $11,並完成了你計劃的每一天旅行。
  • 示例 2:
    輸入:days = [1,2,3,4,5,6,7,8,9,10,30,31], costs = [2,7,15]
    輸出:17
    解釋:
    例如,這裏有一種購買通行證的方法,可以讓你完成你的旅行計劃:
    在第 1 天,你花了 costs[2] = $15 買了一張爲期 30 天的通行證,它將在第 1, 2, …, 30 天生效。
    在第 31 天,你花了 costs[0] = $2 買了一張爲期 1 天的通行證,它將在第 31 天生效。
    你總共花了 $17,並完成了你計劃的每一天旅行。
  • 提示:
    1 <= days.length <= 365
    1 <= days[i] <= 365
    days 按順序嚴格遞增
    costs.length == 3
    1 <= costs[i] <= 1000
def mincostTickets(days, costs):
    # dp數組,每個元素代表到當前天數最少錢數,爲下標方便對應,多加一個 0 位置( + 1)
    dp = [0 for _ in range(days[-1] + 1)]
    # 設定一個days指標,標記應該處理 days 數組中哪一個元素
    days_idx = 0
    for i in range(1, len(dp)):
        # 若當前天數不是待處理天數,則其花費費用和前一天相同
        if i != days[days_idx]:
            dp[i] = dp[i - 1]
        else:
            # 若 i 走到了待處理天數,則從三種方式中選一個最小的
            dp[i] = min(dp[max(0, i - 1)] + costs[0],
                        dp[max(0, i - 7)] + costs[1],
                        dp[max(0, i - 30)] + costs[2])
            days_idx += 1
    # 返回最後一天對應的費用即可
    return dp[-1]
print(mincostTickets([1,4,6,7,8,20],[2,7,15]))

輸出:

11
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章