探索子序列的奥秘:从基础概念到实际应用

在计算机科学和数学领域,子序列是一个既基础又充满魅力的概念。它如同隐藏在数据中的密码,通过特定的规则组合,能为我们揭示出许多重要的信息和规律。无论是在算法设计、数据分析,还是在生物信息学等领域,子序列都发挥着不可或缺的作用。​

一、子序列的定义与基本概念​

从定义上讲,子序列是从给定序列中通过删除某些元素(也可以不删除任何元素),但不改变剩余元素相对顺序而得到的新序列。简单来说,假如有一个序列 [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;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值