经典算法:最长上升子序列(LIS)深度解析 C++ 实现

前言

最长上升子序列(Longest Increasing Subsequence,LIS) 是算法面试和动态规划学习中的经典必考题。它不仅能考察我们对动态规划思想的理解,还能延伸出更高效的贪心 + 二分解法。

本文将基于你提供的 C++ 代码,从错误思路分析到正确解法,手把手带你彻底搞懂最长上升子序列问题,包含完整代码、逐行解析、复杂度分析,新手也能轻松掌握。

一、问题描述

给定一个无序的整数数组 ,找到其中最长上升子序列的长度。

  • 上升:要求子序列中元素严格递增
  • 子序列:不要求连续,但元素相对顺序不能改变
  • 只需输出长度,无需输出具体序列

示例 :输入:[10, 9, 2, 5, 3, 7, 101, 18]输出:4解释:最长上升子序列为 [2, 3, 7, 101],长度为 4。

二、核心思路

1. 动态规划定义

我们定义:dp[i] 表示以数组中第 i 个元素结尾的最长上升子序列长度。

2. 状态转移方程

  1. 每个元素自身就是一个长度为 1 的子序列,所以 dp[i] 初始值 = 1
  2. 对于每个位置 i,遍历它前面所有位置 jj < i
    • 如果 ar[i] > ar[j],说明可以把 ar[i] 接在以 ar[j] 结尾的上升子序列后面
    • 此时 dp[i] = max(dp[i], dp[j] + 1)

最终答案 = dp 数组中的最大值


三、代码实现与深度解析

版本 1:错误思路分析(MaxLengthSub1)

很多初学者最容易写出这个版本,只比较相邻元素,这是典型错误!

cpp 复制代码
// 错误版本:只比较相邻元素
int MaxLengthSub1(const std::vector<int>& ar)
{
	int n = ar.size();
	if (0 == n) return 0;
	if (1 == n) return 1;
	std::vector<int> dp(n, 0);
	dp[0] = 1;
	for (int i = 1; i < n; ++i)
	{
		// 错误:只看前一个,不看前面所有
		dp[i] = std::max(1, dp[i - 1] + (ar[i] > ar[i - 1] ? 1 : 0));
	}
	PrintVec(dp);
	return dp[n - 1];
}

❌ 错误原因

  • 最长上升子序列不一定是连续的
  • 只比较 ii-1,会漏掉前面所有能和当前元素形成上升序列的位置
  • 例如:[2,5,3],正确 LIS 是 [2,3],但此代码会错误认为是 [2,5]

版本 2:标准动态规划解法(MaxLengthSub2)

这是正确、通用、面试必写的 O (n²) 解法,完全符合题目要求:

cpp 复制代码
// 正确版本:动态规划 O(n²)
int MaxLengthSub2(const std::vector<int>& ar)
{
	int n = ar.size();
	if (0 == n) return 0;   // 空数组返回0
	if (1 == n) return 1;   // 一个元素长度为1

	std::vector<int> dp(n, 0); // dp[i] = 以i结尾的最长上升子序列长度
	int maxsub = 1;            // 记录最终答案

	for (int i = 0; i < n; ++i)
	{
		dp[i] = 1;  // 每个元素自身长度为1

		// 遍历 i 前面所有元素 j
		for (int j = 0; j < i; ++j)
		{
			// 当前元素 > 前面元素,可以接在后面
			if (ar[i] > ar[j])
			{
				dp[i] = std::max(dp[j] + 1, dp[i]);
			}
		}

		// 更新全局最大值
		if (dp[i] > maxsub)
		{
			maxsub = dp[i];
		}
	}
	return maxsub;
}

✅ 逐行解析

  1. 边界处理
    • 数组为空 → 返回 0
    • 只有一个元素 → 最长子序列就是自己,返回 1
  2. dp 数组初始化
    • dp[i] 初始化为 1,每个元素单独构成长度为 1 的序列
  3. 双层循环
    • 外层 i:遍历每个元素作为结尾
    • 内层 j:遍历 i 前面所有元素
    • 满足 ar[i] > ar[j] 时更新 dp[i]
  4. 记录最大值
    • 最终答案不是 dp[n-1],而是 dp 数组中的最大值

四、完整可运行代码

cpp 复制代码
#include<stdio.h>
#include<iostream>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<limits.h>
#include<float.h>
#include<stack>
#include<queue>
#include<vector>
#include<algorithm>
using namespace std;

