diff --git a/Contents/10.Dynamic-Programming/03.Linear-DP/01.Linear-DP.md b/Contents/10.Dynamic-Programming/03.Linear-DP/01.Linear-DP.md index 9c661048..df5f715a 100644 --- a/Contents/10.Dynamic-Programming/03.Linear-DP/01.Linear-DP.md +++ b/Contents/10.Dynamic-Programming/03.Linear-DP/01.Linear-DP.md @@ -1,8 +1,675 @@ -## 1. 线性 DP 简介 +## 1. 线性动态规划简介 -> **线性 DP**:具有「线性」阶段划分的动态规划方法统称为线性动态规划(简称为「线性 DP」)。 +> **线性动态规划**:具有「线性」阶段划分的动态规划方法统称为线性动态规划(简称为「线性 DP」),如下图所示。 + +![](https://qcdn.itcharge.cn/images/202303122358154.png) + +如果状态包含多个维度,但是每个维度上都是线性划分的阶段,也属于线性 DP。比如背包问题、区间 DP、数位 DP 等都属于线性 DP。 + +线性 DP 问题的划分方法有多种方式。 + +- 如果按照「状态的维度数」进行分类,我们可以将线性 DP 问题分为:一维线性 DP 问题、二维线性 DP 问题,以及多维线性 DP 问题。 +- 如果按照「问题的输入格式」进行分类,我们可以将线性 DP 问题分为:单串线性 DP 问题、双串线性 DP 问题、矩阵线性 DP 问题,以及无串线性 DP 问题。 + +本文中,我们将按照问题的输入格式进行分类,对线性 DP 问题中各种类型问题进行一一讲解。 + +## 2. 单串线性 DP 问题 + +> **单串线性 DP** 问题:问题的输入为单个数组或单个字符串的线性 DP 问题。状态一般可定义为 $dp[i],ドル表示为: +> +> 1. 「以数组中第 $i$ 个位置元素 $nums[i]$ 为结尾的子数组($nums[0]...nums[i]$)」的相关解。 +> 2. 「以数组中前 $i$ 个元素为子数组($nums[0]...nums[i - 1]$)」的相关解。 + +这两种状态的定义区别在于相差一个元素 $nums[i]$。前者子数组的长度为 $i + 1,ドル子数组长度不可为空;后者子数组的长度为 $i,ドル子数组长度可为空。后者在 $i = 0$ 时,方便用于表示空数组(以数组中前 0ドル$ 个元素为子数组)。 + +### 2.1 最长递增子序列 + +单串线性 DP 问题中最经典的问题就是「最长递增子序列(Longest Increasing Subsequence,简称 LIS)」。 + +#### 2.1.1 题目链接 + +- [300. 最长递增子序列 - 力扣](https://leetcode.cn/problems/longest-increasing-subsequence/) + +#### 2.1.2 题目大意 + +**描述**:给定一个整数数组 $nums$。 + +**要求**:找到其中最长严格递增子序列的长度。 + +**说明**: + +- **子序列**:由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,$[3,6,2,7]$ 是数组 $[0,3,1,6,2,2,7]$ 的子序列。 +- 1ドル \le nums.length \le 2500$。 +- $-10^4 \le nums[i] \le 10^4$。 + +**示例**: + +- 示例 1: + +```Python +输入:nums = [10,9,2,5,3,7,101,18] +输出:4 +解释:最长递增子序列是 [2,3,7,101],因此长度为 4。 +``` + +- 示例 2: + +```Python +输入:nums = [0,1,0,3,2,3] +输出:4 +``` + +#### 2.1.3 解题思路 + +##### 思路 1:动态规划 + +###### 1. 划分阶段 + +按照子序列的结尾位置进行阶段划分。 + +###### 2. 定义状态 + +定义状态 $dp[i]$ 表示为:以 $nums[i]$ 结尾的最长递增子序列长度。 + +###### 3. 状态转移方程 + +一个较小的数后边如果出现一个较大的数,则会形成一个更长的递增子序列。 + +对于满足 0ドル \le j < i$ 的数组元素 $nums[j]$ 和 $nums[i]$ 来说: + +- 如果 $nums[j] < nums[i],ドル则 $nums[i]$ 可以接在 $nums[j]$ 后面,此时以 $nums[i]$ 结尾的最长递增子序列长度会在「以 $nums[j]$ 结尾的最长递增子序列长度」的基础上加 1ドル,ドル即 $dp[i] = dp[j] + 1$。 + +- 如果 $nums[j] \le nums[i],ドル则 $nums[i]$ 不可以接在 $nums[j]$ 后面,可以直接跳过。 + +综上,我们的状态转移方程为:$dp[i] = max(dp[i], dp[j] + 1),0 \le j < i,nums[j] < nums[i]$。 + +###### 4. 初始条件 + +默认状态下,把数组中的每个元素都作为长度为 1ドル$ 的递增子序列。即 $dp[i] = 1$。 + +###### 5. 最终结果 + +根据我们之前定义的状态,$dp[i]$ 表示为:以 $nums[i]$ 结尾的最长递增子序列长度。那为了计算出最大的最长递增子序列长度,则需要再遍历一遍 $dp$ 数组,求出最大值即为最终结果。 + +##### 思路 1:动态规划代码 + +```Python +class Solution: + def lengthOfLIS(self, nums: List[int]) -> int: + size = len(nums) + dp = [1 for _ in range(size)] + + for i in range(size): + for j in range(i): + if nums[i]> nums[j]: + dp[i] = max(dp[i], dp[j] + 1) + + return max(dp) +``` + +##### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n^2)$。两重循环遍历的时间复杂度是 $O(n^2),ドル最后求最大值的时间复杂度是 $O(n),ドル所以总体时间复杂度为 $O(n^2)$。 +- **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。 + +### 2.2 最大子数组和 + +单串线性 DP 问题中除了子序列相关的线性 DP 问题,还有子数组相关的线性 DP 问题。 + +> **注意**: +> +> - **子序列**:由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。 +> - **子数组**:指的是数组中的一个连续子序列。 +> +> 「子序列」与「子数组」都可以看做是原数组的一部分,而且都不会改变原来数组中元素的相对顺序。其区别在于数组元素是否要求连续。 + +#### 2.2.1 题目链接 + +- [53. 最大子数组和 - 力扣](https://leetcode.cn/problems/maximum-subarray/) + +#### 2.2.2 题目大意 + +**描述**:给定一个整数数组 $nums$。 + +**要求**:找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 + +**说明**: + +- **子数组**:指的是数组中的一个连续部分。 +- 1ドル \le nums.length \le 10^5$。 +- $-10^4 \le nums[i] \le 10^4$。 + +**示例**: + +- 示例 1: + +```Python +输入:nums = [-2,1,-3,4,-1,2,1,-5,4] +输出:6 +解释:连续子数组 [4,-1,2,1] 的和最大,为 6。 +``` + +- 示例 2: + +```Python +输入:nums = [1] +输出:1 +``` + +#### 2.2.3 解题思路 + +##### 思路 1:动态规划 + +###### 1. 划分阶段 + +按照连续子数组的结束位置进行阶段划分。 + +###### 2. 定义状态 + +定义状态 $dp[i]$ 为:以第 $i$ 个数结尾的连续子数组的最大和。 + +###### 3. 状态转移方程 + +状态 $dp[i]$ 为:以第 $i$ 个数结尾的连续子数组的最大和。则我们可以从「第 $i - 1$ 个数结尾的连续子数组的最大和」,以及「第 $i$ 个数的值」来讨论 $dp[i]$。 + +- 如果 $dp[i - 1] < 0,ドル则「第 $i - 1$ 个数结尾的连续子数组的最大和」+「第 $i$ 个数的值」<「第 $i$ 个数的值」,即:$dp[i - 1] + nums[i] < nums[i]$。所以,此时 $dp[i]$ 应取「第 $i$ 个数的值」,即 $dp[i] = nums[i]$。 +- 如果 $dp[i - 1] \ge 0,ドル则「第 $i - 1$ 个数结尾的连续子数组的最大和」 +「第 $i$ 个数的值」>= 第 $i$ 个数的值,即:$dp[i - 1] + nums[i] \ge nums[i]$。所以,此时 $dp[i]$ 应取「第 $i - 1$ 个数结尾的连续子数组的最大和」+「 第 $i$ 个数的值」,即 $dp[i] = dp[i - 1] + nums[i]$。 + +归纳一下,状态转移方程为: + +$dp[i] = \begin{cases} nums[i], & dp[i - 1] < 0 \cr dp[i - 1] + nums[i] & dp[i - 1] \ge 0 \end{cases}$ + +###### 4. 初始条件 + +- 第 0ドル$ 个数结尾的连续子数组的最大和为 $nums[0],ドル即 $dp[0] = nums[0]$。 + +###### 5. 最终结果 + +根据状态定义,$dp[i]$ 为:以第 $i$ 个数结尾的连续子数组的最大和。则最终结果应为所有 $dp[i]$ 的最大值,即 $max(dp)$。 + +##### 思路 1:代码 + +```Python +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + size = len(nums) + dp = [0 for _ in range(size)] + + dp[0] = nums[0] + for i in range(1, size): + if dp[i - 1] < 0: + dp[i] = nums[i] + else: + dp[i] = dp[i - 1] + nums[i] + return max(dp) +``` + +##### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n),ドル其中 $n$ 为数组 $nums$ 的元素个数。 +- **空间复杂度**:$O(n)$。 + +##### 思路 2:动态规划 + 滚动优化 + +因为 $dp[i]$ 只和 $dp[i - 1]$ 和当前元素 $nums[i]$ 相关,我们也可以使用一个变量 $subMax$ 来表示以第 $i$ 个数结尾的连续子数组的最大和。然后使用 $ansMax$ 来保存全局中最大值。 + +##### 思路 2:代码 + +```Python +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + size = len(nums) + subMax = nums[0] + ansMax = nums[0] + + for i in range(1, size): + if subMax < 0: + subMax = nums[i] + else: + subMax += nums[i] + ansMax = max(ansMax, subMax) + return ansMax +``` + +##### 思路 2:复杂度分析 + +- **时间复杂度**:$O(n),ドル其中 $n$ 为数组 $nums$ 的元素个数。 +- **空间复杂度**:$O(1)$。 + +### 2.3 最长的斐波那契子序列的长度 + +有一些单串线性 DP 问题在定义状态时需要考虑两个结束位置,只考虑一个结束位置的无法清楚描述问题。这时候我们就需要需要增加一个结束位置维度来定义状态。 + +#### 2.3.1 题目链接 + +- [873. 最长的斐波那契子序列的长度 - 力扣](https://leetcode.cn/problems/length-of-longest-fibonacci-subsequence/) + +#### 2.3.2 题目大意 + +**描述**:给定一个严格递增的正整数数组 $arr$。 + +**要求**:从数组 $arr$ 中找出最长的斐波那契式的子序列的长度。如果不存斐波那契式的子序列,则返回 0。 + +**说明**: + +- **斐波那契式序列**:如果序列 $X_1, X_2, ..., X_n$ 满足: + + - $n \ge 3$; + - 对于所有 $i + 2 \le n,ドル都有 $X_i + X_{i+1} = X_{i+2}$。 + + 则称该序列为斐波那契式序列。 + +- **斐波那契式子序列**:从序列 $A$ 中挑选若干元素组成子序列,并且子序列满足斐波那契式序列,则称该序列为斐波那契式子序列。例如:$A = [3, 4, 5, 6, 7, 8]$。则 $[3, 5, 8]$ 是 $A$ 的一个斐波那契式子序列。 + +- 3ドル \le arr.length \le 1000$。 + +- 1ドル \le arr[i] < arr[i + 1] \le 10^9$。 + +**示例**: + +- 示例 1: + +```Python +输入: arr = [1,2,3,4,5,6,7,8] +输出: 5 +解释: 最长的斐波那契式子序列为 [1,2,3,5,8]。 +``` + +- 示例 2: + +```Python +输入: arr = [1,3,7,11,12,14,18] +输出: 3 +解释: 最长的斐波那契式子序列有 [1,11,12]、[3,11,14] 以及 [7,11,18]。 +``` + +#### 2.3.3 解题思路 + +##### 思路 1: 暴力枚举(超时) + +假设 $arr[i]$、$arr[j]$、$arr[k]$ 是序列 $arr$ 中的 3ドル$ 个元素,且满足关系:$arr[i] + arr[j] == arr[k],ドル则 $arr[i]$、$arr[j]$、$arr[k]$ 就构成了 $arr$ 的一个斐波那契式子序列。 + +通过 $arr[i]$、$arr[j],ドル我们可以确定下一个斐波那契式子序列元素的值为 $arr[i] + arr[j]$。 + +因为给定的数组是严格递增的,所以对于一个斐波那契式子序列,如果确定了 $arr[i]$、$arr[j],ドル则可以顺着 $arr$ 序列,从第 $j + 1$ 的元素开始,查找值为 $arr[i] + arr[j]$ 的元素 。找到 $arr[i] + arr[j]$ 之后,然后再顺着查找子序列的下一个元素。 + +简单来说,就是确定了 $arr[i]$、$arr[j],ドル就能尽可能的得到一个长的斐波那契式子序列,此时我们记录下子序列长度。然后对于不同的 $arr[i]$、$arr[j],ドル统计不同的斐波那契式子序列的长度。 + +最后将这些长度进行比较,其中最长的长度就是答案。 + +##### 思路 1:代码 + +```Python +class Solution: + def lenLongestFibSubseq(self, arr: List[int]) -> int: + size = len(arr) + ans = 0 + for i in range(size): + for j in range(i + 1, size): + temp_ans = 0 + temp_i = i + temp_j = j + k = j + 1 + while k < size: + if arr[temp_i] + arr[temp_j] == arr[k]: + temp_ans += 1 + temp_i = temp_j + temp_j = k + k += 1 + if temp_ans> ans: + ans = temp_ans + + if ans> 0: + return ans + 2 + else: + return ans +``` + +##### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n^3),ドル其中 $n$ 为数组 $arr$ 的元素个数。 +- **空间复杂度**:$O(1)$。 + +##### 思路 2:哈希表 + +对于 $arr[i]$、$arr[j],ドル要查找的元素 $arr[i] + arr[j]$ 是否在 $arr$ 中,我们可以预先建立一个反向的哈希表。键值对关系为 $value : idx,ドル这样就能在 $O(1)$ 的时间复杂度通过 $arr[i] + arr[j]$ 的值查找到对应的 $arr[k],ドル而不用像原先一样线性查找 $arr[k]$ 了。 + +##### 思路 2:代码 + +```Python +class Solution: + def lenLongestFibSubseq(self, arr: List[int]) -> int: + size = len(arr) + ans = 0 + idx_map = dict() + for idx, value in enumerate(arr): + idx_map[value] = idx + + for i in range(size): + for j in range(i + 1, size): + temp_ans = 0 + temp_i = i + temp_j = j + while arr[temp_i] + arr[temp_j] in idx_map: + temp_ans += 1 + k = idx_map[arr[temp_i] + arr[temp_j]] + temp_i = temp_j + temp_j = k + + if temp_ans> ans: + ans = temp_ans + + if ans> 0: + return ans + 2 + else: + return ans +``` + +##### 思路 2:复杂度分析 + +- **时间复杂度**:$O(n^2),ドル其中 $n$ 为数组 $arr$ 的元素个数。 +- **空间复杂度**:$O(n)$。 + +##### 思路 3:动态规划 + 哈希表 + +###### 1. 划分阶段 + +按照斐波那契式子序列相邻两项的结尾位置进行阶段划分。 + +###### 2. 定义状态 + +定义状态 $dp[i][j]$ 表示为:以 $arr[i]$、$arr[j]$ 为结尾的斐波那契式子序列的最大长度。 + +###### 3. 状态转移方程 + +以 $arr[j]$、$arr[k]$ 结尾的斐波那契式子序列的最大长度 = 满足 $arr[i] + arr[j] = arr[k]$ 条件下,以 $arr[i]$、$arr[j]$ 结尾的斐波那契式子序列的最大长度加 1ドル$。即状态转移方程为:$dp[j][k] = max_{(A[i] + A[j] = A[k],i < j < k)}(dp[i][j] + 1)$。 + +###### 4. 初始条件 + +默认状态下,数组中任意相邻两项元素都可以作为长度为 2ドル$ 的斐波那契式子序列,即 $dp[i][j] = 2$。 + +###### 5. 最终结果 + +根据我们之前定义的状态,$dp[i][j]$ 表示为:以 $arr[i]$、$arr[j]$ 为结尾的斐波那契式子序列的最大长度。那为了计算出最大的最长递增子序列长度,则需要在进行状态转移时,求出最大值 $ans$ 即为最终结果。 + +因为题目定义中,斐波那契式中 $n \ge 3,ドル所以只有当 $ans \ge 3$ 时,返回 $ans$。如果 $ans < 3,ドル则返回 0ドル$。 + +> **注意**:在进行状态转移的同时,我们应和「思路 2:哈希表」一样采用哈希表优化的方式来提高效率,降低算法的时间复杂度。 + +##### 思路 3:代码 + +```Python +class Solution: + def lenLongestFibSubseq(self, arr: List[int]) -> int: + size = len(arr) + + dp = [[0 for _ in range(size)] for _ in range(size)] + ans = 0 + + # 初始化 dp + for i in range(size): + for j in range(i + 1, size): + dp[i][j] = 2 + + idx_map = {} + # 将 value : idx 映射为哈希表,这样可以快速通过 value 获取到 idx + for idx, value in enumerate(arr): + idx_map[value] = idx + + for i in range(size): + for j in range(i + 1, size): + if arr[i] + arr[j] in idx_map: + # 获取 arr[i] + arr[j] 的 idx,即斐波那契式子序列下一项元素 + k = idx_map[arr[i] + arr[j]] + + dp[j][k] = max(dp[j][k], dp[i][j] + 1) + ans = max(ans, dp[j][k]) + + if ans>= 3: + return ans + return 0 +``` + +##### 思路 3:复杂度分析 + +- **时间复杂度**:$O(n^2),ドル其中 $n$ 为数组 $arr$ 的元素个数。 +- **空间复杂度**:$O(n)$。 + +## 3. 双串线性 DP 问题 + +> **双串线性 DP 问题**:问题的输入为两个数组或两个字符串的线性 DP 问题。状态一般可定义为 $dp[i][j],ドル表示为: > -> - 如果状态包含多个维度,则每个维度都是线性划分的阶段,也属于线性 DP。 +> 1. 「以第一个数组中第 $i$ 个位置元素 $nums1[i]$ 为结尾的子数组($nums1[0]...nums1[i]$)」与「以第二个数组中第 $j$ 个位置元素 $nums2[j]$ 为结尾的子数组($nums2[0]...nums2[j]$)」的相关解。 +> 2. 「以第一个数组中前 $i$ 个元素为子数组($nums1[0]...nums1[i - 1]$)」与「以第二个数组中前 $j$ 个元素为子数组($nums2[0]...nums2[j - 1]$)」的相关解。 + +这两种状态的定义区别在于相差一个元素 $nums1[i]$ 或 $nums2[j]$。前者子数组的长度为 $i + 1$ 或 $j + 1,ドル子数组长度不可为空;后者子数组的长度为 $i$ 或 $j,ドル子数组长度可为空。后者在 $i = 0$ 或 $j = 0$ 时,方便用于表示空数组(以数组中前 0ドル$ 个元素为子数组)。 + +### 3.1 最长公共子序列 + +双串线性 DP 问题中最经典的问题就是「最长公共子序列(Longest Common Subsequence,简称 LCS)」。 + +#### 3.1.1 题目链接 + +- [1143. 最长公共子序列 - 力扣](https://leetcode.cn/problems/longest-common-subsequence/) + +#### 3.1.2 题目大意 + +**描述**:给定两个字符串 $text1$ 和 $text2$。 + +**要求**:返回两个字符串的最长公共子序列的长度。如果不存在公共子序列,则返回 0ドル$。 + +**说明**: + +- **子序列**:原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 +- **公共子序列**:两个字符串所共同拥有的子序列。 +- 1ドル \le text1.length, text2.length \le 1000$。 +- $text1$ 和 $text2$ 仅由小写英文字符组成。 + +**示例**: + +- 示例 1: + +```Python +输入:text1 = "abcde", text2 = "ace" +输出:3 +解释:最长公共子序列是 "ace",它的长度为 3。 +``` + +- 示例 2: + +```Python +输入:text1 = "abc", text2 = "abc" +输出:3 +解释:最长公共子序列是 "abc",它的长度为 3。 +``` + +#### 3.1.3 解题思路 + +##### 思路 1:动态规划 + +###### 1. 划分阶段 + +按照两个字符串的结尾位置进行阶段划分。 + +###### 2. 定义状态 + +定义状态 $dp[i][j]$ 表示为:「以 $text1$ 中前 $i$ 个元素组成的子字符串 $str1$ 」与「以 $text2$ 中前 $j$ 个元素组成的子字符串 $str2$」的最长公共子序列长度为 $dp[i][j]$。 + +###### 3. 状态转移方程 + +双重循环遍历字符串 $text1$ 和 $text2,ドル则状态转移方程为: + +1. 如果 $text1[i - 1] = text2[j - 1],ドル说明两个子字符串的最后一位是相同的,所以最长公共子序列长度加 1ドル$。即:$dp[i][j] = dp[i - 1][j - 1] + 1$。 +2. 如果 $text1[i - 1] \ne text2[j - 1],ドル说明两个子字符串的最后一位是不同的,则 $dp[i][j]$ 需要考虑以下两种情况,取两种情况中最大的那种:$dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])$。 + 1. 「以 $text1$ 中前 $i - 1$ 个元素组成的子字符串 $str1$ 」与「以 $text2$ 中前 $j$ 个元素组成的子字符串 $str2$」的最长公共子序列长度,即 $dp[i - 1][j]$。 + 2. 「以 $text1$ 中前 $i$ 个元素组成的子字符串 $str1$ 」与「以 $text2$ 中前 $j - 1$ 个元素组成的子字符串 $str2$」的最长公共子序列长度,即 $dp[i][j - 1]$。 + +###### 4. 初始条件 + +1. 当 $i = 0$ 时,$str1$ 表示的是空串,空串与 $str2$ 的最长公共子序列长度为 0ドル,ドル即 $dp[0][j] = 0$。 +2. 当 $j = 0$ 时,$str2$ 表示的是空串,$str1$ 与 空串的最长公共子序列长度为 0ドル,ドル即 $dp[i][0] = 0$。 + +###### 5. 最终结果 + +根据状态定义,最后输出 $dp[sise1][size2]$(即 $text1$ 与 $text2$ 的最长公共子序列长度)即可,其中 $size1$、$size2$ 分别为 $text1$、$text2$ 的字符串长度。 + +##### 思路 1:代码 + +```Python +class Solution: + def longestCommonSubsequence(self, text1: str, text2: str) -> int: + size1 = len(text1) + size2 = len(text2) + dp = [[0 for _ in range(size2 + 1)] for _ in range(size1 + 1)] + for i in range(1, size1 + 1): + for j in range(1, size2 + 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 dp[size1][size2] +``` + +##### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n \times m),ドル其中 $n$、$m$ 分别是字符串 $text1$、$text2$ 的长度。两重循环遍历的时间复杂度是 $O(n \times m),ドル所以总的时间复杂度为 $O(n \times m)$。 +- **空间复杂度**:$O(n \times m)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n \times m)$。 + +### 3.2 最长重复子数组 + +#### 3.2.1 题目链接 + +- [718. 最长重复子数组 - 力扣](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/) + +#### 3.2.2 题目大意 + +**描述**:给定两个整数数组 $nums1$、$nums2$。 + +**要求**:计算两个数组中公共的、长度最长的子数组长度。 + +**说明**: + +- 1ドル \le nums1.length, nums2.length \le 1000$。 +- 0ドル \le nums1[i], nums2[i] \le 100$。 + +**示例**: + +- 示例 1: + +```Python +输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7] +输出:3 +解释:长度最长的公共子数组是 [3,2,1] 。 +``` + +- 示例 2: + +```Python +输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0] +输出:5 +``` + +#### 3.2.3 解题思路 + +##### 思路 1:动态规划 + +###### 1. 划分阶段 + +按照子数组结尾位置进行阶段划分。 + +###### 2. 定义状态 + +定义状态 $dp[i][j]$ 为:「以 $nums1$ 中前 $i$ 个元素为子数组」和「以 $nums2$ 中前 $j$ 个元素为子数组($nums2[0]...nums2[j - 1]$)」的最长公共子数组长度。 + +###### 3. 状态转移方程 + +1. 如果 $nums1[i] = nums2[j],ドル则当前元素可以构成公共子数组,此时 $dp[i][j] = dp[i - 1][j - 1] + 1$。 +2. 如果 $nums1[i] \ne nums2[j],ドル则当前元素不能构成公共子数组,此时 $dp[i][j] = 0$。 + +###### 4. 初始条件 + +- 当 $i = 0$ 时,$nums1[0]...nums1[i - 1]$ 表示的是空数组,空数组与 $nums2[0]...nums2[j - 1]$ 的最长公共子序列长度为 0ドル,ドル即 $dp[0][j] = 0$。 +- 当 $j = 0$ 时,$nums2[0]...nums2[j - 1]$ 表示的是空数组,空数组与 $nums1[0]...nums1[i - 1]$ 的最长公共子序列长度为 0ドル,ドル即 $dp[i][0] = 0$。 + +###### 5. 最终结果 + +- 根据状态定义, $dp[i][j]$ 为:「以 $nums1$ 中前 $i$ 个元素为子数组」和「以 $nums2$ 中前 $j$ 个元素为子数组($nums2[0]...nums2[j - 1]$)」的最长公共子数组长度。在遍历过程中,我们可以使用 $res$ 记录下所有 $dp[i][j]$ 中最大值即为答案。 + +##### 思路 1:代码 + +```Python +class Solution: + def findLength(self, nums1: List[int], nums2: List[int]) -> int: + size1 = len(nums1) + size2 = len(nums2) + dp = [[0 for _ in range(size2 + 1)] for _ in range(size1 + 1)] + res = 0 + for i in range(1, size1 + 1): + for j in range(1, size2 + 1): + if nums1[i - 1] == nums2[j - 1]: + dp[i][j] = dp[i - 1][j - 1] + 1 + if dp[i][j]> res: + res = dp[i][j] + + return res +``` + +##### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n \times m)$。其中 $n$ 是数组 $nums1$ 的长度,$m$ 是数组 $nums2$ 的长度。 +- **空间复杂度**:$O(n \times m)$。 + +### 3.3 编辑距离 + +#### 3.3.1 题目链接 + +#### 3.3.2 题目大意 + +#### 3.3.3 解题思路 + +## 4. 矩阵线性 DP问题 + +> **矩阵线性 DP 问题**:问题的输入为二维矩阵的线性 DP 问题。状态一般可定义为 $dp[i][j],ドル表示为:从「位置 $(0, 0)$」到达「位置 $(i, j)$」的相关解。 + +### 4.1 最小路径和 + +#### 4.1.1 题目链接 + +#### 4.1.2 题目大意 + +#### 4.1.3 解题思路 + +### 4.2 最大正方形 + +#### 4.2.1 题目链接 + +#### 4.2.2 题目大意 + +#### 4.2.3 解题思路 + +## 5. 无串线性 DP 问题 + +> **无串线性 DP 问题**:问题的输入不是显式的数组或字符串,但依然可分解为若干子问题的线性 DP 问题。 + +### 5.1 整数拆分 + +#### 5.1.1 题目链接 + +#### 5.1.2 题目大意 + +#### 5.1.3 解题思路 + +### 5.2 只有两个键的键盘 + +#### 5.2.1 题目链接 + +#### 5.2.2 题目大意 + +#### 5.2.3 解题思路 + +## 参考资料 -## 2. 线性 DP 的应用 +- 【书籍】算法竞赛进阶指南 +- 【文章】[动态规划概念和基础线性DP | 潮汐朝夕](https://chengzhaoxi.xyz/1a4a2483.html) diff --git "a/Solutions/0062. 344円270円215円345円220円214円350円267円257円345円276円204円.md" "b/Solutions/0062. 344円270円215円345円220円214円350円267円257円345円276円204円.md" index 31b2e649..6e2ce081 100644 --- "a/Solutions/0062. 344円270円215円345円220円214円350円267円257円345円276円204円.md" +++ "b/Solutions/0062. 344円270円215円345円220円214円350円267円257円345円276円204円.md" @@ -5,7 +5,7 @@ ## 题目大意 -**描述**:给定两个整数 `m` 和 `n`,代表大小为 `m * n` 的棋盘, 一个机器人位于棋盘左上角的位置,机器人每次只能向右、或者向下移动一步。 +**描述**:给定两个整数 $m$ 和 $n,ドル代表大小为 $m \times n$ 的棋盘, 一个机器人位于棋盘左上角的位置,机器人每次只能向右、或者向下移动一步。 **要求**:计算出机器人从棋盘左上角到达棋盘右下角一共有多少条不同的路径。 @@ -47,21 +47,21 @@ ###### 2. 定义状态 -定义状态 `dp[i][j]` 为:从左上角到达 `(i, j)` 位置的路径数量。 +定义状态 $dp[i][j]$ 为:从左上角到达位置 $(i, j)$ 的路径数量。 ###### 3. 状态转移方程 -因为我们每次只能向右、或者向下移动一步,因此想要走到 `(i, j)`,只能从 `(i - 1, j)` 向下走一步走过来;或者从 `(i, j - 1)` 向右走一步走过来。所以可以写出状态转移方程为:`dp[i][j] = dp[i - 1][j] + dp[i][j - 1]`,此时 `i> 0,j> 0`。 +因为我们每次只能向右、或者向下移动一步,因此想要走到 $(i, j),ドル只能从 $(i - 1, j)$ 向下走一步走过来;或者从 $(i, j - 1)$ 向右走一步走过来。所以可以写出状态转移方程为:$dp[i][j] = dp[i - 1][j] + dp[i][j - 1],ドル此时 $i> 0,j> 0$。 ###### 4. 初始条件 -- 从左上角走到 `(0, 0)` 只有一种方法,即 `dp[0][0] = 1`。 -- 第一行元素只有一条路径(即只能通过前一个元素向右走得到),所以 `dp[0][j] = 1`。 -- 同理,第一列元素只有一条路径(即只能通过前一个元素向下走得到),所以 `dp[i][0] = 1`。 +- 从左上角走到 $(0, 0)$ 只有一种方法,即 $dp[0][0] = 1$。 +- 第一行元素只有一条路径(即只能通过前一个元素向右走得到),所以 $dp[0][j] = 1$。 +- 同理,第一列元素只有一条路径(即只能通过前一个元素向下走得到),所以 $dp[i][0] = 1$。 ###### 5. 最终结果 -根据状态定义,最终结果为 `dp[m - 1][n - 1]`,即从左上角到达右下角 `(m - 1, n - 1)` 位置的路径数量为 `dp[m - 1][n - 1]`。 +根据状态定义,最终结果为 $dp[m - 1][n - 1],ドル即从左上角到达右下角 $(m - 1, n - 1)$ 位置的路径数量为 $dp[m - 1][n - 1]$。 ### 思路 1:动态规划代码 @@ -85,4 +85,4 @@ class Solution: ### 思路 1:复杂度分析 - **时间复杂度**:$O(m * n)$。初始条件赋值的时间复杂度为 $O(m + n),ドル两重循环遍历的时间复杂度为 $O(m * n),ドル所以总体时间复杂度为 $O(m * n)$。 -- **空间复杂度**:$O(m * n)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(m * n)$。因为 `dp[i][j]` 的状态只依赖于上方值 `dp[i - 1][j]` 和左侧值 `dp[i][j - 1]`,而我们在进行遍历时的顺序刚好是从上至下、从左到右。所以我们可以使用长度为 `m` 的一维数组来保存状态,从而将空间复杂度优化到 $O(m)$。 \ No newline at end of file +- **空间复杂度**:$O(m * n)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(m * n)$。因为 $dp[i][j]$ 的状态只依赖于上方值 $dp[i - 1][j]$ 和左侧值 $dp[i][j - 1],ドル而我们在进行遍历时的顺序刚好是从上至下、从左到右。所以我们可以使用长度为 $m$ 的一维数组来保存状态,从而将空间复杂度优化到 $O(m)$。 \ No newline at end of file diff --git "a/Solutions/0070. 347円210円254円346円245円274円346円242円257円.md" "b/Solutions/0070. 347円210円254円346円245円274円346円242円257円.md" index eadd87b5..2afd866b 100644 --- "a/Solutions/0070. 347円210円254円346円245円274円346円242円257円.md" +++ "b/Solutions/0070. 347円210円254円346円245円274円346円242円257円.md" @@ -42,10 +42,10 @@ 根据我们的递推三步走策略,写出对应的递归代码。 -1. 写出递推公式:`f(n) = f(n - 1) + f(n - 2)`。 -2. 明确终止条件:`f(0) = 0, f(1) = 1`。 +1. 写出递推公式:$f(n) = f(n - 1) + f(n - 2)$。 +2. 明确终止条件:$f(0) = 0, f(1) = 1$。 3. 翻译为递归代码: - 1. 定义递归函数:`climbStairs(self, n)` 表示输入参数为问题的规模 `n`,返回结果为爬 $n$ 阶台阶到达楼顶的方案数。 + 1. 定义递归函数:`climbStairs(self, n)` 表示输入参数为问题的规模 $n,ドル返回结果为爬 $n$ 阶台阶到达楼顶的方案数。 2. 书写递归主体:`return self.climbStairs(n - 1) + self.climbStairs(n - 2)`。 3. 明确递归终止条件: 1. `if n == 0: return 0` @@ -72,25 +72,25 @@ class Solution: ###### 1. 划分阶段 -按照台阶的层数进行划分为 `0` ~ `n`。 +按照台阶的层数进行划分为 0ドル \sim n$。 ###### 2. 定义状态 -定义状态 `dp[i]` 为:爬到第 `i` 阶台阶的方案数。 +定义状态 $dp[i]$ 为:爬到第 $i$ 阶台阶的方案数。 ###### 3. 状态转移方程 -根据题目大意,每次只能爬 `1` 或 `2` 个台阶。则第 `i` 阶楼梯只能从第 `i - 1` 阶向上爬 `1`阶上来,或者从第 `i - 2` 阶向上爬 `2` 阶上来。所以可以推出状态转移方程为 `dp[i] = dp[i - 1] + dp[i - 2]`。 +根据题目大意,每次只能爬 1ドル$ 或 2ドル$ 个台阶。则第 $i$ 阶楼梯只能从第 $i - 1$ 阶向上爬 1ドル$ 阶上来,或者从第 $i - 2$ 阶向上爬 2ドル$ 阶上来。所以可以推出状态转移方程为 $dp[i] = dp[i - 1] + dp[i - 2]$。 ###### 4. 初始条件 -- 第 `0` 层台阶方案数:可以看做 `1` 种方法(从 `0` 阶向上爬 `0` 阶),即 `dp[0] = 1`。 -- 第 `1` 层台阶方案数:`1` 种方法(从 `0` 阶向上爬 `1` 阶),即 `dp[1] = 1`。 -- 第 `2` 层台阶方案数:`2` 中方法(从 `0` 阶向上爬 `2` 阶,或者从 `1` 阶向上爬 `1` 阶)。 +- 第 0ドル$ 层台阶方案数:可以看做 1ドル$ 种方法(从 0ドル$ 阶向上爬 0ドル$ 阶),即 $dp[0] = 1$。 +- 第 1ドル$ 层台阶方案数:1ドル$ 种方法(从 0ドル$ 阶向上爬 1ドル$ 阶),即 $dp[1] = 1$。 +- 第 2ドル$ 层台阶方案数:2ドル$ 种方法(从 0ドル$ 阶向上爬 2ドル$ 阶,或者从 1ドル$ 阶向上爬 1ドル$ 阶)。 ###### 5. 最终结果 -根据状态定义,最终结果为 `dp[n]`,即爬到第 `n` 阶台阶(即楼顶)的方案数为 `dp[n]`。 +根据状态定义,最终结果为 $dp[n],ドル即爬到第 $n$ 阶台阶(即楼顶)的方案数为 $dp[n]$。 ### 思路 2:代码 @@ -109,5 +109,5 @@ class Solution: ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。 -- **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。因为 `dp[i]` 的状态只依赖于 `dp[i - 1]` 和 `dp[i - 2]`,所以可以使用 `3` 个变量来分别表示 `dp[i]`、`dp[i - 1]`、`dp[i - 2]`,从而将空间复杂度优化到 $O(1)$。 +- **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。因为 $dp[i]$ 的状态只依赖于 $dp[i - 1]$ 和 $dp[i - 2],ドル所以可以使用 3ドル$ 个变量来分别表示 $dp[i]$、$dp[i - 1]$、$dp[i - 2],ドル从而将空间复杂度优化到 $O(1)$。 diff --git "a/Solutions/0189. 350円275円256円350円275円254円346円225円260円347円273円204円.md" "b/Solutions/0189. 350円275円256円350円275円254円346円225円260円347円273円204円.md" index f48f5493..a80b8eec 100644 --- "a/Solutions/0189. 350円275円256円350円275円254円346円225円260円347円273円204円.md" +++ "b/Solutions/0189. 350円275円256円350円275円254円346円225円260円347円273円204円.md" @@ -59,7 +59,7 @@ class Solution: nums[left] = nums[right] nums[right] = tmp left += 1 - right -= 1https://lestore.lenovo.com/detail/25113 + right -= 1 ``` ### 思路 1:复杂度分析 diff --git "a/Solutions/0509. 346円226円220円346円263円242円351円202円243円345円245円221円346円225円260円.md" "b/Solutions/0509. 346円226円220円346円263円242円351円202円243円345円245円221円346円225円260円.md" index 79b2c4fa..35247ace 100644 --- "a/Solutions/0509. 346円226円220円346円263円242円351円202円243円345円245円221円346円225円260円.md" +++ "b/Solutions/0509. 346円226円220円346円263円242円351円202円243円345円245円221円346円225円260円.md" @@ -40,10 +40,10 @@ 根据我们的递推三步走策略,写出对应的递归代码。 -1. 写出递推公式:`f(n) = f(n - 1) + f(n - 2)`。 -2. 明确终止条件:`f(0) = 0, f(1) = 1`。 +1. 写出递推公式:$f(n) = f(n - 1) + f(n - 2)$。 +2. 明确终止条件:$f(0) = 0, f(1) = 1$。 3. 翻译为递归代码: - 1. 定义递归函数:`fib(self, n)` 表示输入参数为问题的规模 `n`,返回结果为第 `n` 个斐波那契数。 + 1. 定义递归函数:`fib(self, n)` 表示输入参数为问题的规模 $n,ドル返回结果为第 $n$ 个斐波那契数。 2. 书写递归主体:`return self.fib(n - 1) + self.fib(n - 2)`。 3. 明确递归终止条件: 1. `if n == 0: return 0` @@ -70,23 +70,23 @@ class Solution: ###### 1. 划分阶段 -我们可以按照整数顺序进行阶段划分,将其划分为整数 `0` ~ `n`。 +我们可以按照整数顺序进行阶段划分,将其划分为整数 0ドル \sim n$。 ###### 2. 定义状态 -定义状态 `dp[i]` 为:第 `i` 个斐波那契数。 +定义状态 $dp[i]$ 为:第 $i$ 个斐波那契数。 ###### 3. 状态转移方程 -根据题目中所给的斐波那契数列的定义 `f(n) = f(n - 1) + f(n - 2)`,则直接得出状态转移方程为 `dp[i] = dp[i - 1] + dp[i - 2]`。 +根据题目中所给的斐波那契数列的定义 $f(n) = f(n - 1) + f(n - 2),ドル则直接得出状态转移方程为 $dp[i] = dp[i - 1] + dp[i - 2]$。 ###### 4. 初始条件 -根据题目中所给的初始条件 `f(0) = 0, f(1) = 1` 确定动态规划的初始条件,即 `dp[0] = 0, dp[1] = 1`。 +根据题目中所给的初始条件 $f(0) = 0, f(1) = 1$ 确定动态规划的初始条件,即 $dp[0] = 0, dp[1] = 1$。 ###### 5. 最终结果 -根据状态定义,最终结果为 `dp[n]`,即第 `n` 个斐波那契数为 `dp[n]`。 +根据状态定义,最终结果为 $dp[n],ドル即第 $n$ 个斐波那契数为 $dp[n]$。 ### 思路 2:代码 @@ -108,4 +108,4 @@ class Solution: ### 思路 2:复杂度分析 - **时间复杂度**:$O(n)$。一重循环遍历的时间复杂度为 $O(n)$。 -- **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。因为 `dp[i]` 的状态只依赖于 `dp[i - 1]` 和 `dp[i - 2]`,所以可以使用 `3` 个变量来分别表示 `dp[i]`、`dp[i - 1]`、`dp[i - 2]`,从而将空间复杂度优化到 $O(1)$。 +- **空间复杂度**:$O(n)$。用到了一维数组保存状态,所以总体空间复杂度为 $O(n)$。因为 $dp[i]$ 的状态只依赖于 $dp[i - 1]$ 和 $dp[i - 2],ドル所以可以使用 3ドル$ 个变量来分别表示 $dp[i]$、$dp[i - 1]$、$dp[i - 2],ドル从而将空间复杂度优化到 $O(1)$。 diff --git "a/Solutions/0718. 346円234円200円351円225円277円351円207円215円345円244円215円345円255円220円346円225円260円347円273円204円.md" "b/Solutions/0718. 346円234円200円351円225円277円351円207円215円345円244円215円345円255円220円346円225円260円347円273円204円.md" index bdb1d939..93d3fbec 100644 --- "a/Solutions/0718. 346円234円200円351円225円277円351円207円215円345円244円215円345円255円220円346円225円260円347円273円204円.md" +++ "b/Solutions/0718. 346円234円200円351円225円277円351円207円215円345円244円215円345円255円220円346円225円260円347円273円204円.md" @@ -5,7 +5,7 @@ ## 题目大意 -**描述**:给定两个整数数组 `nums1`、`nums2`。 +**描述**:给定两个整数数组 $nums1$、$nums2$。 **要求**:计算两个数组中公共的、长度最长的子数组长度。 @@ -35,10 +35,10 @@ ### 思路 1:暴力(超时) -1. 枚举数组 `nums1` 和 `nums2` 的子数组开始位置 `i`、`j`。 -2. 如果遇到相同项,即 `nums1[i] == nums2[j]`,则以 `nums1[i]`、`nums2[j]` 为前缀,同时向后遍历,计算当前的公共子数组长度 `sub_len` 最长为多少。 -3. 直到遇到超出数组范围或者 `nums1[i + sub_len] == nums2[j + sub_len]` 情况时,停止遍历,并更新答案。 -4. 继续执行 1 ~ 3 步,直到遍历完,输出答案。 +1. 枚举数组 $nums1$ 和 $nums2$ 的子数组开始位置 $i$、$j$。 +2. 如果遇到相同项,即 $nums1[i] == nums2[j],ドル则以 $nums1[i]$、$nums2[j]$ 为前缀,同时向后遍历,计算当前的公共子数组长度 $subLen$ 最长为多少。 +3. 直到遇到超出数组范围或者 $nums1[i + subLen] == nums2[j + subLen]$ 情况时,停止遍历,并更新答案。 +4. 继续执行 1ドル \sim 3$ 步,直到遍历完,输出答案。 ### 思路 1:代码 @@ -50,31 +50,31 @@ class Solution: for i in range(size1): for j in range(size2): if nums1[i] == nums2[j]: - sub_len = 1 - while i + sub_len < size1 and j + sub_len < size2 and nums1[i + sub_len] == nums2[j + sub_len]: - sub_len += 1 - ans = max(ans, sub_len) + subLen = 1 + while i + subLen < size1 and j + subLen < size2 and nums1[i + subLen] == nums2[j + subLen]: + subLen += 1 + ans = max(ans, subLen) return ans ``` ### 思路 1:复杂度分析 -- **时间复杂度**:$O(n \times m \times min(n, m))$。其中 $n$ 是数组 `nums1` 的长度,$m$ 是数组 `nums2` 的长度。 +- **时间复杂度**:$O(n \times m \times min(n, m))$。其中 $n$ 是数组 $nums1$ 的长度,$m$ 是数组 $nums2$ 的长度。 - **空间复杂度**:$O(1)$。 ### 思路 2:滑动窗口 暴力方法中,因为子数组在两个数组中的位置不同,所以会导致子数组之间会进行多次比较。 -我们可以将两个数组分别看做是两把直尺。然后将数组 `nums1` 固定, 让 `nums2` 的尾部与 `nums1` 的头部对齐,如下所示。 +我们可以将两个数组分别看做是两把直尺。然后将数组 $nums1$ 固定, 让 $nums2$ 的尾部与 $nums1$ 的头部对齐,如下所示。 ```Python nums1 = [1, 2, 3, 2, 1] nums2 = [3, 2, 1, 4, 7] ``` -然后逐渐向右移动直尺 `nums2`,比较 `nums1` 与 `nums2` 重叠部分中的公共子数组的长度,直到直尺 `nums2` 的头部移动到 `nums1` 的尾部。 +然后逐渐向右移动直尺 $nums2,ドル比较 $nums1$ 与 $nums2$ 重叠部分中的公共子数组的长度,直到直尺 $nums2$ 的头部移动到 $nums1$ 的尾部。 ```Python nums1 = [1, 2, 3, 2, 1] @@ -102,7 +102,7 @@ nums1 = [1, 2, 3, 2, 1] nums2 = [3, 2, 1, 4, 7] ``` -在这个过程中求得的 `nums1` 与 `nums2` 重叠部分中的最大的公共子数组的长度就是 `nums1` 与 `nums2` 数组中公共的、长度最长的子数组长度。 +在这个过程中求得的 $nums1$ 与 $nums2$ 重叠部分中的最大的公共子数组的长度就是 $nums1$ 与 $nums2$ 数组中公共的、长度最长的子数组长度。 ### 思路 2:代码 @@ -136,7 +136,7 @@ class Solution: ### 思路 2:复杂度分析 -- **时间复杂度**:$O(n + m) \times min(n, m)$。其中 $n$ 是数组 `nums1` 的长度,$m$ 是数组 `nums2` 的长度。 +- **时间复杂度**:$O(n + m) \times min(n, m)$。其中 $n$ 是数组 $nums1$ 的长度,$m$ 是数组 $nums2$ 的长度。 - **空间复杂度**:$O(1)$。 ### 思路 3:动态规划 @@ -147,20 +147,21 @@ class Solution: ###### 2. 定义状态 -定义状态 `dp[i][j]` 为:以 `nums1[i - 1]` 结尾和 `nums2[j - 1]` 结尾的最长公共子数组长度。 +定义状态 $dp[i][j]$ 为:「以 $nums1$ 中前 $i$ 个元素为子数组」和「以 $nums2$ 中前 $j$ 个元素为子数组($nums2[0]...nums2[j - 1]$)」的最长公共子数组长度。 ###### 3. 状态转移方程 -1. 如果 `nums1[i] == nums2[j]`,则当前元素可以构成公共子数组,此时 `dp[i][j] = dp[i - 1][j - 1] + 1`。 -2. 如果 `nums1[i] != nums2[j]`,则当前元素不能构成公共子数组,此时 `dp[i][j] = 0`。 +1. 如果 $nums1[i] = nums2[j],ドル则当前元素可以构成公共子数组,此时 $dp[i][j] = dp[i - 1][j - 1] + 1$。 +2. 如果 $nums1[i] \ne nums2[j],ドル则当前元素不能构成公共子数组,此时 $dp[i][j] = 0$。 ###### 4. 初始条件 -- 初始状态下,默认以 `nums1[i - 1]` 结尾和 `nums2[j - 1]` 结尾的最长公共子数组长度为 `0`,即 `dp[i][j] = 0`。 +- 当 $i = 0$ 时,$nums1[0]...nums1[i - 1]$ 表示的是空数组,空数组与 $nums2[0]...nums2[j - 1]$ 的最长公共子序列长度为 0ドル,ドル即 $dp[0][j] = 0$。 +- 当 $j = 0$ 时,$nums2[0]...nums2[j - 1]$ 表示的是空数组,空数组与 $nums1[0]...nums1[i - 1]$ 的最长公共子序列长度为 0ドル,ドル即 $dp[i][0] = 0$。 ###### 5. 最终结果 -- 使用 `res` 记录下所有 `dp[i][j]` 中最大值即为答案。 +- 根据状态定义, $dp[i][j]$ 为:「以 $nums1$ 中前 $i$ 个元素为子数组」和「以 $nums2$ 中前 $j$ 个元素为子数组($nums2[0]...nums2[j - 1]$)」的最长公共子数组长度。在遍历过程中,我们可以使用 $res$ 记录下所有 $dp[i][j]$ 中最大值即为答案。 ### 思路 3:代码 @@ -183,5 +184,5 @@ class Solution: ### 思路 3:复杂度分析 -- **时间复杂度**:$O(n \times m)$。其中 $n$ 是数组 `nums1` 的长度,$m$ 是数组 `nums2` 的长度。 +- **时间复杂度**:$O(n \times m)$。其中 $n$ 是数组 $nums1$ 的长度,$m$ 是数组 $nums2$ 的长度。 - **空间复杂度**:$O(n \times m)$。 \ No newline at end of file diff --git "a/Solutions/1143. 346円234円200円351円225円277円345円205円254円345円205円261円345円255円220円345円272円217円345円210円227円.md" "b/Solutions/1143. 346円234円200円351円225円277円345円205円254円345円205円261円345円255円220円345円272円217円345円210円227円.md" index 12ea23e9..f13b1fcb 100644 --- "a/Solutions/1143. 346円234円200円351円225円277円345円205円254円345円205円261円345円255円220円345円272円217円345円210円227円.md" +++ "b/Solutions/1143. 346円234円200円351円225円277円345円205円254円345円205円261円345円255円220円345円272円217円345円210円227円.md" @@ -5,16 +5,16 @@ ## 题目大意 -**描述**:给定两个字符串 `text1` 和 `text2`。 +**描述**:给定两个字符串 $text1$ 和 $text2$。 -**要求**:返回两个字符串的最长公共子序列的长度。如果不存在公共子序列,则返回 `0`。 +**要求**:返回两个字符串的最长公共子序列的长度。如果不存在公共子序列,则返回 0ドル$。 **说明**: - **子序列**:原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 - **公共子序列**:两个字符串所共同拥有的子序列。 - 1ドル \le text1.length, text2.length \le 1000$。 -- `text1` 和 `text2` 仅由小写英文字符组成。 +- $text1$ 和 $text2$ 仅由小写英文字符组成。 **示例**: @@ -44,24 +44,25 @@ ###### 2. 定义状态 -定义状态 `dp[i][j]` 表示为:前 `i` 个字符组成的字符串 `str1` 与前 `j` 个字符组成的字符串 `str2` 的最长公共子序列长度为 `dp[i][j]`。 +定义状态 $dp[i][j]$ 表示为:「以 $text1$ 中前 $i$ 个元素组成的子字符串 $str1$ 」与「以 $text2$ 中前 $j$ 个元素组成的子字符串 $str2$」的最长公共子序列长度为 $dp[i][j]$。 ###### 3. 状态转移方程 -双重循环遍历字符串 `text1` 和 `text2`,则状态转移方程为: +双重循环遍历字符串 $text1$ 和 $text2,ドル则状态转移方程为: -- 如果 `text1[i - 1] == text2[j - 1]`,则说明找到了一个公共字符,此时 `dp[i][j] = dp[i - 1][j - 1] + 1`。 -- 如果 `text1[i - 1] != text2[j - 1]`,则 `dp[i][j]` 需要考虑以下两种情况,取两种情况中最大的那种:`dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])`。 - - `text1` 前 `i - 1` 个字符组成的字符串 `str1` 与 `text2` 前 `j` 个字符组成的 `str2` 的最长公共子序列长度,即 `dp[i - 1][j]`。 - - `text1` 前 `i` 个字符组成的字符串 `str1` 与 `text2` 前 `j - 1` 个字符组成的 str2 的最长公共子序列长度,即 `dp[i][j - 1]`。 +1. 如果 $text1[i - 1] = text2[j - 1],ドル说明两个子字符串的最后一位是相同的,所以最长公共子序列长度加 1ドル$。即:$dp[i][j] = dp[i - 1][j - 1] + 1$。 +2. 如果 $text1[i - 1] \ne text2[j - 1],ドル说明两个子字符串的最后一位是不同的,则 $dp[i][j]$ 需要考虑以下两种情况,取两种情况中最大的那种:$dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])$。 + 1. 「以 $text1$ 中前 $i - 1$ 个元素组成的子字符串 $str1$ 」与「以 $text2$ 中前 $j$ 个元素组成的子字符串 $str2$」的最长公共子序列长度,即 $dp[i - 1][j]$。 + 2. 「以 $text1$ 中前 $i$ 个元素组成的子字符串 $str1$ 」与「以 $text2$ 中前 $j - 1$ 个元素组成的子字符串 $str2$」的最长公共子序列长度,即 $dp[i][j - 1]$。 ###### 4. 初始条件 -初始状态下,默认前 `i` 个字符组成的字符串 `str1` 与前 `j` 个字符组成的字符串 `str2` 的最长公共子序列长度为 `0`,即 `dp[i][j] = 0`。 +1. 当 $i = 0$ 时,$str1$ 表示的是空串,空串与 $str2$ 的最长公共子序列长度为 0ドル,ドル即 $dp[0][j] = 0$。 +2. 当 $j = 0$ 时,$str2$ 表示的是空串,$str1$ 与 空串的最长公共子序列长度为 0ドル,ドル即 $dp[i][0] = 0$。 ###### 5. 最终结果 -根据状态定义,最后输出 `dp[sise1][size2]`(即 `text1` 与 `text2` 的最长公共子序列长度)即可,其中 `size1`、`size2` 分别为 `text1`、`text2` 的字符串长度。 +根据状态定义,最后输出 $dp[sise1][size2]$(即 $text1$ 与 $text2$ 的最长公共子序列长度)即可,其中 $size1$、$size2$ 分别为 $text1$、$text2$ 的字符串长度。 ### 思路 1:代码 @@ -83,6 +84,6 @@ class Solution: ### 思路 1:复杂度分析 -- **时间复杂度**:$O(n \times m),ドル其中 $n$、$m$ 分别是字符串 `text1`、`text2` 的长度。两重循环遍历的时间复杂度是 $O(n \times m),ドル所以总的时间复杂度为 $O(n \times m)$。 +- **时间复杂度**:$O(n \times m),ドル其中 $n$、$m$ 分别是字符串 $text1$、$text2$ 的长度。两重循环遍历的时间复杂度是 $O(n \times m),ドル所以总的时间复杂度为 $O(n \times m)$。 - **空间复杂度**:$O(n \times m)$。用到了二维数组保存状态,所以总体空间复杂度为 $O(n \times m)$。

AltStyle によって変換されたページ (->オリジナル) /