刷题不会系列——动态规划,一文带你走天下

前言

首先说明下什么情况下会使用到动态规划来解决问题。简而言之,你看到一个问题,第一反应就是这问题能穷举,然后比较得出答案!但是,在穷举过程中我们不是一个路径一个路径的重新找,而是针对前面曾遍历过的信息通过创建一个n维列表存储起来,当我们列举新的路径的时候可以再重复利用遍历过的信息,从而不断扩展和维护列表。这就是动态规划的核心思想了。
因此,针对子数组、寻找路径、找零钱等问题,我们都可以采用这一思想来解决。话不多说,直接上题手撕。

1. 连续子数组的最大和

由浅入深,首先看Leecode简单难度的一道动态规划题目:连续子数组的最大和。网址如下:
连续子数组的最大和
PS:大家可以看题目要求及其它语言的代码,本文主要分析python版本代码。
思路分析:
本问题你一看,可以通过穷举出所有子集然后比较加和大小,那就可以用动态规划来实现。
首先说下动态规划问题的官话流程:确定子问题;确定状态;确定状态转移方程;确定边界条件;确定实现方式;确定优化方法
子问题就是数组子集,状态就是子集连续元素加和。
因为问题是一维的,所以可以通过维护一个一维数组来解决此问题,可以新建一个辅助数组来解决此问题,也可以直接用给出的数组来做。因为是连续子数组,可以将状态num[i]定义为到这个点为止的连续子数组最大和,因此状态转移方程可以定义为: n u m s [ i ] = m a x ( n u m s [ i − 1 ] + n u m s [ i ] , n u m s [ i ] ) nums[i] = max(nums[i-1]+nums[i], nums[i]) nums[i]=max(nums[i1]+nums[i],nums[i])
因为第0个数最大和肯定为其本身,因此从第一个数开始循环。
至此代码如下:

for i in range(1,len(nums)):
	nums[i] = max(nums[i-1]+nums[i], nums[i])
return (max(nums))

2. 打家劫舍

再来看一道经典的入门题目:House Robber (小偷问题)。
打家劫舍
思路分析:
因为问题输入是一维的,原输入数组为nums,同时构建一个辅助的一维数组dp来解决此问题。和上个题类似,dp[n]代表前 n间能偷窃到的最高金额。此时最难的就是分析状态转移方程。首先本题有个限制条件:不能偷窃相邻的房屋,因此我们想知道的第n个房间可以由以下几种情况得到:
1.抢了第n间和它相隔的前面一间,那么dp[n] = dp[n-2] + nums[n];#nums[n]为当前房屋的价值
2.没抢第n间,那么dp[n] = dp[n-1] ;
3.没抢第n-1和n:首先不可能,因为我们所求dp[n]和dp[n-1]为最后两间且nums[n-1]和nums[n]均大于0,其次没抢n的话,dp[n-1] = dp[n-2],此时dp[n] = dp[n-1] + nums[n] = dp[n-2] + nums[n],同1。
因此,最后状态转移方程可以定义为: d p [ n ] = m a x ( d p [ n − 2 ] + n u m s [ n ] , d p [ n − 1 ] ) dp[n] = max(dp[n-2]+nums[n], dp[n-1]) dp[n]=max(dp[n2]+nums[n],dp[n1])
至此代码如下:

m = len(nums)
dp = [0 for _ in range(m)]
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2,m):
    dp[i] = max(dp[i-1],dp[i-2]+nums[i])
print(dp[m-1]) 

此时时间复杂度O(N),空间复杂度O(N),可以通过定义两个变量m和n交替记录,将空间复杂度降到 O(1)。m定义为dp[n-1],n定义为dp[n-2],每次运算左边向后移位一次。
至此代码如下:

m, n = 0, 0
for i in nums:
	m, n = max(n + i, m), m
return m

3. 不同路径

再来看Leecode中等难度的一道动态规划题目,此题进阶为二维然后可以优化为一维。
不同路径

思路分析:
首先此题一看就知道可以穷举出来,所以就可以用动态规划来优化。

  1. 首先建立辅助矩阵dp[i][j]表示到达当前位置有几条路径;
  2. 建立状态转移方程即想到扩展列表的思路,本题中机器人每次只能向下或者向右移动一步,所以新的点有多少种路径,等于上面和左面两点的和,即: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − 1 ] dp[i][j] = dp[i-1][j] + dp[i][j-1] dp[i][j]=dp[i1][j]+dp[i][j1]
  3. 填充列表中已知数据。对于第一行和第一列的数据,只有一条路能到达,即向右和向下直接走,因此可填充: d p [ i ] [ 0 ] = 1 , d p [ 0 ] [ j ] = 1 dp[i][0]=1,dp[0][j]=1 dp[i][0]=1,dp[0][j]=1

至此代码如下:

// 无优化直接循环
dp = [[0 for i in range(n)] for j in range(m)]
for i in range(m):
	dp[i][0] = 1
for j in range(n):
	dp[0][j] = 1
for i in range(1,m):
	for j in range(1,n):
		dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]

至此题已经算是完成了,但是本着题完优化不会完的原则,对题目进行空间复杂度和时间复杂度的优化。目前时间复杂度为O(mn),空间复杂度为O(mn)。但是考虑到每点的值仅和上面左面有关联,因此可以通过仅维护一行或一列的值降低空间复杂度。直观点理解,比如我们维护一行,就是通过循环对每一行进行更新,一直到第m行后第n个值即为所求。