// 打印数组,方便调试观察dp数组变化
void PrintVec(const std::vector<int>& ar)
{
	int n = ar.size();
	for (int i = 0; i < n; ++i)
	{
		printf("%5d", ar[i]);
	}
	printf("\n------------------------------------\n");
}

// 正确解法:动态规划 O(n²)
int MaxLengthSub2(const std::vector<int>& ar)
{
	int n = ar.size();
	if (0 == n) return 0;
	if (1 == n) return 1;

	std::vector<int> dp(n, 0);
	int maxsub = 1;

	for (int i = 0; i < n; ++i)
	{
		dp[i] = 1;
		for (int j = 0; j < i; ++j)
		{
			if (ar[i] > ar[j])
			{
				dp[i] = std::max(dp[j] + 1, dp[i]);
			}
		}
		PrintVec(dp);
		if (dp[i] > maxsub)
		{
			maxsub = dp[i];
		}
	}
	return maxsub;
}

int main()
{
	std::vector<int> ar = { 10,9,2,5,3,7,101,18 };
	int maxlen = MaxLengthSub2(ar);
	cout << "最长上升子序列长度:" << maxlen << endl;
	return 0;
}

五、运行结果

bash 复制代码
    1    0    0    0    0    0    0    0
------------------------------------
    1    1    0    0    0    0    0    0
------------------------------------
    1    1    1    0    0    0    0    0
------------------------------------
    1    1    1    2    0    0    0    0
------------------------------------
    1    1    1    2    2    0    0    0
------------------------------------
    1    1    1    2    2    3    0    0
------------------------------------
    1    1    1    2    2    3    4    0
------------------------------------
    1    1    1    2    2    3    4    4
------------------------------------
最长上升子序列长度:4

✅ 输出结果完全正确!


六、进阶扩展:O (n log n) 最优解法

面试中如果要求更优时间复杂度 ,可以使用贪心 + 二分查找,效率更高:

复制代码
// 最优解法:贪心 + 二分 O(nlogn)
int lengthOfLIS(vector<int>& nums) {
    vector<int> res;
    for (int x : nums) {
        auto it = lower_bound(res.begin(), res.end(), x);
        if (it == res.end()) res.push_back(x);
        else *it = x;
    }
    return res.size();
}

核心思想

  • 维护一个最小可能的上升序列
  • 用二分查找加速替换 / 插入位置
  • 适合数据量很大的场景(百万级别)

七、两种解法对比

解法 时间复杂度 空间复杂度 优点 适用场景
动态规划 O(n²) O(n) 思路直观、易理解、易写 笔试、入门、小数据
贪心 + 二分 O(n log n) O(n) 效率极高 面试、大数据量

建议

  • 基础练习、考试:写 O (n²) 动态规划
  • 面试优化:写 O (n log n) 贪心 + 二分

八、总结

  1. 最长上升子序列(LIS) 是动态规划经典题,核心是 dp[i] 定义 + 状态转移

  2. 初学者常见错误:只比较相邻元素,正确做法是遍历前面所有元素

  3. 标准方程

    cpp 复制代码
    dp[i] = 1;
    if(ar[i] > ar[j]) dp[i] = max(dp[i], dp[j]+1);
  4. 最终答案 = dp 数组最大值,不是最后一个元素

  5. 进阶可学习贪心 + 二分优化到 O (n log n)

本文代码完整可直接运行,从错误到正确、从基础到进阶,非常适合 C++ 新手学习动态规划,建议收藏反复练习~

相关推荐
.Ashy.2 小时前
2026.4.11 蓝桥杯软件类C/C++ G组山东省赛 小记
c语言·c++·蓝桥杯
Y4090012 小时前
【多线程】线程安全(1)
java·开发语言·jvm
不爱吃炸鸡柳2 小时前
Python入门第一课:零基础认识Python + 环境搭建 + 基础语法精讲
开发语言·python
minji...3 小时前
Linux 线程同步与互斥(三) 生产者消费者模型,基于阻塞队列的生产者消费者模型的代码实现
linux·运维·服务器·开发语言·网络·c++·算法
Dxy12393102163 小时前
Python基于BERT的上下文纠错详解
开发语言·python·bert
语戚4 小时前
力扣 968. 监控二叉树 —— 贪心 & 树形 DP 双解法递归 + 非递归全解(Java 实现)
java·算法·leetcode·贪心算法·动态规划·力扣·
skywalker_114 小时前
力扣hot100-7(接雨水),8(无重复字符的最长子串)
算法·leetcode·职场和发展