学习总结
学习内容
高级动态规划
递归分治复习
递归 - 函数自己调用自己
def recursion(level,param1,param2,...):
# 递归终止条件
if level > MAX_LEVEL:
process_result
return
# 处理当前层逻辑
process(level, data...)
# 往下一层
self.recursion(level + 1, p1, ...)
# 清理当前层
分治代码模板
排序算法中的归并排序,典型的分而治之
def divide_conquer(problem, param1, param2, ...):
# 递归终止条件
if problem is None:
print_result
return
# 处理当前层逻辑(分解子问题)
data = prepare_data(problem)
subproblems = split_problem(problem, data)
# 往下一层(解决子问题)
subresult1 = self.divide_conquer(subproblems[0], p1, ...)
subresult2 = self.divide_conquer(subproblems[1], p1, ...)
subresult3 = self.divide_conquer(subproblems[2], p1, ...)
…
# 合并子结果(合并结果)
result = process_result(subresult1, subresult2, subresult3, …)
# 清理当前层
DP 顺推模板
function DP():
dp = [][] # ⼆维情况
for i = 0 .. M {
for j = 0 .. N {
dp[i][j] = _Function(dp[i’][j’]…)
}
}
return dp[M][N];
关键点
动态规划和递归或者分治没有根本上的区别(关键看有无最优的子结构)
共性:找到重复子问题
差异性:最优子结构、中途可以淘汰次优解
复杂度来源
状态拥有更多维度(二维、三维、或者更多、甚至需要压缩)
状态方程更加复杂
leetcode股票买卖问题
121. 买卖股票的最佳时机
122. 买卖股票的最佳时机 II
123. 买卖股票的最佳时机 III
188. 买卖股票的最佳时机 IV
309. 最佳买卖股票时机含冷冻期
714. 买卖股票的最佳时机含手续费
其状态转移模型
以 188. 买卖股票的最佳时机 IV 为例,因为其具有通用性并且其他几个股票买卖问题可以由其演化而来
188. 买卖股票的最佳时机 IV
状态定义
dp[i] [k][ 0 or 1] (0 <= i <= n-1, 1 <= k <= K)
- i 为天数
- k 为最多交易次数 (买入并卖出算一次交易)
- [0,1] 为是否持有股票
总状态数: n * K * 2 种状态
第i天,之前交易k次,今天持有股票(1) 或 今天 不持有股票 的最大收益
DP状态方程
dp[i][k][s] = max(buy, sell, rest)
3 层嵌套循环
for 0 <= i < n:
for 1 <= k <= K:
for s in {0, 1}:
dp[i][k][s] = max(buy, sell, rest)
具体状态转移方程
A:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 选择 rest , 选择 sell )
解释:今天(第i天)我没有持有股票,有两种可能:
- 我昨天(第i-1天)就没有持有,然后今天选择 rest,所以我今天还是没有持有;
dp[i-1][k][0]
- 我昨天(第i-1天)持有股票,但是今天我 sell 了,所以我今天没有持有股票了。(买入并卖出算一次交易,所以只卖出 交易k不变)
dp[i-1][k][1] + prices[i]
B:
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )
解释:今天(第i天)我持有着股票,有两种可能:
- 我昨天(第i-1天)就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
dp[i-1][k][1]
- 我昨天(第i-1天)本没有持有,但今天我选择 buy,所以今天我就持有股票了。(这里买入了交易次数需+1,那么昨天交易次数应该是 k-1)
dp[i-1][k-1][0] - prices[i]
综合,状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
初始状态:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = float("-inf")
完整的解题
# @author:leacoder
# @des: 动态规划 买卖股票的最佳时机 IV(通用型)
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
n = len(prices)
if n <= 1: return 0
if k > int(n / 2): # 会超时
# k = int(n/2)
return self.greedy(prices) # 使用贪心
maxprof = 0
profit = [[[0 for _ in range(2)] for _ in range(k + 1)] for _ in
range(0, len(prices))] # DP[ii][kk][0] 第ii天完成kk次操作无股票 DP[ii][kk][1]第ii天完成kk次操作有股票 prices[ii] 第ii天股票价格
for i in range(0, k + 1):
profit[0][i][0] = 0 # 第 1 天 操作i 次 没有股票,所以初始值为 0
profit[0][i][1] = - prices[0] # 第 1 天 操作i 次 有股票, 所以初始值为 - prices[ii]
for ii in range(1, len(prices)): # 天数
for kk in range(0, k + 1): # 交易次数
if kk == 0: #
profit[ii][kk][0] = profit[ii - 1][kk][0] # 0 次交易 今天利润 == 前一天利润
else:
# 今天完成kk次操作无股票 max(前一天无股票今天不交易,前一天有股票今天卖出) 买卖一次算一笔交易 以买入算一次交易 故 profit[ii - 1][kk ][1] + prices[ii]
profit[ii][kk][0] = max(profit[ii - 1][kk][0], profit[ii - 1][kk][1] + prices[ii])
# 今天完成kk次操作有股票 max(前一天有股票今天不交易,前一天无股票今天买入) 以买入算一次交易
profit[ii][kk][1] = max(profit[ii - 1][kk][1], profit[ii - 1][kk - 1][0] - prices[ii])
maxprof = max(maxprof, profit[ii][kk][0])
return maxprof
def greedy(self, prices: List[int]) -> int:
max = 0
for i in range(1, len(prices)):
if prices[i] > prices[i - 1]:
max += prices[i] - prices[i - 1]
return max
123. 买卖股票的最佳时机 III
相对于 188 这里限制只能买卖 2 次
将 188. 买卖股票的最佳时机 IV 完整的解题,k设置为2即可。
代码可以进一步优化如下
# @author:leacoder
# @des: 动态规划 买卖股票的最佳时机 III(通用型)
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices: return 0
# 三维数组
# profit[ii][kk][jj]
# ii 第 ii 天, kk 股票操作了几次 , jj 是否有股票
# 最多可以完成 两笔 交易: kk 可以为 0 1 2 次操作 , jj可以为 0 ,1 0 没有股票 1有股票
profit = [[[0 for _ in range(2)] for _ in range(3)] for _ in range(len(prices))]
# 第一天 初始化
for k in range(3): #最多可以完成 两笔 交易 故range(3)
profit[0][k][0] = 0 # 第 1 天 操作 k 次 没有股票,所以初始值为 0
profit[0][k][1] = - prices[0] # 第 1 天 操作i 次 有股票, 所以初始值为 - prices[0]
# 注意 买 卖 都进行一次算一次操作 k + 1,单独 买入 不算完成一次操作 以卖出 算一次操作(同理 可以 以买入 算一次操作,卖出时操作不计数)
for i in range(1,len(prices)):
# 第 i 天 0 次交易 没有股票最大利润 = 第 i-1 天 0 次交易 没有股票最大利润
profit[i][0][0] = profit[i-1][0][0]
# 第 i 天 0 次交易 有股票最大利润 = max(第 i-1 天 0 次交易 有股票最大利润 , 第 i-1 天 0 次交易 无股票最大利润 - 当天股票价格prices[i](买入))
profit[i][0][1] = max(profit[i-1][0][1],profit[i-1][0][0] - prices[i])
# 第 i 天 1 次交易 无股票最大利润 = max(第 i-1 天 1次交易 无股票最大利润 , 第 i-1 天 0 次交易 有股票最大利润 + 当天股票价格prices[i](卖出))
profit[i][1][0] = max(profit[i-1][1][0],profit[i-1][0][1] +prices[i] )
# 第 i 天 1 次交易 有股票最大利润 = max(第 i-1 天 1 次交易 有股票最大利润 , 第 i-1 天 1 次交易 无股票最大利润 - 当天股票价格prices[i](买入))
profit[i][1][1] = max(profit[i-1][1][1],profit[i-1][1][0] - prices[i])
# 第 i 天 2 次交易 无股票最大利润 = max(第 i-1 天 2次交易 无股票最大利润 , 第 i-1 天 1 次交易 有股票最大利润 + 当天股票价格prices[i](卖出))
profit[i][2][0] = max(profit[i-1][2][0],profit[i-1][1][1] + prices[i] )
end = len(prices) - 1
return max(profit[end][0][0],profit[end][1][0],profit[end][2][0])
122. 买卖股票的最佳时机 II
相对于 188 这里没有买卖限制,也就是 k 为无穷大,k 与 k - 1 是一样的,k参数可从状态维度中移除从而简化状态方程
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
可改写为:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
初始状态:
dp[0][0] = 0
dp[0][1] = -prices[0]
# @author:leacoder
# @des: 动态规划 买卖股票的最佳时机 II
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
if n <= 1: return 0
maxprof = 0
profit = [[0 for _ in range(2)] for _ in range(0, len(prices))]
# DP[ii][0] 第ii天 无股票的利润 DP[ii][1]第ii天有股票的利润 prices[ii] 第ii天股票价格
profit[0][0] = 0 # 第 1 天没有股票,所以初始值为 0
profit[0][1] = - prices[0] # 第 1 天 有股票, 所以初始值为 - prices[ii]
for ii in range(1, len(prices)): # 天数
# 今天 无股票 max(前一天无股票今天不交易,前一天有股票今天卖出)
profit[ii][0] = max(profit[ii - 1][0], profit[ii - 1][1] + prices[ii])
# 今天 有股票 max(前一天有股票今天不交易,前一天无股票今天买入)
profit[ii][1] = max(profit[ii - 1][1], profit[ii - 1][0] - prices[ii])
maxprof = max(maxprof, profit[ii][0])
return maxprof
121. 买卖股票的最佳时机
相对于 188 这里限制只能买卖 1 次
将 188. 买卖股票的最佳时机 IV 完整的解题,k设置为1即可。
代码可以进一步优化如下
# @author:leacoder
# @des: 动态规划 买卖股票的最佳时机 II (通用型)
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices: return 0
result = 0
# 二维数组
# profit[i][0] 第i天 没有股票 的利润
# profit[i][1] 第i天 当前有股票 max( 前面有股票今天不卖 ,前面没股票 今天买入 ) 的利润
profit = [[0 for _ in range(2)] for _ in range(len(prices))]
# 第一天利润初始
profit[0][0] = 0
profit[0][1] = - prices[0]
for i in range(1,len(prices)):
# 今天 无股票 max(前一天无股票今天不交易,前一天有股票今天卖出)
profit[i][0] = max(profit[i - 1][0], profit[i - 1][1] + prices[i])
# 今天 有股票 max(前一天有股票今天不交易,前一天无股票今天买入)
profit[i][1] = max(profit[i - 1][1], - prices[i])
result = max(result,profit[i][0],profit[i][1])
return result
字符串匹配算法
1. 暴力法(brute force)- O(mn)
public static int forceSearch(String txt, String pat) {
int M = txt.length();
int N = pat.length();
for (int i = 0; i <= M - N; i++) {
int j;
for (j = 0; j < N; j++) {
if (txt.charAt(i + j) != pat.charAt(j))
break;
}
if (j == N) {
return i;
}
// 更加聪明?
// 1. 预先判断– hash(txt.substring(i, M)) == hash(pat)
// 2. KMP
}
return -1;
}
Rabin-Karp 算法
在朴素算法中(暴力),我们需要挨个比较所有字符,才知道目标字符串中是否包含子串。那么, 是否有别的方法可以用来判断目标字符串是否包含子串呢?(也就是加速第二层循环)
确实存在一种更快的方法。为了避免挨个字符对目标字符串和子串进行比较, 我们可以尝试一次性判断两者是否相等。因此,我们需要一个好的哈希函数(hash function)。 通过哈希函数,我们可以算出子串的哈希值,然后将它和目标字符串中的子串的哈希值进行比较。 这个新方法在速度上比暴力法有显著提升。
Rabin-Karp 算法的思想:
- 假设子串的长度为 M (pat),目标字符串的长度为 N (txt)
- 计算子串的 hash 值 hash_pat
- 计算目标字符串txt中每个长度为 M 的子串的 hash 值(共需要计算 N-M+1次)
- 比较 hash 值:如果 hash 值不同,字符串必然不匹配; 如果 hash 值相同,还需要使用朴素算法再次判断
关键在于 3 中字符串的哈希值如何计算?如果采用常规计算hash,起不到加速。想象成滑动窗口,中间不变部分不再重复计算,只是处理 进出滑动窗口的字符的hash
KMP 算法
KMP算法(Knuth-Morris-Pratt)的思想就是,当子串与目标字符串不匹配时,其实你已经知道了前面已经匹配成功那 一部分的字符(包括子串与目标字符串)。KMP 算法的想法是,设法利用这个已知信息,不要把“搜索位置” 移回已经比较过的位置,继续把它向后移,这样就提高了效率。(加速二层循环,暴力法中每次比较后移一个字符,KMP算法通过已知信息找出能够后移的n个字符后移n个字符从而加速)
参看:
https://www.bilibili.com/video/av11866460?from=search&seid=17425875345653862171
http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html