动态规划-最长上升子序列问题

个人主页:

<zephyr05>

个人专栏:

<Linux> <C/C++> <竞赛专栏>

座右铭:

等风来不如追风去,追寻的过程就是人生的意义。


目录

[一. 问题定义](#一. 问题定义)

[二. 实现算法](#二. 实现算法)

动态规划(O(n²))

[三. 例题解析](#三. 例题解析)

题目描述:题目链接

思路分析:

代码:


一. 问题定义

最长上升子序列(Longest Increasing Subsequence,简称 LIS)是动态规划中最经典、最基础的问题之一。

给定一个无序的整数数组 nums,找到其中最长的严格递增子序列的长度。

  • 子序列:不要求连续,但保持原数组的相对顺序。

  • 严格递增 :后一个元素必须 大于 前一个(nums[i] < nums[j])。

nums = [10, 9, 2, 5, 3, 7, 101, 18]

最长上升子序列是 [2, 3, 7, 101][2, 5, 7, 101],长度为 4

二. 实现算法

动态规划(O(n²))

  • 状态定义 :令 dp[i] 表示 nums[i] 结尾 的最长上升子序列的长度。

  • 状态转移 :遍历 i 之前的所有元素 j0 <= j < i),如果 nums[j] < nums[i],说明可以把 nums[i] 接在以 nums[j] 结尾的序列后面,形成更长的序列。

  • 初始化 :每个元素自身至少构成长度为 1 的子序列,所以 dp[i] = 1

  • 最终答案 :取 dp 数组中的最大值。

三. 例题解析

题目描述:题目链接

N位同学站成一排,音乐老师要请其中的 (N-K) 位同学出列,使得剩下的K位同学排成合唱队形。

合唱队形是指这样的一种队形:设K位同学从左到右依次编号为 1,2...,K,他们的身高分别为 T1,T2,...,TK, 则他们的身高满足 t1<t2...<ti>ti+1>...>tk−1>tk(1≤i≤k) t1​<t2​...<ti​>ti+1​>...>tk−1​>tk​(1≤i≤k)

你的任务是,已知所有 n 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

思路分析:

题目问"最少出列几人",如果直接想删谁,情况极其复杂,但我们可以转换一下思路,总数 n 固定,最少出列 = n - 最多留下, 因此**,** 我们不再关注踢掉谁,而是全力计算 "最多能留下多少人,组成合唱队形"

合唱队形的数学定义是:t1 < t2 < ... < ti > ... > tk

这个形状像一座山,有且只有一个峰顶 (假设峰顶是第 i 个人),站在峰顶往左看 ,身高严格递增(即最长上升 );站在峰顶往右看 ,身高严格递减(即最长下降)。

现在,把目光锁定在任意一个同学 i 作为峰顶:

  • 左侧(0 到 i) :要求以 arr[i] 结尾,身高严格递增。

    这完美对应了 "以 i 结尾的最长上升子序列"

  • 右侧(i 到 n-1) :要求以 arr[i] 开头,身高严格递减。

    如果我们把右侧从右往左看 ,递减就变成了递增。

    所以它等价于 "以 i 结尾的,从右往左看的最长上升子序列"

为了把上述思路落地,我们定义两个数组 ,分别记录左右两侧的**"最优解"**:

  • fi :表示从左到右,以第 i 个同学作为终点的最长上升子序列长度。

  • gi :表示从右到左,以第 i 个同学作为终点的最长上升子序列长度(等价于原序列中以 i 为起点的最长下降子序列)。

这样一来,如果让第 i 个人当峰顶

  • 左侧最多留下 fi 人(包括峰顶)。

  • 右侧最多留下 gi 人(包括峰顶)。

  • 但峰顶被左边和右边重复计算了两次 ,所以以 i 为峰顶的最大保留人数 = fi+gi−1。

我们并不知道谁最适合当峰顶,但没关系,遍历所有位置 i(0 到 n-1),把每个人假设成峰顶算一遍。

  • 最大保留人数 =

  • 最终答案(最少出列)= n− 最大保留人数

代码:

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

int n;                 // 同学总数
int arr[1010];        // 存储每位同学的身高,下标从 0 开始
int f[1010];          // f[i]:以 i 结尾的【从左到右】最长上升子序列长度
int g[1010];          // g[i]:以 i 开头的【从右到左】最长上升子序列长度
                      //      等价于以 i 结尾的【从左到右】最长递减子序列长度

int main() {
    // ---------- 1. 读入数据 ----------
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> arr[i];
    }

    // ---------- 2. 计算 f[i]:从左到右的最长上升子序列 ----------
    // 状态转移:f[i] = max( f[j] + 1 ),其中 j < i 且 arr[j] < arr[i]
    for (int i = 0; i < n; i++) {
        f[i] = 1;                     // 至少包含自己,长度为 1
        for (int j = 0; j < i; j++) { // 枚举所有可能的左边元素
            if (arr[i] > arr[j]) {    // 只有严格递增才能接在后面
                // 尝试用 f[j] + 1 更新 f[i]
                f[i] = max(f[i], f[j] + 1);
            }
        }
    }

    // ---------- 3. 计算 g[i]:从右到左的最长上升子序列 ----------
    // 也就是从 i 到末尾的最长递减子序列(因为反过来看是上升)
    // 状态转移:g[i] = max( g[j] + 1 ),其中 j > i 且 arr[i] > arr[j]
    for (int i = n - 1; i >= 0; i--) {
        g[i] = 1;                     // 至少包含自己
        for (int j = i + 1; j < n; j++) { // 枚举所有可能的右边元素
            if (arr[i] > arr[j]) {    // 注意这里是 arr[i] > arr[j],构成递减
                g[i] = max(g[i], g[j] + 1);
            }
        }
    }

    // ---------- 4. 枚举每个位置作为"峰顶",求最大保留人数 ----------
    // 以 i 为峰顶的合唱队形长度 = f[i] + g[i] - 1(i 被算了两次)
    // 需要出列的人数 = n - (最大合唱队形长度)
    int ans = 0x3f3f3f3f;   // 初始化为一个很大的数,用于取最小值
    for (int i = 0; i < n; i++) {
        ans=min(ans,n-(f[i]+g[i]-1));
        // 更新最少出列人数
    }

    // ---------- 5. 输出结果 ----------
    cout << ans << endl;
    return 0;
}

如果本文对你有帮助,欢迎点赞、收藏、分享。如有疑问,请在评论区留言,我会尽力解答。

博客首发于 CSDN,转载请注明出处。

相关推荐
闪电悠米1 小时前
力扣hot100-56.合并区间-排序详解
数据结构·算法·leetcode·贪心算法·排序算法
卡提西亚3 小时前
leetcode-1438. 绝对差不超过限制的最长连续子数组
算法·leetcode·职场和发展
Java面试题总结3 小时前
LeetCode 93.复原IP地址
算法·leetcode·职场和发展·.net
从零开始的代码生活_3 小时前
C++ 多态详解:虚函数、动态绑定、抽象类与虚表原理
开发语言·c++·后端·学习·算法
泷寂4 小时前
最小生成树 (MST基础)
算法
Daniel_1234 小时前
数组——总结篇
算法
不懒不懒4 小时前
【针对路面识别数据集,结合三轴加速度标准化数据及多路面识别需求,以下是算法选择与处理方案】
算法
Reart5 小时前
Leetcode 121. 买卖股票的最佳时机(717)
后端·算法
会编程的小孩5 小时前
初识数据类型以及变量定义
数据结构·算法