动态规划(DP 即 Dynamic Programming)
理解为将一个大问题拆分为一堆小问题,并且这些小问题不会被重复的计算。
动态规划分为自顶向下top-down(递归时间复杂度以斐波那契数为例n^h-1,空间复杂度为n)和自底向上bottom-up(效率高,最优解,一般一维dp使用滚动数组来优化)。
动态规划的解题步骤:
1:状态定义(定义dp数组)2:转移方程(拆分问题确定dp数组自底向上的转移方程)3:初始状态(确定最开始底部的dp数值):4:返回值(返回dp数组当中的哪一个值为最终结果)
以斐波那锲数为例
如果使用自顶向下的方式去解题,即为递归
code:
def fib(n):
if n == 1 or n == 2:
return 1
return fib(n - 1) + fib(n - 2)
图片转自力扣
其因为会重复计算f(n-2)等小问题,导致时间复杂度为二叉树的结点数o(2^h-1) = o(2^n),使用记忆化递归法使空间复杂度为o(n)。
使用自底向上的方法:
第一步状态定义:设置dp[]一维数组,dp[i]代表第几个数
第二步转移方程:dp[n] = dp[n-1]+dp[n-2]代表第n个数的转移方程
第三步初始状态:dp[0] = 0 dp[1] = 1
第四步返回值:dp[n],最后一个数即为最终结果
使用滚动数组的思想去优化 (使用一个常数变量去存储结果,两个常数变量去存储转移方程的另外两个值)
code:
class Solution:
def fib(self, n: int) -> int:
a,b = 0,1
for _ in range(n):
a,b = b,a+b
return a%1000000007
同类型的青蛙跳台阶:剑指 Offer 10- II. 青蛙跳台阶问题
code:
class Solution:
def numWays(self, n: int) -> int:
a,b = 1,1
for _ in range(n):
a,b = b,a+b
return a%1000000007
只是修改了初始值,该题初始值为斐波那锲数列不包括0,f(0) = 1,f(1) = 1,转移方程一样为f(n) = f(n-1) +f(n-2),因为青蛙跳的时候为一步或两步,如果使用一步则f(n-2)如果两步则f(n-1)
状态定义:
定义dp[]数组,dp[i]即表示当前第i天的最大利润
转移方程:
dp[i] = max(dp[i-1],prices[i]-min(prices[0:i]))
初始状态:
dp[0] = 0(第一天的利润为0)
返回值:
dp[-1]当前的最大利润
该题是可以进行时间复杂度和空间复杂度的优化的,时间复杂度优化:使用常数变量更新prices[0:i]的最小值缩小o(i)为o(1),空间复杂度优化:前一个dp[i-1](使用结果代替它即可)
code:
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices:
return 0
mincost = prices[0]
res = 0
for i in prices:
mincost = min(i,mincost)
res = max(res,i-mincost)
return res
定义最小值为价格的第一个值,结果初始值为0,之后先获取当前的最小价格,再进行价格对比。
状态定义:
dp[i]表示到nums[i]的子数组最大和,注意其必须包括nums[i]在内(计算nums[i])
转移方程:
当dp[i-1]<0时,其对数组和做负贡献,则直接等于nums[i]
当dp[i-1]>=0时,其可以和当前数进行相加,不用考虑对下一个数的影响。
初始状态:
dp[0]=nums[0]
返回值:
return max(dp),因为在最后的时候有可能对末尾数做负贡献,最大值可能在中间产生。
空间复杂度优化,直接在原有数组当中进行操作,空间复杂度o(1)
code:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
for i in range(1,len(nums)):
nums[i] = nums[i]+max(nums[i-1],0)
return max(nums)
状态定义:
dp[][]二维数组dp,记录每一格的礼物最大的价值
转移方程:
dp[i][j] = max(grid[i][j]+dp[i-1][j],grid[i][j]+dp[i][j-1])
初始状态:
dp[0][0] = grid[0][0]
在对dp后续定义时需要注意二维的第一排与第二排
返回值:
dp[-1][-1]
空间复杂度优化,在原grid二维数组中进行操作,优化为o(1)
code:
class Solution:
def maxValue(self, grid: List[List[int]]) -> int:
for i in range(len(grid)):
for j in range(len(grid[0])):
if i==0 and j==0:continue
if i==0 :
grid[i][j] = grid[i][j]+grid[i][j-1]
elif j ==0:
grid[i][j] = grid[i][j]+grid[i-1][j]
else:
grid[i][j] = max(grid[i][j]+grid[i-1][j],grid[i][j]+grid[i][j-1])
return grid[-1][-1]
状态定义:
dp[i]为当前字符串i的所有可能数
转移方程:
当int(num[i-1:i+1]) <= 25 and int(num[i-1])!=0时(前两个字符串不超过25,并且前一个字符不为0)dp[i] = dp[i-1] + dp[i-2]
其他情况则dp[i]只等于dp[i-1] (因为最近的两个数只能作为两个字符,不能作为一个字符,)
初始状态:
dp[0] = 1,dp[1]的情况要根据两位数是否大于25决定,大于25则等于1,小于则为2。
返回值:
dp[-1](直接返回dp数组的最后一个值就是结果)
code:
class Solution:
def translateNum(self, num: int) -> int:
if num <10:return 1
num = str(num)
dp = [None]*len(num)
dp[0] = 1
if int(num[:2]) <= 25:
dp[1] = 2
else :
dp[1] = 1
for i in range(2,len(num)):
if int(num[i-1:i+1]) <= 25 and int(num[i-1])!=0:
dp[i] = dp[i-1] + dp[i-2]
else:
dp[i] = dp[i-1]
return dp[-1]
状态定义:
dp[i]表示当前i的最大不重复字符串
转移方程:
j为当前字符的前一个出现的下标,当i-j>dp[i-1]时dp[i] = dp[i-1]+1
当i-j<=dp[i-1]时dp[i] = i - j
初始状态:
dp[0] = 1
返回值:
max(dp)dp数组的最大值
本题需要使用哈希表的数据结构来存储数组的下标(上面dp思路中的j)
空间复杂度优化:使用滚动数组思想,temp记录当前的dp[i-1],res为数组当中的最大值一直更新,
code:
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if not s: return 0
res = temp = 0
hashmap = {}
for j in range(len(s)):
i = hashmap.get(s[j],-1)
temp = temp + 1 if temp<j - i else j - i
hashmap[s[j]] = j
res = max(temp,res)
return res
本文其他:
常见的时间复杂度:
从小到大依次是o(1)<o(log2n)<o(n)<o(nlog2n)<o(n^2)<o(n^3)<o(2^n)<o(n!)<o(n^n)
log2n的代表性算法为二分折半查找:每次进姓折半为1/2,一共进行了k次运算,因为最终的结果只有一个则有n*(1/2)^k = 1推导出2^k = n 进而得出k为logn
n表示算法为一次循环的线性算法
n^2表示对数组排序的各种简单算法