算法:动态规划(详解及例题)

        最近在刷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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章