在一个乱序的数组中找到最长的递增子序列

本文介绍了一道算法题目,涉及在乱序数组中找到最长递增子序列的问题。文中提供了两种解法,分别是平方时间复杂度和对数时间复杂度的解决方案,并附带了代码实现及简单注释。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这个题是牛客上左程云讲其他俄国沙皇问题是提及到的一个算法原型。代码中给出了两个思路。时间复杂度分别为 N^2 和 NlogN,源代码中没有给出注释,自己照着思路又重新捋了遍程序,稍微加了点注释,方便理解。

原题目的讲解在牛客网公开课http://www.nowcoder.com/live/11/1/1

以下是代码

public class Problem_05_LIS {

	public static int[] lis1(int[] arr) {
		if (arr == null || arr.length == 0) {
			return null;
		}
		int[] dp = getdp1(arr);
		return generateLIS(arr, dp);
	}
//dp中放的是,对应数组中每个元素为输出序列的最后一个值时的最大长度
//方法一:O(n^2)复杂度,具体思路是:两层循环,外层为i时,内从从0到i,依次判断,当加入 arr[i]的值后,更新dp中最长子序列的长度,最长
//子序列更新,依赖于i前面的dp中的数组值,若是arr[i]比i之前的某个位置上的的数要大,则通过max(dp[i], dp[j] + 1);来
//更新第i位上的值。
	public static int[] getdp1(int[] arr) {
		int[] dp = new int[arr.length];
		for (int i = 0; i < arr.length; i++) {
			dp[i] = 1;
			for (int j = 0; j < i; j++) {
				if (arr[i] > arr[j]) {
					dp[i] = Math.max(dp[i], dp[j] + 1);
				}
			}
		}
		return dp;
	}

