动态规划原理介绍(附7个算例,有代码讲解)

动态规划思想

动态规划(Dynamicprogramming)是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。

动态规划算例

**算例1
来源个人其他博文
leetcode解码方法(动态规划python)
描述

有一个消息包含A-Z通过以下规则编码

‘A’ -> 1
‘B’ -> 2

‘Z’ -> 26
现在给你一个加密过后的消息,问有几种解码的方式

我们不能解码空串,因此若消息为空,你应该返回0。
消息的长度 n \leq 100n≤100

您在真实的面试中是否遇到过这个题?
样例
样例 1:

输入: “12”
输出: 2
解释: 它可以被解码为 AB (1 2) 或 L (12).
样例 2:

输入: “10”
输出: 1

思路
动态规划,从s的位置1读起,每次读取两个数,因为只要是1-9的数,都可以单独出现,因此,dp表可以初始化为1,为了避免第二个条件[i-1]无法执行,我们使得dp表从位置2开始修改,dp位置0,1的值都初始化为1

情况
#特殊情况 输入’’ 则无法解码,可直接返回 0
#输入0 则无法解码,可直接返回 0
#若为 00 或 30、40、50… 则无法解码,可直接返回 0
#为 10、20 的情况,则 i 处字符必须与前一位结合,则为双字符解码
dp[i + 1] = dp[i - 1]
若数字为10-26:既可以单字解码,又可以双字解码 dp[i + 1] = dp[i] + dp[i - 1]
#1-9,27-29、31-39、41~49…只能单字解码 dp[i + 1] = dp[i]

class Solution:
    def numDecodings(self, s):
        if len(s)==0: return 0 #特殊情况 输入'' 则无法解码,可直接返回 0
        if s[0]=="0":return 0 #输入0 则无法解码,可直接返回 0
        dp = [0] * (len(s) + 1)
        dp[0],dp[1]=1,1

        for i in range(1, len(s)):
            if s[i] == '0':
                if int(s[i - 1]) > 2 or int(s[i - 1]) == 0: #若为 00 或 30、40、50... 则无法解码,可直接返回 0;
                    return 0
                dp[i + 1] = dp[i - 1] #为 10、20 的情况,则 i 处字符必须与前一位结合,则为双字符解码
            elif 9 < int(s[i - 1:i + 1]) < 27:
                dp[i + 1] = dp[i] + dp[i - 1] #既可以单字解码,又可以双字解码
            else:
                dp[i + 1] = dp[i]#1-9,27~29、31~39、41~49....只能单字解码
        return dp[-1] 
test = Solution()
s_li = ["9", "226", "300"]
for s in s_li:
    print(test.numDecodings(s))

算例2:

来源个人博文
leetcode最大矩形 (动态规划 python)

描述

给你一个二维矩阵,权值为False和True,找到一个最大的矩形,使得里面的值全部为True,输出它的面积

您在真实的面试中是否遇到过这个题?
样例
样例1

输入:
[
[1, 1, 0, 0, 1],
[0, 1, 0, 0, 1],
[0, 0, 1, 1, 1],
[0, 0, 1, 1, 1],
[0, 0, 0, 0, 1]
]
输出: 6
样例2

输入:
[
[0,0],
[0,0]
]
输出: 0

解题思路

假设我们输入的矩形第一行数时是:
[1, 1, 1, 0, 1,1,0,1,1]
我们统计这行的最大宽度:
得到数组a如下:
a=1,2,3,0,1,2,0,1,2]

