动态规划解最长不下降子序列:深入理解状态转移与内层循环

一、问题引入

最长不下降子序列(Longest Non-Decreasing Subsequence,简称LNDS)是一个经典的算法问题。给定一个数字序列,我们需要找到一个子序列(可以不连续),使得这个子序列中的元素是非严格递增的(即每个元素大于等于前一个元素),并且这个子序列的长度尽可能长。

示例

cpp 复制代码
序列:13, 7, 9, 16, 38, 24, 37, 18, 44, 18, 21, 22, 63, 15
最长不下降子序列:[7, 9, 16, 24, 37, 44, 63] 或 [7, 9, 16, 18, 21, 22, 63] 等
长度:7

二、为什么用动态规划?

动态规划适用于具有"最优子结构"和"重叠子问题"特征的问题。最长不下降子序列问题正好具备这两个特征:

2.1 最优子结构

以某个元素结尾的最长不下降子序列,可以由更短序列的最长不下降子序列推导出来。

例如,在序列 [3, 1, 2, 4]中:

  • 以4结尾的最长不下降子序列是 [1, 2, 4]

  • 这个序列去掉4后得到的 [1, 2]正是以2结尾的最长不下降子序列

2.2 重叠子问题

计算不同位置结尾的最长不下降子序列时,会反复用到相同的子问题解。

例如,计算以4结尾和以5结尾的最长不下降子序列时,都需要知道以3结尾的最长不下降子序列长度。

三、动态规划解法详解

3.1 状态定义

定义 dp[i]表示以序列中第 i个元素结尾的最长不下降子序列的长度。

为什么这样定义?

因为"以某个元素结尾"是一个很好的状态表示,它满足:

  • 可计算:可以从之前的状态推导出来

  • 完备:最终答案就是所有 dp[i]中的最大值

  • 无后效:dp[i]只依赖于 i之前的状态

3.2 状态转移方程

状态转移方程是动态规划的核心。对于每个位置 i

cpp 复制代码
dp[i] = max{1, dp[j] + 1 | 1 ≤ j < i 且 a[i] ≥ a[j]}

这个方程怎么理解?

  • 1:至少可以只包含自己

  • dp[j] + 1:如果 a[i]可以接在 a[j]后面(即 a[i] ≥ a[j]),那么长度可以是 dp[j] + 1

  • max:在所有可能的情况中取最大值

四、关键问题:为什么需要比较所有前驱?

这是理解这个问题的关键点。内层循环比较所有 j < i是必须的,原因有三:

4.1 子序列可以不连续

这是最核心的原因。由于子序列可以不连续,当前元素可以跳过中间元素,接在任何满足条件的前驱后面。

示例[1, 2, 3, 4, 5,1, 2, 3, 4, 6]

当处理6时:

  • 可以跳过1234,接在12345后面:[1, 2,3, 4, 5,6]

  • 也可以直接,接在1234后面:[1, 2, 3,4, 6]

如果不比较所有前驱,只和前一个元素比较,就会错过第一种可能。

4.2 序列可能有多个上升段

序列中可能有多个上升段,当前元素应接在能给出最长序列的那个上升段的末尾。

示例[1, 2, 3, 4, 1, 2, 3,5]

  • 第一段:1, 2, 3

  • 第二段:1, 2, 3, 4

当处理5时:

  • 如果只和前一个元素(3)比较,得到 [1, 2, 3, 5],长度4

  • 但实际上,4可以接在第一段的4后面,形成 [1, 2, 3, 4, 5],长度5

4.3 前驱数值的限制

即使某个前驱的 dp值很大,但当前元素可能无法接在它后面。

示例[1, 100, 2, 3, 4]

  • dp[2](对应100)的值是2

  • 但4不能接在100后面(4 < 100)

  • 4可以接在3后面,形成 [1, 2, 3, 4]

五、代码实现与解释

5.1 基础版本

cpp 复制代码
#include<iostream>
#include<algorithm>

using namespace std;

int main()
{
	int dp[10010];
	int a[10010];
	
	int n;
	cin >> n;
	int i = 1;
	for (i = 1; i <= n; i++)
	{
		cin >> a[i];
	}

	int res=0;
	for (i = 1; i <= n; i++)
	{
		dp[i] = 1;
		int j = 1;
		for (j = 1; j < i; j++)//注意这里不能写=i,不然会和自己本身的d[i]+1比较多加一个
		{
			if (a[i] >= a[j])
			{
				dp[i] = max(dp[i],dp[j]+1);//这里是为了确定下来每个dp[i]是什么
			}
			
		}
		res = max(dp[i], res);//这里是比较不同的i,哪个个dp[i],比较大
	}

	

	cout << res;
	return 0;
}

