目录
序、递归和循环
一、剑指 Offer 10- I. 斐波那契数列
1.1 题求
1.2 求解
法一:无记忆递归
Python - 2021/5/11 - 超出时间限制
# Python
class Solution:
def fib(self, n: int) -> int:
def recur(n: int):
if n < 2:
return n
return recur(n-1) + recur(n-2)
return recur(n) % 1000000007
法二:有记忆递归 (自顶向下)
使用哈希映射 (字典) 来记忆计算过的结果,避免重复冗余的计算。
Python - 2021/5/11 - 82.25% (36ms)
# Python
class Solution:
def fib(self, n: int) -> int:
hashtable = {0: 0, 1: 1} # 记录所有以计算的结果
def recur(n: int):
if hashtable.get(n) is None:
hashtable[n] = recur(n-1) + recur(n-2) # 无记录就计算, 有则直接取出
return hashtable[n]
return recur(n) % 1000000007
C++ - 2021/5/11 - 100.00% (0ms) - 7.51% (6.2MB)
注 1:对 C++ map 使用下标索引,若 key 不在容器中,则 会添加一个具有此 key 的元素到 map 中,这与常见的顺序容器因下标不存在而报错的行为很不同。
注 2:若仅对最后的结果取模,则在过程中会存在 整型数溢出 问题,因此改为每次计算都取模。
class Solution {
private:
int mode = 1000000007;
map<int, int> fib_memo = {{0, 0}, {1, 1}}; // 使用关联容器 map 记录已计算过的值
int recur(int n)
{
if (fib_memo.find(n) == fib_memo.end()) // 若尚未记录
{
fib_memo[n] = (recur(n-1) + recur(n-2)) % mode; // 计算并记录
}
return fib_memo[n];
}
public:
int fib(int n)
{
return recur(n);
}
};
法三:有记忆递归 (自底向上)
使用哈希映射 (字典) 来记忆计算过的结果,避免重复冗余的计算。
Python - 2021/5/11 - 82.25% (36ms)
# Python
class Solution:
def fib(self, n: int) -> int:
if n < 2:
return n
hashtable = {0: 0, 1: 1}
for i in range(2, n+1):
hashtable[i] = hashtable[i-1] + hashtable[i-2]
return hashtable[n] % 1000000007
C++ - 2021/5/11 - 100.00% (0ms) - 13.2% (6.1MB)
// C++
class Solution {
public:
int fib(int n)
{
if (n < 2)
{
return n;
}
int mode = 1000000007;
map<int, int> fib_memo = {{0, 0}, {1, 1}};
for (int i = 2; i <= n; ++i)
{
fib_memo[i] = (fib_memo[i-1] + fib_memo[i-2]) % mode;
}
return fib_memo[n];
}
};
官方解答 (重点:动态规划)
# Python - 推荐
class Solution:
def fib(self, n: int) -> int:
a, b = 0, 1 # 初始状态
for _ in range(n): # n 为几, 就转移几次状态, 从而得到第 n 个状态
a, b = b, (a + b) % 1000000007 # 状态转移
return a
# Python
# 不考虑大数越界问题
class Solution:
def fib(self, n: int) -> int:
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a % 1000000007 # 仅在最后取模
C++ - 2021/5/11 - 100.00% (0ms) - 67.66% (5.8MB)
// C++ - 推荐
class Solution {
public:
int fib(int n)
{
// 初始状态 + 中间状态
int a = 0, b = 1, sum;
for (int i = 0; i < n; ++i)
{
// 状态转移
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return a;
}
};
## 这就牛掰了, 开始使用数学思想了, 所以更有些费解, 但很值得学习!
class Solution:
def fib(self, N: int) -> int:
if (N <= 1):
return N
A = [[1, 1], [1, 0]] # 中间幂矩阵的基数矩阵
self.matrix_power(A, N-1) # 使用递归函数 matrixPower 计算给定矩阵 A 的幂。幂为 N-1
return A[0][0]
def matrix_power(self, A: list, N: int):
if (N <= 1):
return A
self.matrix_power(A, N//2) # matrixPower 函数将对 N/2 个斐波那契数进行操作!
self.multiply(A, A) # 矩阵乘法
B = [[1, 1], [1, 0]]
if (N%2 != 0): # 奇数次补乘 如 3, 因为对于偶数次幂 N=2x 而奇数次幂 N=2x+1
self.multiply(A, B)
def multiply(self, A: list, B: list):
x = A[0][0] * B[0][0] + A[0][1] * B[1][0]
y = A[0][0] * B[0][1] + A[0][1] * B[1][1]
z = A[1][0] * B[0][0] + A[1][1] * B[1][0]
w = A[1][0] * B[0][1] + A[1][1] * B[1][1]
A[0][0] = x
A[0][1] = y
A[1][0] = z
A[1][1] = w
# 这是什么神仙操作 ...
class Solution:
def fib(self, N):
golden_ratio = (1 + 5 ** 0.5) / 2
return int((golden_ratio ** N + 1) / 5 ** 0.5)
其他实现
若此时要求 通过递归返回斐波那契数列的前 N 项,则除了有记忆递归,还可以:
LRU cache 缓存装饰器 (decorator):根据参数缓存每次函数调用结果,对于相同参数的,无需重新函数计算,直接返回之前缓存的返回值。缓存数据是并不一定快,要综合考量缓存的代价以及缓存的时效大小。经典例子是改造 fib 序列。
- 若 maxsize=None,则禁用 LRU 功能,且缓存可无限制增长;当 maxsize=2^x 时,LRU 功能执行得最好;
- 若 typed=True,则 不同类型的函数参数将单独缓存。例如,f(2) 和 f(2.0) 将被视为具有不同结果的不同调用;
- 缓存是有内存存储空间限制的;
from functools import lru_cache
class Solution:
@lru_cache
def fib(self, N: int) -> int:
if N <= 1:
return N
return self.fib(N-1) + self.fib(N-2)
1.3 解说
参考文献:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/50fxu1/
https://blog.youkuaiyun.com/qq_39478403/article/details/107468507
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/50fji7/
二、剑指 Offer 10- II. 青蛙跳台阶问题
2.1 题求
2.2 求解
# Python
# 不考虑大数越界问题
class Solution:
def numWays(self, n: int) -> int:
a, b = 1, 1 # 与斐波那契数列问题的唯一区别是起始值不同
for _ in range(n):
a, b = b, a + b
return a % 1000000007
// C++
class Solution {
public:
int numWays(int n)
{
int a = 1, b = 1, sum;
for(int i = 0; i < n; i++)
{
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return a;
}
};
2.3 解答
参考文献:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/57hyl5/
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/57xs06/
https://leetcode-cn.com/problems/climbing-stairs/
三、剑指 Offer 19. 正则表达式匹配
3.1 题求
3.2 求解
法一:动态规划
- 时间复杂度
,其中
和
分别是字符串 s 和 p 的长度。需要计算出所有的状态,且每个状态在进行转移时的时间复杂度为
- 空间复杂度
,即为存储所有状态使用的空间。
Python - 2021/6/21 - 77.43% (56ms) - 该解答及其解释比官方解答清晰易懂得多
class Solution:
def isMatch(self, s: str, p: str) -> bool:
# 两个字符串的长度
len_s = len(s)
len_p = len(p)
# dp table
# dp[i][j] 表示 s 的前 i 个字符和 p 的前 j 个字符是否匹配
# 换言之, s[0:i] 和 p[0:j] 是否匹配
dp = [[False] * (len_p+1) for _ in range(len_s+1)]
# base case
# 两个空字符串什么也不用做就能匹配
dp[0][0] = True
# 若当前 p 的字符 p[j-1]='*', 则 dp[0][k] 取决于 dp[0][j-2]
# 因为可以令 p[j-1]='*' 对 p[j-2] 的字符重复 0 次,
# 使得 dp[0][j] 的结果只取决于 p[j-3] 是否与当前 s 的字符匹配
for j in range(1, len_p+1):
if p[j-1] == '*':
dp[0][j] = dp[0][j-2]
# 状态转移
# 注意索引偏移问题, s 和 p 的第 i 和 j 个字符分别为 s[i-1] 和 p[j-1]
for i in range(1, len_s+1):
for j in range(1, len_p+1):
# 若 p 的当前字符 p[j-1] = s[i-1] or '.'
# 则 dp[i][j] 的结果仅取决于 dp[i-1][j-1]
# 若 dp[i-1][j-1]=True, 则 dp[i][j]=True
# 若 dp[i-1][j-1]=False, 则 p[j-1] 即使匹配也没用, 仍有 dp[i-1][j-1]=False
if p[j-1] in {s[i-1], '.'}:
dp[i][j] = dp[i-1][j-1]
# 若 p 的当前字符 p[j-1] = '*', 则结果取决于前一个字符 p[j-2]
# 若前一个字符 p[j-2] = s[i-1] or '.', 则有两个匹配可能
# 否则, 不匹配, 令 p[j-1] = '*' 对 p[j-2] 重复 0 次,
# 结果取决于 p[j-3] 与 s[i-1] 的匹配结果 dp[i][j-2]
elif p[j-1] == '*':
if p[j-2] in {s[i-1], '.'}:
dp[i][j] = dp[i-1][j] or dp[i][j-2]
else:
dp[i][j] = dp[i][j-2]
return dp[len_s][len_p]
'''
以一个例子详解上述动态规划转移方程 :
S = abbbbc
P = ab*d*c
1. 当前第 i, j 个字符 s[i-1] 和 p[j-1] 均为字母(或 '.' 可以看成一个特殊的字母)时,
只需判断对应位置的字符 s[i-1] 和 p[j-1] 的相等关系
若相等,只需判断 s[i-1] 和 p[j-1] 之前的字符串是否匹配即可,转化为子问题 dp[i-1][j-1].
若不等,则当前的 s[i-1] 和 p[j-1] 肯定不能匹配,为 dp[i][j] = false.
dp[i-1][j-1] i-1
| |
S [a b b b b][c]
子问题:dp[i-1][j-1]
P [a b * d *][c]
|
j-1
2. 若当前第 j 个字符 p[j-1] = '*',则不妨把类似 'a*', 'b*' 等的当成整体看待,
此时为子问题 dp[i][j]
i-1
|
S a b [b] b b c
子问题:dp[i][j]
P a [b *] d * c
|
j-1
当 'b*' 匹配完 'b' 后,它仍可继续发挥作用,因此可把 i-1 前移 1 位到 i-2,而不丢弃 'b*',
转化为子问题 dp[i-1][j]:
i-2
| <--
S a [b] b b b c
子问题:dp[i-1][j]
P a [b *] d * c
|
j-1
另外,也可让 'b*' 不再进行匹配,把 j-1 前移 2 位到 j-3, 从而丢弃 'b*'
转化为子问题 dp[i][j-2]:
i-1
|
S a b [b] b b c
子问题:dp[i][j-2]
P [a] b * d * c
| <--
j-3
3. 冗余的状态转移不会影响答案,
因为当 j-2 指向 'b*' 中的 'b' 时, 这个状态对于答案是没有用的,
原因参见评论区 稳中求胜 的解释, 当 j-1 指向 '*' 时,
dp[i][j] 只与 dp[i][j-2]有关, 跳过了 dp[i][j-1].
'''
# 重要图解:https://leetcode-cn.com/problems/regular-expression-matching/solution/zheng-ze-biao-da-shi-pi-pei-by-leetcode-solution/452279
3.3 解答
参考文献:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/9a1ypc/
https://leetcode-cn.com/problems/regular-expression-matching/
四、剑指 Offer 42. 连续子数组的最大和
4.1 题求
4.2 求解
法一:动态规划 - 一维 dp table
- 空间复杂度
- 时间复杂度
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
n = len(nums)
# dp[i] 表示 num[:i] 中最大的连续子数组之和
# 换言之,代表以元素 nums[i 为结尾的连续子数组最大和
dp = [0 for _ in range(n+1)]
max_sum = nums[0] # 注意要使用一个 max_sum 变量维护过程中遇到的最大和
# 状态转移
for i in range(1, n+1):
cur_num = nums[i-1]
dp[i] = max(dp[i-1] + cur_num, cur_num)
max_sum = max(max_sum, dp[i])
return max_sum
法二:动态规划 - 滚动数组
- 空间复杂度
- 时间复杂度
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
dp_i = 0 # 由于 dp[i] 只与 dp[i-1] 有关,使用一个状态变量维护即可
max_sum = nums[0] # 注意要使用一个 max_sum 变量维护过程中遇到的最大和
for i in range(len(nums)):
# dp_i = (dp_i + nums[i]) if dp_i > 0 else nums[i]
dp_i = max(dp_i+nums[i], nums[i])
max_sum = max(max_sum, dp_i)
return max_sum
官方说明
4.3 解答
参考文献:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/59gq9c/
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/59h9mr/
五、剑指 Offer 46. 把数字翻译成字符串
5.1 题求
5.2 求解
法一:动态规划 - 1-D dp table
- 空间复杂度
- 时间复杂度
class Solution:
def translateNum(self, num: int) -> int:
nums = str(num)
n = len(nums)
if n < 2:
return n
# dp[i] 表示 num[:i] 共有几种翻译方法
dp = [0 for _ in range(n+1)]
# 初始状态
dp[0] = 1 # 没有数字也是一种翻译
dp[1] = 1 # 首个数字只能单独翻译
for i in range(2, n+1):
prev_num = nums[i-2] # 先前数字
curr_num = nums[i-1] # 当前数字
# 必然可令当前数字独立翻译
dp[i] = dp[i-1]
# 若满足组成 2 位数的条件, 则当前数字还可与先前数字组合翻译 (注意合法性条件一定要写对)
if (prev_num in {'1' ,'2'}) and (eval(prev_num + curr_num) < 26):
dp[i] += dp[i-2]
return dp[n]
法二:动态规划 - 滚动数组 / dp table 降维
- 空间复杂度
- 时间复杂度
class Solution:
def translateNum(self, num: int) -> int:
nums = str(num)
n = len(nums)
if n < 2:
return n
dp_0 = 1 # 没有数字也是一种翻译
dp_1 = 1 # 首个数字只能单独翻译
dp_2 = 0 # 当前状态 - 最新状态
for i in range(2, n+1):
prev_num = nums[i-2] # 先前数字
curr_num = nums[i-1] # 当前数字
# 必然可令当前数字独立翻译
dp_2 = dp_1
# 若满足组成 2 位数的条件, 则当前数字还可与先前数字组合翻译 (注意合法性条件一定要写对)
if (prev_num in {'1' ,'2'}) and (eval(prev_num + curr_num) < 26):
dp_2 += dp_0
# 先前状态更新 - 状态后移
dp_0 = dp_1
dp_1 = dp_2
return dp_2
官方说明
class Solution:
def translateNum(self, num: int) -> int:
s = str(num)
a = b = 1
for i in range(2, len(s) + 1):
tmp = s[i - 2:i]
c = a + b if "10" <= tmp <= "25" else a
b = a
a = c
return a
class Solution:
def translateNum(self, num: int) -> int:
a = b = 1
y = num % 10
while num > 9:
num //= 10
x = num % 10
a, b = (a + b if 10 <= 10 * x + y <= 25 else a), a
y = x
return a
5.3 解答
参考文献:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/99wd55/
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/99dnh6/
六、剑指 Offer 47. 礼物的最大价值
6.1 题求
6.2 求解
法一:动态规划 - 2-D dp table - 自底向上
- 空间复杂度
- 时间复杂度
class Solution:
def maxValue(self, grid: List[List[int]]) -> int:
m = len(grid) # 行数
n = len(grid[0]) # 列数
# dp[i][j] 表示在 i 行 j 列可得到的最多价值的礼物
dp = [[0 for _ in range(n+1)] for _ in range(m+1)]
# 状态转移 - 自底向上
for i in range(1, m+1):
for j in range(1, n+1):
# 选出当前位置 上侧和左侧 最大的一个 与当前位置求和 作为状态
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i-1][j-1]
return dp[m][n]
法二:动态规划 - 2D- dp table 降维 1-D - 每次仅保留相邻列 (或行) 的状态
- 空间复杂度
- 时间复杂度
class Solution:
def maxValue(self, grid: List[List[int]]) -> int:
m = len(grid) # 行数
n = len(grid[0]) # 列数
# dp[i] 表示在 i 行可得到的最多价值的礼物
# 由于从 2-D 降维 1-D 每次仅保存相邻 1 列的状态
# 整体空间复杂度降低了, 但时间复杂度不会有任何改善
dp = [0 for _ in range(m+1)] # 多整一个第 0 行和第 0 列简化判断
# 状态转移
for j in range(1, n+1): # 0 ~ n 列 共 n+1 列
for i in range(1, m+1): # 0 ~ m 行 共 m+1 行
dp[i] = max(dp[i-1], dp[i]) + grid[i-1][j-1]
return dp[m]
官方说明
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 - 1]
elif j == 0:
grid[i][j] += grid[i - 1][j]
else:
grid[i][j] += max(grid[i][j - 1], grid[i - 1][j])
return grid[-1][-1]
class Solution:
def maxValue(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
for j in range(1, n): # 初始化第一行
grid[0][j] += grid[0][j - 1]
for i in range(1, m): # 初始化第一列
grid[i][0] += grid[i - 1][0]
for i in range(1, m):
for j in range(1, n):
grid[i][j] += max(grid[i][j - 1], grid[i - 1][j])
return grid[-1][-1]
6.3 解答
参考文献:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/5vokvr/
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/5vr32s/
七、剑指 Offer 48. 最长不含重复字符的子字符串
7.1 题求
7.2 求解
法一:双指针-滑动窗口 - 使用 dict 维护窗口
- 空间复杂度
- 时间复杂度
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if len(s) < 2:
return len(s)
left, right = 0, 0 # # 左指针-指向旧字符, 右指针-指向新字符 [left, right)
char_win = {} # 当前滑窗内的字符计数字典
max_len = 0 # 维护滑窗过程中的最大不重复字符串长度
# 遍历右指针
while right < len(s):
# 右指针右移
new_char = s[right]
right += 1
# 增加新字符
if char_win.get(new_char) is None:
char_win[new_char] = 0
char_win[new_char] += 1
# 一旦存在重复, right 便停止右移动
while char_win[new_char] > 1:
# 左指针右移
old_char = s[left]
left += 1
# 移除旧字符
char_win[old_char] = max(char_win[old_char]-1, 0)
# 若不存在重复, 更新最大长度
max_len = max(max_len, right-left)
return max_len
法二: 双指针-滑动窗口 - 使用 set 维护窗口
- 空间复杂度
- 时间复杂度
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
left, right = 0, 0 # 左指针-指向旧字符, 右指针-指向新字符 [left, right)
char_win = set() # 当前滑窗内的字符计数字典
max_len = 0 # 维护滑窗过程中的最大不重复字符串长度
# 遍历右指针
while right < len(s):
# 右指针右移
new_char = s[right]
right += 1
# 一旦存在重复, right 便停止右移动, 删除旧字符至不重复
while new_char in char_win:
# 左指针右移
old_char = s[left]
left += 1
# 删除旧字符
char_win.remove(old_char)
# 加入新字符
char_win.add(new_char)
# 此时必已不存在重复, 更新最大长度
max_len = max(max_len, len(char_win))
return max_len
法三:双指针-滑动窗口-最简化版 (效率最佳)
- 空间复杂度
- 时间复杂度
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
char_win = {} # 当前滑窗内最右字符索引记录
max_len = 0 # 最大长度
left = -1 # 左指针 - 无重复的最大左边界
# 遍历字符串 s
for right in range(len(s)):
# 右指针指向当前字符 cur_char
cur_char = s[right]
# 若当前字符 cur_char 已存在于滑窗 char_win 中, 更新最大左指针 left
if cur_char in char_win:
left = max(char_win[cur_char], left)
# 更新当前字符 cur_char 的最大 index 为 right
char_win[cur_char] = right
# 更新最大长度 max_len
max_len = max(max_len, right-left)
return max_len
法四:动态规划-哈希表
- 空间复杂度
- 时间复杂度
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
char_win = {} # 滑窗内字符索引字典
cur_len = 0 # 当前长度
max_len = 0 # 最大长度
# 遍历字符串
for right in range(len(s)):
# 获取与 s[right] 最近的相同字符的索引 left, 否则返回 -1
left = char_win.get(s[right], -1)
# 更新哈希表, 使用 right 覆盖 s[right] 的索引作为当前最新索引
char_win[s[right]] = right
# 更新当前长度 dp[right - 1] -> dp[right] (分两种情况)
d = right - left # d 与 f(i-1) 存在两种大小关系 (详见 7.3 解答)
cur_len = cur_len + 1 if cur_len < d else d
# 更新最大长度 max(dp[right - 1], dp[right])
max_len = max(max_len, cur_len)
return max_len
官方说明
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
dic = {}
res = tmp = 0
for j in range(len(s)):
i = dic.get(s[j], -1) # 获取与 s[j] 最近的相同字符的索引 i, 否则返回 -1
dic[s[j]] = j # 更新哈希表, 使用 j 覆盖 s[j] 的索引作为当前最新索引
tmp = tmp + 1 if tmp < j - i else j - i # 更新当前长度 dp[j - 1] -> dp[j]
res = max(res, tmp) # 更新最大长度 max(dp[j - 1], dp[j])
return res
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
res = tmp = i = 0
for j in range(len(s)):
i = j - 1
while i >= 0 and s[i] != s[j]: i -= 1 # 线性查找 i
tmp = tmp + 1 if tmp < j - i else j - i # dp[j - 1] -> dp[j]
res = max(res, tmp) # max(dp[j - 1], dp[j])
return res
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
dic, res, i = {}, 0, -1
for j in range(len(s)):
if s[j] in dic:
i = max(dic[s[j]], i) # 更新左指针 i
dic[s[j]] = j # 哈希表记录
res = max(res, j - i) # 更新结果
return res
7.3 解答
参考文献:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/5dgr0c/
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/5dz9di/
八、剑指 Offer 49. 丑数
8.1 题求
8.2 求解
官方说明
class Solution:
def nthUglyNumber(self, n: int) -> int:
### https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/9hq0r6/
### 丑数的递推性质:丑数只包含因子 2, 3, 5 ,因此有 “丑数 == 某较小丑数 × 某因子”
# dp[i] 代表第 i + 1 个丑数
dp = [1] * n
# 辅助指针 a, b, c 指向首个丑数, 根据递推公式得下个丑数,并每轮将对应指针 +1
a, b, c = 0, 0, 0
# 状态转移
for i in range(1, n):
# dp[a], dp[b], dp[c] 为首个乘 2, 3, 5 后大于 dp[i] 的数
n2, n3, n5 = dp[a] * 2, dp[b] * 3, dp[c] * 5
# dp[i] 是三种情况中的最小值
dp[i] = min(n2, n3, n5)
# 对应 dp[i] 的指针 +1
# 为什么要 +1?因为同一个 [丑数*因子] 的组合用过之后就不能再用了!!!!!
if dp[i] == n2:
a += 1
if dp[i] == n3:
b += 1
if dp[i] == n5:
c += 1
return dp[-1]
网上解法
# 99.88% - 预计算法
class Ugly:
def __init__(self):
self.dp = [1] # 作为类属性的 dp 数组
a = b = c = 0 # 辅助指针
### 提前计算好所有结果, 然后查表!!!
for i in range(1, 1690):
n2, n3, n5 = self.dp[a] * 2, self.dp[b] * 3, self.dp[c] * 5
ugly = min(n2, n3, n5)
self.dp.append(ugly)
if ugly == n2:
a += 1
if ugly == n3:
b += 1
if ugly == n5:
c += 1
class Solution:
u = Ugly()
def nthUglyNumber(self, n):
return self.u.dp[n-1]
8.3 解答
参考文献:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/9h3im5/
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/9hq0r6/
https://leetcode-cn.com/submissions/detail/193153631/
九、剑指 Offer 60. n 个骰子的点数
9.1 题求
9.2 求解
法一:动态规划 - 2D - dp table
- 空间复杂度
- 时间复杂度
### 20ms - 100.00% - 逆向遍历
class Solution:
def dicesProbability(self, n: int) -> List[float]:
# n 个骰子 共有 6^n 种组合 (有重复)
# n 个骰子 最小值 n*1, 最大值 n*6, 有 n*(6-1) + 1 种 (不重复)
num_max = 6 * n # 骰子和 最大值 (每个骰子取 6 之和即为最小值 6n)
num_min = n # 骰子和 最小值 (每个骰子取 1 之和即为最小值 n)
num_kind = num_max - num_min + 1 # 骰子和 种类数 (最大值 - 最小值 + 1 = 6*n - 5*n + 1)
num_sum = 6 ** n # 骰子 组合总数 (如 1 个 6 种、2 个 6**2 = 36 种、...)
# dp[i][j]: 投出第 i 个骰子时累积和中第 j 小数字的频数
dp = [[0 for _ in range((n*6+1))] for _ in range(n+1)]
# 初始状态 - 哑节点 - 没投也是一种组合
dp[0][0] = 1
# 投 1 ~ n 个骰子 (无偏移)
for i in range(1, n + 1):
# 中值 - 山峰数组峰顶(左、中)
end = 7 * i
mid = int(end // 2)
# 第 i 次骰子的和涉及的数值范围前半段 (无偏移)
for j in range(i, mid+1): # 2 ~ 7
# 与当前之和 j 相关的、上一轮的前 num 个数值
num = min(7, j-i+2) # 至多与前 6 个有关
for k in range(1, num):
# i-1 表示上一个骰子, j-k 表示前 k 个
dp[i][j] += dp[i-1][j-k]
# 第 i 次骰子的和涉及的数值范围后半段 (无偏移)
dp[i][end - j] = dp[i][j] # 对称 - 直接复制
# 组合频率 = 组合频数 / 组合总数
return [dp[n][n+i] / num_sum for i in range(num_kind)]
法二:动态规划 - 1D - dp table
- 空间复杂度
- 时间复杂度
# 降维, 每次仅保存相邻的两个状态
class Solution:
def dicesProbability(self, n: int) -> List[float]:
num_max = 6 * n # 骰子和 最大值 (每个骰子取 6 之和即为最小值 6n)
num_min = n # 骰子和 最小值 (每个骰子取 1 之和即为最小值 n)
num_kind = num_max - num_min + 1 # 骰子和 种类数 (最大值 - 最小值 + 1 = 6*n - 5*n + 1)
num_sum = 6 ** n # 骰子 组合总数 (如 1 个 6 种、2 个 6**2 = 36 种、...)
# dp[i][j]: 投出第 i 个骰子时累积和中第 j 小数字的频数
dp = [0 for _ in range((n*6+1))]
# 初始状态 - 哑节点 - 没投也是一种组合
dp[0] = 1
# 投 1 ~ n 个骰子 (无偏移)
for i in range(1, n + 1):
tmp = [0 for _ in range((n*6+1))] # 临时保存当前 dp 数组
# 中值 - 山峰数组峰顶(左、中)
end = 7 * i
mid = int(end // 2)
# 第 i 次骰子的和涉及的数值范围前半段 (无偏移)
for j in range(i, mid+1): # 2 ~ 7
# 与当前之和 j 相关的、上一轮的前 num 个数值
num = min(7, j-i+2) # 至多与前 6 个有关
for k in range(1, num):
# i-1 表示上一个骰子, j-k 表示前 k 个
tmp[j] += dp[j-k]
# 第 i 次骰子的和涉及的数值范围后半段 (无偏移)
tmp[end - j] = tmp[j] # 对称 - 直接复制
dp = tmp # dp 数组 更新
# 组合频率 = 组合频数 / 组合总数
return [dp[n+i] / num_sum for i in range(num_kind)]
官方说明
### 28ms - 98.99% - 正向遍历(非常简洁推荐)
class Solution:
def dicesProbability(self, n: int) -> List[float]:
# dp 数组:第 1 次骰子概率均等
dp = [1.0 / 6.0] * 6
# 余 2 ~ n 次骰子
for i in range(2, n + 1):
# 当前可能产生结果数 num_kind
tmp = [0] * (5*i + 1)
# 根据上一次结果正向遍历
for j in range(len(dp)):
# 每个上一次结果 影响 当前相邻 6 个结果
for k in range(6):
tmp[j + k] += dp[j] / 6
# 更新 dp 数组
dp = tmp
return dp
9.3 解答
参考文献:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/ozzl1r/
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/ozsdss/
十、剑指 Offer 63. 股票的最大利润
10.1 题求
10.2 求解
法一:动态规划 - 3-D dp table
- 空间复杂度
- 时间复杂度
# 动态规划 - 基本版
class Solution:
def maxProfit(self, prices: List[int]) -> int:
# 总交易天数
n = len(prices)
# dp[i][j][k] 表示第 i 天、可交易次数为 j、股票持有状态为 k 状态下的最大利润
# 以 0 初始化省去很多细节
dp = [[[0 for k in range(2)] for j in range(2)] for i in range(n+1)]
# 初始状态
dp[0][0][1] = float('-inf') # 第 0 天, 还没交易不可能持有股票
for i in range(n+1):
dp[i][1][1] = float('-inf') # 第 i 天, 还没交易不可能持有股票
# 状态转移
for i in range(1, n+1):
# 第 i 天交易次数为 0 且未持有股票, 只可能源自第 i-1 天卖出股票 or 保持第 i-1 天的相同状态
dp[i][0][0] = max(dp[i-1][0][1] + prices[i-1], dp[i-1][0][0])
# 第 i 天交易次数为 0 且持有股票, 只可能源自第 i-1 天买入股票 or 保持第 i-1 天的相同状态
dp[i][0][1] = max(dp[i-1][1][0] - prices[i-1], dp[i-1][0][1])
# 其余两个状态不用转移
# dp[i][1][0] = 0 # 毕竟总可交易次数为 1 是不可能有利润的
# dp[i][1][1] = float('-inf') # 还没交易不可能持有股票
# 最大利润
return dp[n][0][0]
法二:动态规划 - 2D dp table (滚动数组)
- 空间复杂度
- 时间复杂度
class Solution:
def maxProfit(self, prices: List[int]) -> int:
# dp[j][k] 表示当前天、可交易次数为 j、股票持有状态为 k 状态下的最大利润
# 以 0 初始化省去很多细节
dp = [[0 for k in range(2)] for j in range(2)]
# 初始状态
dp[0][1] = float('-inf') # 第 0 天, 还没交易不可能持有股票
dp[1][1] = float('-inf') # 第 0 天, 还没交易不可能持有股票
# 状态转移
for i in range(1, len(prices)+1):
### 注意, 因为涉及 dp[0][1] 的转移, 顺序不能反, 否则需要用 temp 保存临时状态 ###
# 第 i 天交易次数为 0 且未持有股票, 源自第 i-1 天卖出股票 or 保持第 i-1 天的相同状态
dp[0][0] = max(dp[0][1] + prices[i-1], dp[0][0])
# 第 i 天交易次数为 0 且持有股票, 源自第 i-1 天买入股票 or 保持第 i-1 天的相同状态
dp[0][1] = max(dp[1][0] - prices[i-1], dp[0][1])
# 最大利润
return dp[0][0]
更好、更快的写法:
class Solution:
def maxProfit(self, prices: List[int]) -> int:
# dp_j_k 表示当前天、可交易次数为 j、股票持有状态为 k 状态下的最大利润
dp_0_0 = dp_1_0 = 0
dp_0_1 = dp_1_1 = float('-inf') # 第 0 天, 还没交易不可能持有股票
# 状态转移
for i in range(len(prices)):
### 注意, 因为涉及 dp_0_1 的转移, 顺序不能反, 否则需要用 temp 保存临时状态 ###
# 第 i 天交易次数为 0 且未持有股票, 源自第 i-1 天卖出股票 or 保持第 i-1 天的相同状态
dp_0_0 = max(dp_0_1 + prices[i], dp_0_0)
# 第 i 天交易次数为 0 且持有股票, 源自第 i-1 天买入股票 or 保持第 i-1 天的相同状态
dp_0_1 = max(dp_1_0 - prices[i], dp_0_1)
# 最大利润
return dp_0_0
法三 - 一次遍历
- 空间复杂度
- 时间复杂度
class Solution:
def maxProfit(self, prices: List[int]) -> int:
min_price = float('inf') # 最小买入价格
max_profit = 0 # 最大利润 = 最大卖出价格 - 最小买入价格
for price in prices:
# 由于必须在未来卖出股票, 故先计算最大利润, 再计算最小买入价格
max_profit = max(max_profit, price-min_price)
min_price = min(min_price, price)
return max_profit
官方说明
class Solution:
def maxProfit(self, prices: List[int]) -> int:
cost, profit = float("+inf"), 0
for price in prices:
cost = min(cost, price)
profit = max(profit, price - cost)
return profit
10.3 解答
参考文献:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/58nn7r/
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/58vmds/