至此代码如下:

// 优化循环
dp = [1 for i in range(n)]
for i in range(1,m):
	for j in range(1,n):
		dp[j] += dp[j-1]
return dp[n-1]

此时时间复杂度为O(m*n),空间复杂度为O(n)。over收工。

4. 零钱兑换

下面在看一道经典的动态规划题目:找零问题,找零问题因为硬币可以使用无限多个,所以是完全背包问题,对于0-1背包和多重背包,本文暂且略过。
零钱兑换
完全背包相比于0-1背包和多重背包,三者非常相似,最大区别在于完全背包是从小到大枚举金额,而另外两者相反。为了方便快速理解,通过表格来分析例题:硬币为[1,2,5],总数为11,穷举表格如下:

01234567891011
01234567891011
011223344556
011221223323

三行为硬币种类,及第一行为只有1,第二行有1和2,第三行为1,2,5。列为找零数。若维护二维数组,则就是此列表,而一维数组,则就是一行,直观理解就是随着硬币种类增加,维护的这一行逐渐下降一直到最后一行。从公式上分析,二维状态转移方程为: d p [ i ] [ j ] = m i n ( d p [ i − 1 ] d p [ j ] , d p [ i ] [ j − c ] + 1 ) dp[i][j] = min(dp[i-1]dp[j], dp[i][j-c]+1) dp[i][j]=min(dp[i1]dp[j],dp[i][jc]+1)只与上一行同列和同一行前c列有关,而一维情况下下降一行则为本元素,因此可以很直观的优化降维状态方程: d p [ j ] = m i n ( d p [ j ] , d p [ j − i ] + 1 ) dp[j] = min(dp[j], dp[j-i]+1) dp[j]=min(dp[j],dp[ji]+1)
至此代码如下:

// 优化循环
dp = [float('inf') for i in range(amount+1)]
dp[0] = 0
for c in coins:
    for j in range(c,amount+1):
        dp[j] = min(dp[j], dp[j-c]+1)
return dp[-1] if dp[-1] != float('inf') else -1

5. 青蛙过河

青蛙过河
这道题本身思路不难,状态转移方程很容易就能想到,列出这道题的原因是在题解中,有人通过哈希表做出了一个简单的方法。
首先这道题最直接的思路就是dp[i][j]表示跳 j 步能否跳到第 i 个石头。但是在写的过程中你会发现用于表示 j 的列太大了无法表示,所以采用哈希表来解决这个问题,即在dp[i]每个点采用set()或者dict()形式存储,此处采用set(),即dp[i][j]为True时,将 j 值存储在dp[i]中。最后判断dp[-1]是否为空则可以判断是否可以到达。
至此代码如下:

n = len(stones)
dp = [set() for i in range(n)]
dp[0].add(0)

for i in range(n):
	cur = stones[i]
	for j in range(i):
		step = cur - stones[j]
		if step - 1 in dp[j] or step in dp[j] or step + 1 in dp[j]:
			dp[i].add(step)

return len(dp[-1]) > 0

本题和前面解题思路并无多大区别,主要在于数据存储的问题,困难也在于这一点。

6. 买卖股票的最佳时机 IV

又是一道经典的题目。买卖股票在LeeCode上有很多题目,分别如下:
121:最多进行 1 笔交易(k=1)(贪心)
122:不限交易次数(k=+inf)(贪心)
309:不限交易次数(k=+inf),但有冷冻期(二维dp)
714:不限交易次数(k=+inf),但有手续费(二维dp)
123:最多进行 2 笔交易(k=2)(三维DP或者一维DP+贪心)
188:最多进行 k 次交易(三维DP或者一维DP+贪心)
这里不多赘述,只选了困难188讲一下。PS:题解中Python3有一篇讲了所有的类型,感兴趣可以看下。
买卖股票的最佳时机 IV
这里只说一下一维DP+贪心的思路,首先进行一个清晰的思路解释。首先要明白因为只能持有一支股票,所以如果进行k次交易肯定是先后的,也就是[买,卖…,买,卖]。把成本和利润创造两个一维数组分别为cost和profit,长短为k+1。更清晰的分析下:当k=1的时候,就是贪心算法寻找最小成本赚取最大利润;当k=2时,cost1和profit1分别为第一只股票的成本和利润,然后当你买第二只股票的时候,你已经赚了profit1的钱,因此成本cost2为p-profit1,而两只股票合起来的最大利润为p-cost2。因为买卖占用两天,所以当k>=n//2,会变成122题不限制交易次数。
至此代码如下:

n = len(prices)
if n <= 1: return 0
if k >= n//2:   
    profit = 0
    for i in range(1, n):
        if prices[i] > prices[i - 1]:
            profit += prices[i] - prices[i - 1]
    return profit
else:
    cost = [float('inf') for i in range(k+1)]
    profit = [0 for i in range(k+1)]
    for p in prices:
        for i in range(1,k+1):
            cost[i] = min(cost[i], p - profit[i-1])
            profit[i] = max(profit[i], p - cost[i])
    return profit[-1]

动态规划的刷题就先讲到这里啦,真正想特别熟练还是得多刷题,这里只是简单的刷题入门经典题目分享哈。又因为本人一向比较懒,各位看官觉得还可以就点赞评论,督促我写出下一篇,么~。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值