	//根据 dp 数组的值生成要打印的数组,作为最后结果的输出,
	public static int[] generateLIS(int[] arr, int[] dp) {
		int len = 0;
		int index = 0;
		for
<think>我们被要求使用中文回答,并且需要按照给定的格式要求(如行内数学公式用$...$,独立公式用$$...$$)。同时,需要生成相关问题,并在回答中引用段落末尾添加引用标识。 根据用户的问题,他想要了解C++中数组最长单调递增子序列(Longest Increasing Subsequence, LIS)的算法或实现方法。我们参考了提供的引用内容,发现引用[1][2][3][4][5]都涉及了这个问题,但实现方法有所不同。 最长单调递增子序列问题:给定一个序列找出一个子序列,使得这个子序列的元素是严格递增(或非严格递增,但通常指严格递增)的,并且这个子序列的长度尽可能长。 常见的解法有: 1. 动态规划(O(n^2)):这是最直接的方法,我们定义dp[i]为以第i个元素结尾的最长递增子序列的长度。对于每个i,我们遍历j从0到i-1,如果a[j] < a[i],则dp[i] = max(dp[i], dp[j]+1)。最后,dp数组中的最大值就是最长递增子序列的长度。 2. 贪心+二分查找(O(n log n)):这种方法效率更高。我们维护一个数组d[],表示长度为i的最长递增子序列的末尾元素的最小值。然后遍历数组,对于每个元素,如果它大于d数组的最后一个元素,则将它加入d数组末尾;否则,在d数组找到一个大于等于它的元素,用这个元素替换掉。这样,d数组的长度就是最长递增子序列的长度。 从引用内容看,引用[1][4]提供了动态规划的代码,引用[5]也提供了一个动态规划(但效率可能不如前两个,因为其内层循环是向前找,但本质也是O(n^2)),而引用[2][3]则介绍了思路。 由于用户要求的是“数组最长单调递增子序列”,我们选择两种方法都介绍,但重点放在动态规划(因为引用中提供了多个动态规划的例子)和贪心+二分法(因为效率更高)。 注意:引用[1]的代码不仅计算了长度,还还原了子序列本身。引用[5]的代码也还原了子序列。 我们分步骤讲解: ### 1. 动态规划(O(n^2)) 步骤: - 定义dp数组,dp[i]表示以a[i]结尾的最长递增子序列的长度。 - 初始化:每个dp[i]至少为1(因为至少包含自己)。 - 对于每个i(从1到n-1),遍历j(从0到i-1): - 如果a[j] < a[i],则更新dp[i] = max(dp[i], dp[j]+1) - 遍历dp数组找出最大值,即为最长递增子序列的长度。 如果要输出这个子序列,我们需要在动态规划的过程中记录路径。具体方法:用一个pre数组记录每个元素的前驱元素的下标(即dp[i]是由哪个j更新来的)。然后在dp数组找到最大值的位置,然后逆序回溯即可。 但是注意引用[1]的代码中,还原子序列的方法:它先找到最长递增子序列的最后一个元素的位置k,然后向前遍历,找到满足条件(a[i] < a[k]且dp[i]==dp[k]-1)的元素,然后更新k为i,继续向前找。 ### 2. 贪心+二分法(O(n log n)) 步骤: - 定义一个数组d,用于存储最长递增子序列的候选序列(注意d中存储的并不一定是真正的子序列,但长度等于最长递增子序列的长度)。 - 初始化:令d[0]=a[0],然后遍历数组a(从1到n-1): - 如果a[i]大于d的最后一个元素,则d.push_back(a[i]) - 否则,在d数组找到一个大于等于a[i]的元素,用a[i]替换它(这里使用二分查找)。 - 最终d的长度就是最长递增子序列的长度。 注意:这种方法只能得到长度,如果要输出子序列,我们需要额外的数组记录每个元素在d数组中的位置,以及每个位置的前驱(但这样比较麻烦)。不过,我们可以通过记录每个元素在d数组中的位置(即该元素作为长度为k的递增子序列的末尾元素),然后从后往前遍历,找到长度为len, len-1, ...的元素,从而得到序列。 但是,由于引用中并没有给出贪心+二分法的具体代码,我们主要参考动态规划方法,因为用户提供的引用都是动态规划。 根据引用[1]和[4]的代码,我们给出一个动态规划的实现,并同时输出长度和子序列。 ### 代码实现(动态规划法) 参考引用[1]的代码,我们进行改写和注释: ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int n; cin >> n; vector<int> a(n); // 存储输入数组 vector<int> dp(n, 1); // dp[i]表示以a[i]结尾的最长递增子序列的长度 vector<int> pre(n, -1); // 记录前驱节点,用于还原子序列 for (int i = 0; i < n; ++i) { cin >> a[i]; } // 动态规划过程 for (int i = 0; i < n; ++i) { for (int j = 0; j < i; ++j) { if (a[j] < a[i] && dp[j] + 1 > dp[i]) { dp[i] = dp[j] + 1; pre[i] = j; // 记录前驱 } } } // 找出最长递增子序列的长度和结束位置 int maxLength = 0, end = -1; for (int i = 0; i < n; ++i) { if (dp[i] > maxLength) { maxLength = dp[i]; end = i; } } // 从结束位置回溯前驱节点,得到逆序的子序列 vector<int> lis; while (end != -1) { lis.push_back(a[end]); end = pre[end]; } reverse(lis.begin(), lis.end()); // 将逆序反转 // 输出结果 cout << "最长递增子序列的长度为: " << maxLength << endl; cout << "序列为: "; for (int num : lis) { cout << num << " "; } cout << endl; return 0; } ``` 注意:这个代码使用了`pre`数组记录前驱,然后回溯得到子序列。这样得到的子序列是正序的。 引用[1]中的代码没有使用`pre`数组,而是通过比较`b[i]`(即dp[i])和当前长度来还原,但原理类似。 ### 另一种还原方法(不使用pre数组) 我们也可以像引用[1]那样,在得到dp数组后,从后往前找: 1. 先找到dp数组中最大值的位置k。 2. 令当前值`cur = a[k]`,当前长度`len = maxLength`,然后从k开始向前遍历,找到一个满足`a[i] < cur`且`dp[i] == len-1`的元素,将其加入结果,然后更新`cur = a[i]`,`len = len-1`,直到len为0。 这种方法不需要pre数组,但需要从后往前遍历,因此得到的序列是正序的(因为我们是逆序加入,然后反转)?实际上,我们可以直接正序输出,但需要先逆序存储。 代码示例(不使用pre数组还原): ```cpp // 找出最长递增子序列的长度和结束位置 int maxLength = 0, end = -1; for (int i = 0; i < n; ++i) { if (dp[i] > maxLength) { maxLength = dp[i]; end = i; } } vector<int> lis; lis.push_back(a[end]); // 先加入最后一个元素 int curLen = maxLength - 1; // 接下来要找长度为maxLength-1的 int curEnd = end; // 从后往前找 for (int i = end-1; i >= 0; --i) { if (a[i] < a[curEnd] && dp[i] == curLen) { lis.push_back(a[i]); curEnd = i; curLen--; } } reverse(lis.begin(), lis.end()); // 因为是从后往前找,所以得到的序列是逆序的,需要反转 ``` ### 效率 动态规划方法的时间复杂度为$O(n^2)$,空间复杂度为$O(n)$。 ### 贪心+二分法(O(n log n))的代码(可选) 由于用户没有要求,但为了完整性,我们给出贪心+二分法的代码(只求长度): ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int n; cin >> n; vector<int> a(n); for (int i = 0; i < n; ++i) { cin >> a[i]; } vector<int> d; // d用于存储递增子序列的候选 d.push_back(a[0]); for (int i = 1; i < n; ++i) { // 如果a[i]大于d的最后一个元素,直接加入 if (a[i] > d.back()) { d.push_back(a[i]); } else { // 二分查找第一个大于等于a[i]的位置 auto it = lower_bound(d.begin(), d.end(), a[i]); *it = a[i]; } } cout << "最长递增子序列的长度为: " << d.size() << endl; return 0; } ``` 注意:这个方法不能直接得到子序列(因为d数组中的元素并不一定构成原序列一个子序列),但长度是正确的。如果要得到子序列,需要额外的数组记录每个位置的前驱,然后从后往前回溯,但这样实现起来相对复杂。 ### 总结 我们提供了两种方法:动态规划(O(n^2))和贪心+二分法(O(n log n)),并给出了动态规划方法中还原子序列的两种方式。用户可以根据需要选择。 ### 相关问题 根据要求,我们生成相关问题: 1. 动态规划方法的时间复杂度是$O(n^2)$,有没有更高效的方法? 2. 如何用贪心+二分法得到最长递增子序列的具体序列? 3. 最长递增子序列问题有哪些变种? 4. 动态规划方法中,如何优化空间复杂度? 5. 如果要求非严格递增(即允许相等)的最长子序列,算法应该如何修改? 注意:在回答中引用的地方要添加引用标识。我们在回答中引用了引用[1]和[4]中的动态规划方法,所以我们在相关地方添加了引用标识[^1][^4]。</think>### 最长单调递增子序列算法详解(C++实现) 最长单调递增子序列(Longest Increasing Subsequence, LIS)是指在一个序列找到一个最长子序列,使得这个子序列中的元素严格递增。以下是两种常用的C++实现方法: #### 方法1:动态规划($O(n^2)$) ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int n; cin >> n; vector<int> arr(n); // 存储原始序列 vector<int> dp(n, 1); // dp[i]表示以arr[i]结尾的LIS长度 vector<int> prev(n, -1); // 记录前驱节点 // 输入序列 for (int i = 0; i < n; i++) { cin >> arr[i]; } // 动态规划计算LIS长度 for (int i = 0; i < n; i++) { for (int j = 0; j < i; j++) { if (arr[j] < arr[i] && dp[j] + 1 > dp[i]) { dp[i] = dp[j] + 1; prev[i] = j; // 记录前驱 } } } // 找出最长LIS的结束位置 int max_len = 0, end_pos = -1; for (int i = 0; i < n; i++) { if (dp[i] > max_len) { max_len = dp[i]; end_pos = i; } } // 回溯构建LIS序列 vector<int> lis; while (end_pos != -1) { lis.push_back(arr[end_pos]); end_pos = prev[end_pos]; } reverse(lis.begin(), lis.end()); // 输出结果 cout << "长度: " << max_len << "\n序列: "; for (int num : lis) { cout << num << " "; } return 0; } ``` **算法分析**: 1. **初始化**:`dp[i]`初始值为1(每个元素自身构成长度为1的子序列) 2. **状态转移**: $dp[i] = \max_{j<i \text{ 且 } arr[j]<arr[i]}(dp[j] + 1)$ 若$arr[j] < arr[i]$,则更新$dp[i]$并记录前驱节点[^4] 3. **回溯输出**:从最大$dp[i]$位置回溯前驱节点,得到LIS序列[^1] #### 方法2:贪心+二分查找($O(n \log n)$) ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int n; cin >> n; vector<int> arr(n); vector<int> tail; // 存储长度为i+1的LIS的最小末尾值 for (int i = 0; i < n; i++) cin >> arr[i]; for (int x : arr) { // 找到一个 >=x 的位置 auto it = lower_bound(tail.begin(), tail.end(), x); if (it == tail.end()) { tail.push_back(x); // x比所有末尾值都大 } else { *it = x; // 替换为更小的末尾值 } } cout << "长度: " << tail.size(); return 0; } ``` **算法分析**: 1. **维护有序数组**:`tail`数组始终保持有序性 2. **二分查找**:使用`lower_bound`找到一个$\geq x$的位置 3. **贪心策略**: - 若$x$大于所有末尾值,则扩展LIS长度 - 否则替换第一个$\geq x$的元素,使后续更容易扩展[^2] 4. **复杂度**:遍历$n$个元素,每次二分查找$O(\log n)$,总复杂度$O(n \log n)$ ### 关键点对比 | 特性 | 动态规划 | 贪心+二分法 | |--------------------|------------------|-------------------| | 时间复杂度 | $O(n^2)$ | $O(n \log n)$ | | 空间复杂度 | $O(n)$ | $O(n)$ | | 能否输出具体序列 | ✓ | ✗(需额外处理) | | 适用场景 | 小规模数据 | 大规模数据 | ### 应用场景 1. 基因序列分析(生物信息学) 2. 数据压缩与路径优化 3. 金融数据分析(股票价格趋势预测) 4. 自然语言处理(文本相似度计算)[^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值