5.2 算法执行流程

以序列 [3, 1, 2, 4]为例,算法执行过程如下:

i a[i] 计算过程 dp[i] 说明
1 3 初始化为1 1 只有自己
2 1 1<3,无法接在3后面 1 只能自己开头
3 2 2<3(否), 2≥1(是) → max(1, 1+1)=2 2 接在1后面
4 4 4≥3→2, 4≥1→2, 4≥2→3 → max(1,2,2,3)=3 3 接在2后面得到最长

最终结果为 max(1,1,2,3) = 3

六、算法优化

6.1 时间复杂度分析

  • 外层循环:O(n)

  • 内层循环:平均O(n/2)

  • 总时间复杂度:O(n²)

当n较大时(如n>10000),可能会超时。

6.2 优化版本(O(n log n))

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    int n;
    cin >> n;
    
    vector<int> d;  // d[i]表示长度为i+1的不下降子序列的最小末尾值
    
    for(int i = 0; i < n; i++) {
        int x;
        cin >> x;
        
        // 找到第一个大于x的位置
        auto it = upper_bound(d.begin(), d.end(), x);
        
        if(it == d.end()) {
            d.push_back(x);  // x可以扩展最长子序列
        } else {
            *it = x;  // 用x替换第一个大于它的元素
        }
    }
    
    cout << d.size() << endl;
    return 0;
}

优化思路

  1. 维护数组 d,其中 d[k]表示长度为 k+1的不下降子序列的最小末尾值

  2. 对于每个元素 x

    • 如果 x大于等于 d的最后一个元素,将 x添加到 d末尾

    • 否则,在 d中找到第一个大于 x的元素,用 x替换它

  3. 最终 d的长度就是最长不下降子序列的长度

七、总结与思考

最长不下降子序列问题很好地展示了动态规划的思想:

  1. 状态定义要准确dp[i]表示以第 i个元素结尾的最长不下降子序列长度

  2. 状态转移要全面:必须考虑所有可能的前驱,因为子序列可以不连续

  3. 内层循环是必要的:它确保了不会错过任何可能的更优解

  4. 可以进一步优化:通过维护单调数组和二分查找,可以将复杂度从 O(n²) 降到 O(n log n)

关键理解:内层循环比较所有前驱,是解决"子序列可以不连续"这一特性的必然要求。虽然增加了时间复杂度,但保证了算法的正确性。

通过这个问题,我们不仅学会了如何解决最长不下降子序列问题,更重要的是理解了动态规划的核心思想:将大问题分解为相互关联的子问题,通过解决子问题来构建原问题的解。这种思想在许多其他问题中也有广泛应用,值得我们深入掌握。

--------豪

2026.3.23 0:51

相关推荐
Rsun0455110 小时前
8、Java 代理模式从入门到实战
java·系统安全·代理模式
我登哥MVP2 天前
【Spring6笔记】 - 12 - 代理模式
java·spring boot·笔记·spring·代理模式·aop
yaaakaaang5 天前
十二、代理模式
java·代理模式
运维儿6 天前
7.ARP 代理与端口隔离:满足通信需求,保证通信安全
网络·智能路由器·代理模式·linux 网络·云计算网络
panzer_maus8 天前
工厂模式、代理模式与单例模式的介绍
java·设计模式·代理模式
花月C9 天前
线性动态规划(Linear DP)
算法·动态规划·代理模式
散峰而望9 天前
【基础算法】动态规划从入门到进阶:记忆化搜索、线性 DP、LIS/LCS 一网打尽
c++·后端·算法·github·深度优先·动态规划·代理模式
淡忘旧梦9 天前
ChatGPT回答白屏
人工智能·chatgpt·代理模式
会编程的土豆10 天前
【数据结构与算法】动态规划
数据结构·c++·算法·leetcode·代理模式
范纹杉想快点毕业11 天前
C语言编程实战宝典:550例精解·涵盖基础语法·数组指针·位运算·递归·贪心·动态规划
c语言·动态规划·代理模式