一、分治,回溯,递归,动态规划
1.1、递归的代码模板
public void recur(int level, int param) {
// terminator
if(level > MAX_LEVEL) {
// process result
return;
}
// process current logic
process(level, param);
// drill down
recur(level: level + 1, newParam);
// restore current status
}
1.2、分治(Divide & Conquer)的代码模板
def divide_conquer(problem, param1, param2, ...):
# recursion terminator
if problem is None:
print_result
return
# prepare data
data = prepare_data(problem)
subproblems = split_problem(problem, data)
# conquer subproblems
subresult1 = self.divide_conquer(subproblems[0], p1, ...)
subresult2 = self.divide_conquer(subproblems[1], p1, ...)
subresult3 = self.divide_conquer(subproblems[2], p1, ...)
...
# process and generate the final result
result = process_result(subresult1, subresult2, subresult2, ...)
1.3、联系
1、动态规划和递归或者分治没有根本上的区别(关键看有无最优的子结构)。
2、共性:找到重复子问题。
3、差异性:最优子结构,中途可以淘汰次优解。
1.4、本质
本质都是在寻找重复性,将重复性转化为 计算机指令集if else, for, loop, while ...
1.5、总结
1、人肉递归低效,很累。
2、找到最近最简方法,将其拆解成可重复解决的问题。
3、数学归纳法思维(抵制人肉递归的诱惑),先看n=1,n=2时的情况,然后看f(n)和f(n-1)之间的联系。
二、动态规划
2.1、定义
1、动态规划的英文叫Dynamic programming,翻译过来叫动态规划,翻译很玄学,其实翻译成动态递推更容易理解。
2、动态规划的本质就是将一个复杂问题拆解成若干个子问题进行解决。原文为:Simplifying a complicated problem by breaking it down into simpler sub-problems.
3、动态规划其实就是分治+ 最优子结构( Divide & Conquer + Optimal substructure)。
2.2、关键点
1、最优子结构:公式为,opt[n] = best_of(opt[n-1], opt[n-2], ...)
2、储存中间状态:opt[i]
3、递推公式(美其名曰:状态转移方程或者DP方程):Fib:opt[i] = opt[n-1] + opt[n-2]. 二维路径为 opt[i,j] = opt[i+1][j] + opt[i][j+1] , 其中,需要判断opt[i,j]是否为空。
2.3、小结
1、打破自己的思维惯性
2、理解复杂逻辑的关键
3、职业进阶的要点要领
三、实战例题
例题1:杨辉三角 https://leetcode-cn.com/problems/pascals-triangle/
给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
示例:
输入: 5
输出:
[
[1],
[1,1],
[1,2,1],
[1,3,3,1],
[1,4,6,4,1]
]
思路:
1、动态规划,dp存的是结果,和nums产生联系
2、初始方程: dp = [[1] * (i+1) for i in range(num)], 例如 num = 5, 得到[[1], [1, 1], [1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1, 1]]
3、递归方程: dp[i][j] = dp[i-1][j] + dp[i-1][j-1]
时间复杂度:O(mn), m为深度, n为每一层的平均个数
空间复杂度:O(mn),m为深度, n为每一层的平均个数
代码实现:
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
dp = [[1]*(i+1) for i in range(numRows)]
for i in range(2, numRows):
for j in range(1, len(dp[i]) - 1):
dp[i][j] = dp[i-1][j] + dp[i-1][j-1]
return dp
例题2: 打家劫舍 II https://leetcode-cn.com/problems/house-robber-ii/
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [0]
输出:0
思路:
1、动态规划
2、初始方程: pre, cur = 0, 0
3、递推方程: pre, cur = cur, max(pre + nums[i], cur)
时间复杂度:O(n)
空间复杂度:O(1)
代码实现:
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
def myrob(nums):
pre, cur = 0, 0
for i in range(len(nums)):
pre, cur = cur, max(pre + nums[i], cur)
return cur
return max(myrob(nums[:-1]), myrob(nums[1:]))
例题3: 62. 不同路径 https://leetcode-cn.com/problems/unique-paths/
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向右 -> 向下
- 向右 -> 向下 -> 向右
- 向下 -> 向右 -> 向右
思路:
如果沿着边走,即m=0或者n=0时,可以走的选择都是1种,如果不是贴着边走的话,每一步的走法等于上边和左边的走法之和。按着这种思路,可以事先把动态方程的初始方程写成贴着边的都是1,其余都是0。当然也可以在遍历的时候写。具体看下面代码实现。
1、初始方程:dp = [[1] * n] + [[1] + [0] * (n - 1) for _ in range(m - 1)]
2、递推方程:dp[i][j] = dp[i][j-1] + dp[i-1][j]
时间复杂度:O(mn)
空间复杂度:O(mn)
代码实现:
# 写法一:事先定义好初始方程,贴着边的都是1,不贴着边的都是0,拿m=3,n=7 举例, 得到的初始方程为,[[1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0]]
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[1] * n] + [[1] + [0] * (n - 1) for _ in range(m - 1)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i][j-1] + dp[i-1][j]
return dp[-1][-1]
# 写法二:直接在遍历的时候判断
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[0] * n for _ in range(m)]
for i in range(m):
for j in range(n):
if i == 0 or j == 0:
dp[i][j] = 1
else:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[-1][-1]
# 写法三:直接在所有格子填写1,在第2行第2列开始做累加(等于上1格+左边1格)
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[1]*n for _ in range(m)]
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[-1][-1]
例题4: 53. 最大子序和https://leetcode-cn.com/problems/maximum-subarray/
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
思路:
1、初始方程:dp = nums
2、递推方程:dp[i] = max(dp[]i-1] + nums[i], dp[i])
时间复杂度:O(n)
空间复杂度:O(n)
代码实现:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
dp = nums
for i in range(1, len(nums)):
dp[i] = max(dp[i-1] + nums[i], dp[i])
return max(dp)
例题5: 718. 最长重复子数组 https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
示例:
输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1] 。
思路:
思路:动态规划dp,数组元素如果相等,则等于对角线+1, 同时,比较当前值和计数值,返回计数值和当前值的较大者。
1、初始方程:dp = [[0] * (len(B) + 1) for _ in range(len(A) + 1)]
2、递推方程:dp[i][j] = dp[i-1][j-1] + 1, res = max(res, dp[i][j])
时间复杂度:O(mn), m为A的长度,n为B的长度
空间复杂度:O(mn)
代码实现:
class Solution:
def findLength(self, A: List[int], B: List[int]) -> int:
res = 0
dp = [[0] * (len(B) + 1) for _ in range(len(A) + 1)]
for i in range(len(A)):
for j in range(len(B)):
if A[i] == B[j]:
dp[i][j] = dp[i-1][j-1] + 1
res = max(res, dp[i][j])
return res
例题6: 70. 爬楼梯 https://leetcode-cn.com/problems/climbing-stairs/
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
思路:
1、初始方程:dp = [0] * (n+1), dp[0] = dp[1] = 1
2、递推方程:dp[i] = dp[i-1] + dp[i-2]
时间复杂度:O(n)
空间复杂度:O(n)
代码实现:
class Solution:
def climbStairs(self, n: int) -> int:
dp = [0] * (n+1)
dp[0] = dp[1] = 1
for i in range(2, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[-1]
例题7: 120. 三角形最小路径和 https://leetcode-cn.com/problems/triangle/
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。
例如,给定三角形:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
思路:
自底向上动态规划,从三角形的底部倒着递推至三角形的顶部,逐层进行递推,递推的规律为,当前元素等于下一层中的相同位置,或者+1位置的最小值中的元素,加上当前元素本身。
1、初始方程:dp = [[0] * (n+1) for _ in range(m+1)]
2、递推方程:dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j]
时间复杂度:O(mn) m为三角形的深度,n为三角形每行的个数
空间复杂度:O(mn)
代码实现:
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
m = len(triangle)
n = len(triangle[-1])
dp = [[0] * (n+1) for _ in range(m+1)]
for i in range(m - 1, -1, -1):
for j in range(len(triangle[i])):
dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j]
return dp[0][0]
2021-01-09 更新思路:构造dp方程的时候,可以直接构造一个三角形
初始方程:dp = [[0]*(i + 1) for i in range(len(triangle) + 1)]
完整的代码为:
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
dp = [[0]*(i + 1) for i in range(len(triangle) + 1)]
for i in range(len(triangle) -1, -1, -1):
for j in range(len(triangle[i])):
dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j]
return dp[0][0]
例题8: 1143. 最长公共子序列 https://leetcode-cn.com/problems/longest-common-subsequence/
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。
思路:
动态规划,注意dp的初始长度是+1,所以递推方程中用的是i+1开始,而不是i开始, 即dp[i+1][j+1]
1、初始方程:dp = [[0] * (len(text2) + 1) for _ in range(len(text1) + 1)]
2、递推方程:相等:dp[i+1][j+1] = dp[i][j] + 1, 不相等:dp[i+1][j+1] = max(dp[i+1][j], dp[i][j+1])
时间复杂度: O(mn) m为text1的长度,n为text2的长度
空间复杂度:O(mn)
代码实现:
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
dp = [[0] * (len(text2) + 1) for _ in range(len(text1) + 1)]
for i in range(len(text1)):
for j in range(len(text2)):
if text1[i] == text2[j]:
dp[i+1][j+1] = dp[i][j] + 1
else:
dp[i+1][j+1] = max(dp[i+1][j], dp[i][j+1])
return dp[-1][-1]
例题9: 第 k 个数 https://leetcode-cn.com/problems/get-kth-magic-number-lcci/
有些数的素因子只有 3,5,7,请设计一个算法找出第 k 个数。注意,不是必须有这些素因子,而是必须不包含其他的素因子。例如,前几个数按顺序应该是 1,3,5,7,9,15,21。
示例 1:
输入: k = 5
输出: 9
思路:
动态规划
1、初始方程:dp = [0] * k, dp[0] = 1
2、递推方程:dp[i] = min(dp[num3] * 3, min(dp[num5] * 5, dp[num7] * 7))
时间复杂度: O(n)
空间复杂度:O(n)
代码实现:
class Solution:
def getKthMagicNumber(self, k: int) -> int:
num3, num5, num7 = 0, 0, 0
dp = [0] * k
dp[0] = 1
for i in range(1, k):
dp[i] = min(dp[num3] * 3, min(dp[num5] * 5, dp[num7] * 7))
if dp[i] == dp[num3] * 3:
num3 += 1
if dp[i] == dp[num5] * 5:
num5 += 1
if dp[i] == dp[num7] * 7:
num7 += 1
return dp[-1]
例题11: 64. 最小路径和 https://leetcode-cn.com/problems/minimum-path-sum/
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
思路:
思路:dp动态规划
初始方程:dp = grid ,因为相等,所以可以不显示写出来,直接用grid; (说明:但是在工业级代码中还是建议用dp表示,因为不写dp,直接用grid的话,会改变原来的入参,把原来的参数改的面目全非,可能工程中并不希望改变原来的入参。)
递推方程:i == 0:grid[i][j] += grid[i][j-1]; j == 0: grid[i][j] += grid[i-1][j]; grid[i][j] += min(grid[i-1][j], grid[i][j-1])
时间复杂度: O(m*n)
空间复杂度:O(1)
代码实现:
class Solution:
def minPathSum(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
elif i == 0:
grid[i][j] += grid[i][j-1]
elif j == 0:
grid[i][j] += grid[i-1][j]
else:
grid[i][j] += min(grid[i-1][j], grid[i][j-1])
return grid[-1][-1]
例题12: 322. 零钱兑换 https://leetcode-cn.com/problems/coin-change/
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
思路:
思路:将总数amount进行拆解,每拆解一个硬币,则计数+1
初始方程:dp = [(amount + 1) for _ in range(amount + 1)]
递推方程:dp[i] = min(dp[i], dp[i-coin] + 1)
时间复杂度: O(m*n),m为硬币总数amount, n为不同硬币的个数
空间复杂度:O(m)
代码实现:
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [(amount + 1) for _ in range(amount + 1)]
dp[0] = 0
for i in range(amount + 1):
for coin in coins:
if coin <= i:
dp[i] = min(dp[i], dp[i-coin] + 1)
if dp[amount] == amount + 1:
return -1
return dp[-1]
例题13: 198. 打家劫舍 https://leetcode-cn.com/problems/house-robber/
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
思路:
思路:动态规划
初始方程:pre, cur = 0, 0
递推方程:pre, cur = cur, max(pre + nums[i], cur)
T:O(n), S:O(1)
时间复杂度: O(n)
空间复杂度:O(1)
代码实现:
class Solution:
def rob(self, nums: List[int]) -> int:
pre, cur = 0, 0
for i in range(len(nums)):
pre, cur = cur, max(pre + nums[i], cur)
return cur
例题14: 第k个数 https://leetcode-cn.com/problems/get-kth-magic-number-lcci/
有些数的素因子只有 3,5,7,请设计一个算法找出第 k 个数。注意,不是必须有这些素因子,而是必须不包含其他的素因子。例如,前几个数按顺序应该是 1,3,5,7,9,15,21。
示例 1:
输入: k = 5
输出: 9
思路:
思路:动态规划dp
初始方程:dp = [0] * k,dp[0] = 1
递推方程:dp[i] = min(dp[num3]3, min(dp[num5]5, dp[num7]*7))
时间复杂度: O(k)
空间复杂度:O(k)
代码实现:
class Solution:
def getKthMagicNumber(self, k: int) -> int:
num3, num5, num7 = 0, 0, 0
dp = [0] * k
dp[0] = 1
for i in range(1, k):
dp[i] = min(dp[num3]*3, min(dp[num5]*5, dp[num7]*7))
if dp[i] == dp[num3]*3:
num3 += 1
if dp[i] == dp[num5]*5:
num5 += 1
if dp[i] == dp[num7]*7:
num7 += 1
return dp[-1]
数据下载:
撰写记录
2020.10.10-10:57:05-第一次撰写
2020.12.14-06:55:17-第二次撰写
2020.12.19-04:18:10-第三次撰写
2020.12.22-06:12:18-第四次撰写
2020.12.23-07:18:00-第五次撰写
2020.12.25-07:32:10-第六次撰写
2020.12.27-08:59:10-第七次撰写
2021.01.04-06:58:00-第八次撰写
2021.01.10-07:39:01-第九次撰写