在计算机科学和数学领域,子序列是一个既基础又充满魅力的概念。它如同隐藏在数据中的密码,通过特定的规则组合,能为我们揭示出许多重要的信息和规律。无论是在算法设计、数据分析,还是在生物信息学等领域,子序列都发挥着不可或缺的作用。
一、子序列的定义与基本概念
从定义上讲,子序列是从给定序列中通过删除某些元素(也可以不删除任何元素),但不改变剩余元素相对顺序而得到的新序列。简单来说,假如有一个序列 [1, 3, 4, 5, 2] , [1, 4, 2] 就是它的一个子序列,因为它是通过选取原序列的第 1、3、5 个元素得到的;[3, 5] 同样是子序列,选取的是原序列的第 2、4 个元素 。特别地,空序列也是任何序列的子序列,原序列本身也是自己的子序列。
子序列与子数组(连续子序列)有着明显区别。子数组要求元素在原序列中是连续的,而子序列不要求连续性,这使得子序列的组合方式更加多样,也赋予了它更强大的表达能力和更广泛的应用场景。
二、子序列的经典问题与算法
(一)最长递增子序列(LIS)
最长递增子序列问题是子序列研究中的经典问题。给定一个序列,目标是找出其中最长的严格递增子序列的长度。例如,对于序列 [10, 22, 9, 33, 21, 50, 41, 60] ,其最长递增子序列为 [10, 22, 33, 50, 60] ,长度为 5 。
下面是使用动态规划算法解决该问题的 Python 代码:
def lengthOfLIS(nums):
dp = [1] * len(nums)
for i in range(len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp) if dp else 0
nums = [10, 22, 9, 33, 21, 50, 41, 60]
print(lengthOfLIS(nums))
在这段代码中,我们定义了一个列表 dp , dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度,初始时,每个元素自身可构成长度为 1 的递增子序列,所以都初始化为 1。然后通过两层循环遍历序列,内层循环中,对于每个 j < i ,如果 nums[j] < nums[i] ,则更新 dp[i] 为 dp[i] 和 dp[j] + 1 中的较大值 。最终,返回 dp 列表中的最大值,就是整个序列的最长递增子序列长度 。该算法的时间复杂度为 O(n2) 通过利用二分查找优化,可以将时间复杂度降低到 O(nlogn) 。
(二)最长公共子序列(LCS)
最长公共子序列问题旨在找出两个序列中最长的相同子序列。例如,对于序列 X = [1, 3, 4, 5, 6, 7, 7, 8] 和 Y = [3, 5, 7, 4, 8, 6, 7, 8, 2] ,它们的最长公共子序列为 [3, 5, 7, 8] ,长度为 4 。
使用动态规划解决该问题的 Python 代码如下:
def longestCommonSubsequence(text1, text2):
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 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[m][n]
text1 = [1, 3, 4, 5, 6, 7, 7, 8]
text2 = [3, 5, 7, 4, 8, 6, 7, 8, 2]
print(longestCommonSubsequence(text1, text2))
这里我们创建了一个二维列表 dp , dp[i][j] 表示 text1 的前 i 个元素和 text2 的前 j 个元素的最长公共子序列长度 。通过两层循环遍历两个序列,如果 text1[i - 1] 等于 text2[j - 1] ,则 dp[i][j] 为 dp[i - 1][j - 1] + 1 ;否则, dp[i][j] 取 dp[i - 1][j] 和 dp[i][j - 1] 中的较大值 。最终, dp[m][n] 就是两个序列的最长公共子序列长度 。时间复杂度为
O(mn),空间复杂度也为 O(mn),通过滚动数组优化,空间复杂度可降低到 O(min(m,n))。
三、C++代码
(一)计算最长公共子序列
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 计算最长公共子序列长度
int longestCommonSubsequence(const string& text1, const string& text2) {
int m = text1.size();
int n = text2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
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[m][n];
}
// 打印最长公共子序列(一种可能的解)
string printLCS(const string& text1, const string& text2) {
int m = text1.size();
int n = text2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
// 构建DP表
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
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]);
}
}
}
// 回溯构建LCS
string lcs;
int i = m, j = n;
while (i > 0 && j > 0) {
if (text1[i - 1] == text2[j - 1]) {
lcs += text1[i - 1];
--i;
--j;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
--i;
} else {
--j;
}
}
reverse(lcs.begin(), lcs.end());
return lcs;
}
int main() {
string text1 = "abcde";
string text2 = "ace";
cout << "最长公共子序列长度: " << longestCommonSubsequence(text1, text2) << endl;
cout << "一个可能的最长公共子序列: " << printLCS(text1, text2) << endl;
return 0;
}
(二)计算最长递增序列
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 方法1:动态规划 O(n²)
int lengthOfLIS_DP(const vector<int>& nums) {
if (nums.empty()) return 0;
int n = nums.size();
vector<int> dp(n, 1); // dp[i]表示以nums[i]结尾的最长递增子序列长度
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
return *max_element(dp.begin(), dp.end());
}
// 方法2:贪心+二分查找 O(nlogn)
int lengthOfLIS_Greedy(const vector<int>& nums) {
vector<int> tails; // tails[i]表示长度为i+1的递增子序列的最小末尾元素
for (int num : nums) {
auto it = lower_bound(tails.begin(), tails.end(), num);
if (it == tails.end()) {
tails.push_back(num); // 无法替换,扩展tails
} else {
*it = num; // 替换为更小的末尾元素
}
}
return tails.size();
}
// 打印最长递增子序列(一种可能的解)
vector<int> printLIS(const vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, 1); // 长度
vector<int> prev(n, -1); // 前驱索引
int max_len = 0, end_idx = -1;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i] && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
prev[i] = j;
}
}
if (dp[i] > max_len) {
max_len = dp[i];
end_idx = i;
}
}
vector<int> lis;
while (end_idx != -1) {
lis.push_back(nums[end_idx]);
end_idx = prev[end_idx];
}
reverse(lis.begin(), lis.end());
return lis;
}
int main() {
vector<int> nums = {10, 9, 2, 5, 3, 7, 101, 18};
cout << "动态规划解法最长LIS长度: " << lengthOfLIS_DP(nums) << endl;
cout << "贪心+二分解法最长LIS长度: " << lengthOfLIS_Greedy(nums) << endl;
vector<int> lis = printLIS(nums);
cout << "一个可能的最长递增子序列: ";
for (int num : lis) cout << num << " ";
cout << endl;
return 0;
}
(三)最大子数组(连续)最大子序列 和不超过3的非空子序列数目
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
// 最大子数组和(连续)- Kadane算法
int maxSubArraySum(const vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0;
int current_sum = nums[0];
int max_sum = nums[0];
for (int i = 1; i < n; ++i) {
current_sum = max(nums[i], current_sum + nums[i]);
max_sum = max(max_sum, current_sum);
}
return max_sum;
}
// 最大子序列和(不要求连续)
int maxSubsequenceSum(const vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0;
int max_sum = INT_MIN;
int sum = 0;
bool has_positive = false;
for (int num : nums) {
if (num > 0) {
has_positive = true;
sum += num;
}
max_sum = max(max_sum, num);
}
return has_positive ? sum : max_sum;
}
// 统计和不超过k的非空子序列数目(假设元素均为正数)
int countSubsequencesWithSum(const vector<int>& nums, int k) {
int n = nums.size();
int count = 0;
// 枚举所有非空子集(使用位运算)
for (int mask = 1; mask < (1 << n); ++mask) {
int sum = 0;
for (int i = 0; i < n; ++i) {
if (mask & (1 << i)) {
sum += nums[i];
}
}
if (sum <= k) {
++count;
}
}
return count;
}
int main() {
vector<int> nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
cout << "最大子数组和: " << maxSubArraySum(nums) << endl;
vector<int> nums2 = {-2, -3, -1};
cout << "最大子序列和: " << maxSubsequenceSum(nums2) << endl;
vector<int> nums3 = {1, 2, 3};
cout << "和不超过3的非空子序列数目: " << countSubsequencesWithSum(nums3, 3) << endl;
return 0;
}
四、力扣例题
(一)1498. 满足条件的子序列数目 - 力扣(LeetCode)
思路:①将数组排序 ②找到一个连续的子序列最大值 + 最小值 <= target ③计算这个序列里的全部子序列
class Solution {
public:
int numSubseq(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
int n = nums.size();
const int MOD = 1e9 + 7;
// 预处理2的幂次
vector<int> pow2(n, 1);
for (int i = 1; i < n; i++) {
pow2[i] = (pow2[i-1] * 2) % MOD;
}
int ans = 0;
int left = 0, right = n - 1;
while (left <= right) {
if (nums[left] + nums[right] > target) {
right--;
} else {
// 以left为最小值,right为最大值的子数组的所有子序列都满足条件
ans = (ans + pow2[right - left]) % MOD;
left++;
}
}
return ans;
}
};