正式解题:
1.输入矩形
[
[1, 1, 0, 0, 1],
[0, 1, 0, 0, 1],
[0, 0, 1, 1, 1],
[0, 0, 1, 1, 1],
[0, 0, 0, 0, 1]
]
maxarea=0
2.然后对矩形的第一行统计最大宽度
得到宽度数组:
[1,2,0,0,1]
计算最大面积=2
更新maxarea
3.然后对矩形的第二行统计最大宽度:
得到宽度数组:
[0, 1, 0, 0, 1]
计算面积:分两步
第一步:计算当前宽度数组最大面积为1 如果大于maxarea 则更新maxarea
第二步 :计算当前宽度数组和上一宽度数组产生的最大面积:
最大面积应为同一列数的(最小值) 乘以 (行数)
结果为1乘以2=2
如果大于maxarea 则更新maxarea
4.然后对矩形的第三行统计最大宽度:
得到宽度数组[0, 0, 1, 1, 1]
在计算这行产生的最大面积时。
第一步只把第三行宽度数组,计算只有第三行宽度度数组产生的最大面积:结果1 如果大于maxarea 则更新maxarea
第二步: 计算(第三行宽度数组,第二行宽度数组)最大面积 2
如果大于maxarea 则更新maxarea
第三步:最后在计算(第三行宽度数组,第二行宽度数组 ,第一行宽度数组)的最大面积。如果大于maxarea 则更新maxarea

后面类似:

整个过程应该是个动态:
代码如下:

lass Solution:
	def maxmalRectangle(self , matrix):
		m = len(matrix)
		n = len(matrix[0])
		maxarea = 0
		dp = [[0]*n for _ in range(m)]
		for i in range(m):#遍历每一列
			for j in range(n):#遍历每一行
				if matrix[i][j] == 0:
					continue
				width = dp[i][j] = dp[i][j-1] + 1 if j else 1#计算最大宽度并使用它更新dp
				for k in range(i,-1,-1):#倒序遍历每一行
					width = min(width , dp[k][j])#最小宽度(同一列 上下几行的最小【最大宽度】)
					maxarea = max(maxarea , width*(i - k + 1)) #同一列上下几行的最小【最大宽度】*行数=当前行最大面积
		return maxarea
test = Solution()
d=test.maxmalRectangle([
  [1, 1, 0, 0, 1],
  [0, 1, 0, 0, 1],
  [0, 0, 1, 1, 1],
  [0, 0, 1, 1, 1],
  [0, 0, 0, 0, 1]
])
print(d)

算例3
来源于个人博客
LeetCode最大子序和 (动态规划)python
描述
给定一个整数数组,找到一个具有最大和的子数组,返回其最大和。

子数组最少包含一个数

您在真实的面试中是否遇到过这个题?
样例
样例1:

输入:[−2,2,−3,4,−1,2,1,−5,3]
输出:6
解释:符合要求的子数组为[4,−1,2,1],其最大和为 6。
样例2:

输入:[1,2,3,4]
输出:10
解释:符合要求的子数组为[1,2,3,4],其最大和为 10。

思路 动态规划
设sum[i]为以第i个元素结尾的最大的连续子数组的和。假设对于元素i,所有以它前面的元素结尾的子数组的长度都已经求得,那么以第i个元素结尾且和最大的连续子数组实际上,要么是以第i-1个元素结尾且和最大的连续子数组加上这个元素,要么是只包含第i个元素,即sum[i]= max(sum[i-1] + a[i], a[i])。可以通过判断sum[i-1] + a[i]是否大于a[i]来做选择。因为中间有的sum[i]已经领过加负值变为0啦,后面的sum[i]求和就得根据后面新的数重新计算。 所以最大子序和就是sum[]数组的最大值

代码1

class Solution:
    """
    @param nums: A list of integers
    @return: A integer indicate the sum of max subarray
    """
    def maxSubArray(self, nums):
        # write your code here
        length=len(nums) 
        if length==0:
            return 0
        for i in range(1,length):  
            #当前值的大小与前面的值之和比较,若当前值更大,则取当前值,舍弃前面的值之和  
            MaxSum=max(nums[i]+nums[i-1],nums[i])  
            nums[i]=MaxSum#将当前和最大的赋给nums[i],新的nums存储的为和值  
        return max(nums)
nums=[-2,2,-3,4,-1,2,1,-5,3]

c=Solution()
d=c.maxSubArray(nums)
print(d)

代码2:

