动态规划(Dynamic Programming,DP)
根据百度百科上的定义:
动态规划是求解决策过程最优化的过程
然后各个博客里对动态规划的描述为:
将需要求解的大问题,分解为一个个的小问题,并在计算过程中保存每个小问题的结果以避免重复计算。
动态规划是一种以空间换时间的技术。我看了几个例子,感觉动态规划其实是递归的反推:
递归: 从要求的的问题(顶层)开始,一层一层递归到出口条件,然后再逐层返回
动态规划: 从出口条件开始 ,逐层上升到要求解的问题。
动态规划感觉是迄今为止最难理解的一种算法了,我找了些资料看,感觉还是没有领会到精髓,做题熟悉吧。
70 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
爬到第n阶有两种可能
- 现在站在第n-2阶,再一次性爬两阶;
- 现在站在第n-1阶,再爬一阶
假设两种情况分别包含f(n-2)和f(n-1)种可能,爬到第n层有f(n)种可能,那么有:
f(n) = f(n-1) + f(n-2)
这就是本道题的状态转移方程。同时本题的边界条件:
f(0) = 1, f(1) = 1
接下来题就很好解了
class Solution:
def climbStairs(self, n: int) -> int:
if n==0 or n==1:
return 1
dn = [0]*(n+1)
dn[0] = 1
dn[1] = 1
for i in range(2, n+1):
dn[i] = dn[i-1]+dn[i-2]
return dn[n]
121 买卖股票的最佳时机
和上一道题不一样,买卖股票涉及到买和卖两种情况,不能单纯的用一个一维数组去维护历史结果。
很容易理解:
第n天卖出的收益为,当天的股票价格减去第n-1天之前的最低买入价格,即:
sell[n] = prices[n] - min_buy[n-1]
第n天卖出的最大收益为,当天卖出收益和历史最大收益中较大的那个,即:
max_sell[n] = max (max_sell[n-1], prices[n] - min_buy[n-1])
而最低买入价格很好求,为
min_buy[n] = min(min_buy[n-1], prices[n])
所以代码为:
class Solution:
def maxProfit(self, prices: List[int]) -> int:
# 注释掉的部分为更节省空间和时间的做法
# 但是我对这是不是动态规划表示怀疑
# 所以还是写了个笨笨的真正的动态规划
# 两者的差别就是前一种做法只维护了两个常量
# if prices == []:
# return 0
# min_in = prices[0]
# max_out = 0
# for i in range(len(prices)):
# min_in = min(min_in, prices[i])
# max_out = max(max_out,prices[i]- min_in)
# return max_out
if prices == []:
return 0
min_buy, max_sell = [0]*len(prices), [0]*len(prices)
min_buy[0] = prices[0]
for i in range(1,len(prices)):
min_buy[i] = min(min_buy[i-1], prices[i])
max_sell[i] = max(max_sell[i-1],prices[i]- min_buy[i-1])
return max(max_sell)
5 最长回文子串
一开始看到这道题的时候,想用滑动窗口解,因为回文串存在两种情况:
- aba
- abba
所以要用宽度为2和3的窗口分别遍历整个字符串,找到回文串的“种子”后,以这个种子为中心进行扩展,寻找最长的字符串,每次的时间复杂度为O(N),整体的时间复杂度也是O(N)
动态规划法没什么思路,看了答案,总结如下:
对于给定长度为N的字符串s,如果存在一个子串s[i:j]是回文串,那么必然有s[i+1,j-1]必然是回文串,如果以P[i, j]来记录s[i, j]是不是回文串,则有:
P
[
i
,
j
]
=
P
[
i
+
1
,
j
−
1
]
∧
(
S
i
=
=
S
j
)
P[i,j] = P[i+1,j-1]∧(S i==S j)
P[i,j]=P[i+1,j−1]∧(Si==Sj)
这就是状态转移方程。
至于边界,如我之前描述的两种回文串,有:
P
[
i
,
i
]
=
T
r
u
e
,
P
[
i
,
i
+
1
]
=
(
S
i
=
=
S
i
+
1
)
P[i,i] = True, P[i,i+1] = (Si==Si+1)
P[i,i]=True,P[i,i+1]=(Si==Si+1)
最终答案为所有值为True的P(i,j)中,j-i+1最大的那个。
还一个需要注意的是,不能用两个指针取遍历,那样会漏掉一些结果,我就吃了这个亏吃了很久,用pycharm进行debug才发现这一点,比如:
s =‘aaaaa’
如果我用双循环进行遍历:
for i in range(n):
for j in range(i,n):
pass
那我们会先得到i=0,j=4,此时P(1,3)还没能进行数据更新,还是False,导致P(0,4)判断错误。
所以需要以长度进行遍历。
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
dp = [[False] * n for _ in range(n)]
ans = ""
# 枚举子串的长度 l+1
for l in range(n):
# 枚举子串的起始位置 i,这样可以通过 j=i+l 得到子串的结束位置
for i in range(n):
j = i + l
if j >= len(s):
break
if l == 0:
dp[i][j] = True
elif l == 1:
dp[i][j] = (s[i] == s[j])
else:
dp[i][j] = (dp[i + 1][j - 1] and s[i] == s[j])
if dp[i][j] and l + 1 > len(ans):
ans = s[i:j+1]
return ans
1143 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 :
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
最长公共子序列(Longest Common Subsequence,简称 LCS)是一道比较经典的题目。
首先考虑设计DP数组,对于长度为m和n的字符串s1和s2,有大小为m*n的二维数组DP,DP[i][j]表示:
对于s1[0:i]和s2[0:j],他们的LCS长度为DP[i][j]。
设计好数组,后面的内容就水到渠成了,状态转移方程为:
如果s1[i]==s2[j]
D
P
[
i
]
[
j
]
=
D
P
[
i
−
1
]
[
j
−
1
]
+
1
DP[i][j] = DP[i-1][j-1] +1
DP[i][j]=DP[i−1][j−1]+1
如果s1[i]!=s2[j]
D
P
[
i
]
[
j
]
=
m
a
x
(
D
P
[
i
]
[
j
−
1
]
,
D
P
[
i
−
1
]
[
j
]
)
DP[i][j] = max(DP[i][j-1], DP[i-1][j])
DP[i][j]=max(DP[i][j−1],DP[i−1][j])
写成代码:
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m = len(text1)
n = len(text2)
dp = [[0] *(n+1) for _ in range(m+1)]
for i in range(1, m+1): # 这里从1开始是为了让dp[i-1][j-1]不溢出,同时提供初值
for j in range(1, n+1):
if text1[i-1] == text2[j-1]:
dp[i][j] =dp[i-1][j-1] + 1
else:
dp[i][j]=max(dp[i-1][j],dp[i][j-1])
return max(max(dp))
还是挺耗费时间的,跑了400+ms。