在动态规划(Dynamic Programming)中,子数组和子序列是两个常见的概念,它们的定义和性质直接影响算法的设计。以下是它们的联系、区别与关系的详细说明:
目录
1. 定义与核心区别
-
子数组 (Subarray)
-
子数组是原数组中连续的一段元素。
-
例如:数组
[1, 2, 3, 4]
的子数组包括[1, 2]
、[2, 3, 4]
等。 -
核心要求:必须连续。
-
-
子序列 (Subsequence)
-
子序列是原数组中元素的有序子集,元素之间可以不连续,但必须保持原顺序。
-
例如:数组
[1, 2, 3, 4]
的子序列包括[1, 3, 4]
、[2, 4]
等。 -
核心要求:保持顺序,但允许不连续。
-
2. 动态规划中的联系
-
共同点
-
两者都需要通过递推关系解决问题。
-
通常需要定义
dp[i]
,表示以第i
个元素为结尾的某种最优值。 -
依赖子问题的解(前驱状态)来推导当前状态。
-
-
典型问题
-
子数组:最大子数组和(如 Kadane 算法)。
-
子序列:最长递增子序列(LIS)、最长公共子序列(LCS)。
-
3. 动态规划中的区别
子数组的动态规划
-
状态定义
dp[i]
通常表示以nums[i]
结尾的连续子数组的最优解(如最大和)。 -
状态转移
由于子数组必须连续,状态转移仅依赖前一个状态:dp[i] = max(nums[i], dp[i-1] + nums[i]); // 最大子数组和
-
时间复杂度
通常为 O(n),如 Kadane 算法。 -
示例代码(最大子数组和)
int maxSubArray(vector<int>& nums) { int dp = nums[0], res = dp; for (int i = 1; i < nums.size(); i++) { dp = max(nums[i], dp + nums[i]); res = max(res, dp); } return res; }
子序列的动态规划
-
状态定义
dp[i]
通常表示以nums[i]
结尾的子序列的最优解(如最长长度)。 -
状态转移
由于子序列不要求连续,需要遍历前面所有可能的状态:for (int j = 0; j < i; j++) { if (nums[i] > nums[j]) { dp[i] = max(dp[i], dp[j] + 1); // 最长递增子序列 } }
-
时间复杂度
通常为 O(n²),可优化到 O(n log n)(如二分法优化 LIS)。 -
示例代码(最长递增子序列)
int lengthOfLIS(vector<int>& nums) { vector<int> dp(nums.size(), 1); int res = 1; for (int i = 1; i < nums.size(); i++) { for (int j = 0; j < i; j++) { if (nums[i] > nums[j]) { dp[i] = max(dp[i], dp[j] + 1); } } res = max(res, dp[i]); } return res; }
4. 关键对比总结
特性 | 子数组 | 子序列 |
---|---|---|
连续性 | 必须连续 | 允许不连续 |
状态转移 | 依赖前一个状态 | 依赖前面所有可能状态 |
时间复杂度 | 通常 O(n) | 通常 O(n²) 或优化到 O(n log n) |
典型问题 | 最大子数组和、最小子数组和 | 最长递增子序列、最长公共子序列 |
5. 关系总结
用Veen图来表示其关系:
-
子数组是子序列的特例:所有子数组都是子序列,但子序列不一定是子数组。
-
动态规划设计差异:子数组的连续性限制了状态转移的范围,而子序列的灵活性要求更复杂的状态转移逻辑。
理解两者的区别与联系,有助于针对不同问题设计高效的动态规划算法。