class Solution:
    """
    @param nums: A list of integers
    @return: A integer indicate the sum of max subarray
    """
    def maxSubArray(self, nums):
        # write your code here
        if len(nums)==0:
            return 0
        presum=maxsum=nums[0]
        #presum当前和,maxsum最好的和
        for i in range(1,len(nums)):
            presum=max(presum+nums[i],nums[i])
            maxsum=max(maxsum,presum)
        return maxsum

nums=[-2,2,-3,4,-1,2,1,-5,3]

c=Solution()
d=c.maxSubArray(nums)
print(d)

算例4
来源于个人博客
LeetCode 392打劫房屋 python
描述
假设你是一个专业的窃贼,准备沿着一条街打劫房屋。每个房子都存放着特定金额的钱。你面临的唯一约束条件是:相邻的房子装着相互联系的防盗系统,且 当相邻的两个房子同一天被打劫时,该系统会自动报警。

给定一个非负整数列表,表示每个房子中存放的钱, 算一算,如果今晚去打劫,在不触动报警装置的情况下, 你最多可以得到多少钱 。

您在真实的面试中是否遇到过这个题?
样例
样例 1:

输入: [3, 8, 4]
输出: 8
解释: 仅仅打劫第二个房子.
样例 2:

输入: [5, 2, 1, 3]
输出: 8
解释: 抢第一个和最后一个房子

动态规划求解
考虑前i项的结果dp[i]时,

dp[i]为到达第i个房间时,得到的最大收益,A为房间钱数组。

当i = 1, 返回dp[0] = A[0]

当i = 2, 返回dp[1] = max(A[0], A[1])

当i = 3, 分为偷3号房屋和不偷3号房屋,

偷的情况下, 2号房间就不能偷了,结果为A[2] + dp[0]

不偷的情况下,结果为dp[1]

所以返回dp[2] = max(dp[0] + A[2], dp[1])

以此类推,dp[i] = max(dp[i-2] + A[i], dp[i-1])

class Solution:
    """
    @param A: An array of non-negative integers
    @return: The maximum amount of money you can rob tonight
    """
    def houseRobber(self, A):
        # write your code here
        if not A:
            return 0
        dp = [0 for _ in A]
        dp[0] = A[0]
        for i in range(1, len(A)):
            if i == 1:
                dp[i] = max(dp[0], A[i])
            else:
                dp[i] = max(dp[i - 2] + A[i], dp[i - 1])
        return dp[-1]
c=Solution()
d=c.rob([3,8,4,13])
print(d)

算例5
来源于个人博客
Leetcode 534打劫房屋II python
描述

在上次打劫完一条街道之后,窃贼又发现了一个新的可以打劫的地方,但这次所有的房子围成了一个圈,这就意味着第一间房子和最后一间房子是挨着的。每个房子都存放着特定金额的钱。你面临的唯一约束条件是:相邻的房子装着相互联系的防盗系统,且 当相邻的两个房子同一天被打劫时,该系统会自动报警。

给定一个非负整数列表,表示每个房子中存放的钱, 算一算,如果今晚去打劫,在不触动报警装置的情况下, 你最多可以得到多少钱 。

这题是House Robber的扩展,只不过是由直线变成了圈

您在真实的面试中是否遇到过这个题?
样例
样例1

输入: nums = [3,6,4]
输出: 6
样例2

输入: nums = [2,3,2,3]
输出: 6

思路:

动态规划求解
(1)不考虑第一间房子和最后一间房子是挨着的时
考虑前i项的结果dp[i]时,

dp[i]为到达第i个房间时,得到的最大收益,A为房间钱数组。

当i = 1, 返回dp[0] = A[0]

当i = 2, 返回dp[1] = max(A[0], A[1])

当i = 3, 分为偷3号房屋和不偷3号房屋,

偷的情况下, 2号房间就不能偷了,结果为A[2] + dp[0]

不偷的情况下,结果为dp[1]

所以返回dp[2] = max(dp[0] + A[2], dp[1])

以此类推,dp[i] = max(dp[i-2] + A[i], dp[i-1])
(2)考虑第一间房子和最后一间房子是挨着时

区别在于1号房屋和最后一号房屋只能二选一
即把1号房间舍弃,或者最后一号房间舍弃。在这两种情况下选最优。

class Solution:
    """
    @param nums: An array of non-negative integers.
    @return: The maximum amount of money you can rob tonight
    """

    def houseRobber(self, nums):
        # write your code here
        if len(nums) == 1:
            return nums[0]
        return max(self.houseRobber2(nums[1:]), self.houseRobber2(nums[:-1]))

    def houseRobber2(self, nums):
        if len(nums) == 0:
            return 0
        dp = [0 for _ in nums]
        dp[0] = nums[0]
        for i in range(1, len(nums)):
            if i == 1:
                dp[i] = max(nums[0], nums[i])
            else:
                dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
        return dp[-1]

c=Solution()
d=c.houseRobber([2,3,2,3])
print(d)

算例6
来源于个人博客
leetcode装最多水的容器383
描述
给定 n 个非负整数 a1, a2, …, an, 每个数代表了坐标中的一个点 (i, ai)。画 n 条垂直线,使得 i 垂直线的两个端点分别为(i, ai)和(i, 0)。找到两条线,使得其与 x 轴共同构成一个容器,以容纳最多水。

容器不可倾斜。

样例 :

输入: [1, 3, 2, 2]
输出: 4
解释:
选择 a1, a2, 容量为 1 * 1 = 1
选择 a1, a3, 容量为 1 * 2 = 2
选择 a1, a4, 容量为 1 * 3 = 3
选择 a2, a3, 容量为 2 * 1 = 2
选择 a2, a4, 容量为 2 * 2 = 4
选择 a3, a4, 容量为 2 * 1 = 2

#解题思路:
这题等于说求面积最多。假设我们有两根柱子,命名为left和right。left和right对应横坐标。它们形成的面积应为
ans=(right-left)*min(height[left],height[right]).
求解时我们应该不断更新这个数据:
ans=max(ans,(right-left)*min(height[left],height[right]))。
开始我们的迭代。
(1)面积ans=max(ans,(right-left)*min(height[left],height[right])).
(2)left从左往右走,rgiht同时 从右往左走.
(3)比较left与right的高度,高的先不动,移动矮的。
迭代结束条件为left和right指向同一点。

class Solution(object):
    def maxArea(self,height):
        ans=left=0
        right=len(height)-1
        while left<right:
            ans=max(ans,(right-left)*min(height[left],height[right]))
            if height[left]<=height[right]:
                left+=1
            else:
                right-=1
        return ans
     
c=Solution()
a=[1, 3, 2]
d=c.maxArea(a)
print(d)

结果2

算例7
来源于个人博客
leetcode111 爬楼梯 python实现
动态规划类题目
描述

假设你正在爬楼梯,需要n步你才能到达顶部。但每次你只能爬一步或者两步,你能有多少种不同的方法爬到楼顶部?

您在真实的面试中是否遇到过这个题?
样例
Example 1:
Input: n = 3
Output: 3

Explanation:

  1. 1, 1, 1
  2. 1, 2
  3. 2, 1
    total 3.
    1
    2
    3
    4
    5
    Example 2:
    Input: n = 1
    Output: 1

Explanation:
only 1 way.
1
2
思路:上第I级台阶的方法总共有两大种,第一种是从第(i-1)级台阶上跨一级,第二种则是从第(I-2)级台阶上跨两级,除此并无他法,由于这个上楼方法从(i-1)和(i-2)级各只有一种方法,那么上第i级台阶的总的方法和数就是上前两级台阶的方法总和数

思路递归代码

class Solution:
    """
    @param n: An integer
    @return: An integer
    """
    def climbStairs(self, n):
        # write your code here
        if n==0:
            return 0
        if n==1:
             return 1
        pre,ppre=1,1#  pre,ppre前时刻,前2时刻
        for i in range(2,n+1):
            tmp = pre#tmp 用于pre ,与ppre 完成数值改变
            pre = pre + ppre#当前等于前面两步相加
            ppre = tmp#前一时刻变成前前时刻
        return pre

来源 @Author: yudengwu

猜你喜欢

转载自blog.csdn.net/kobeyu652453/article/details/106896988