最长递增子序列问题(Longest Increasing Subsequence),动态规划法解决,贪心算法 + 二分查找优化




问题描述:在一个大小乱序的数列中,找到一个最大长度的递增子序列,子序列中的数据在原始数列中的相对位置保持不变,可以不连续,但必须递增。

输入描述:

第一行输入数列的长度 n。(1 <= n <= 200)
第二行输入数列的数据:a1 a2 a3 ... ... ,每个数据之间用空格隔开。(1 <= ai <= 350)

输出:

输出最大递增子序列的长度。

示例:

输入:
6
2 5 1 5 4 5
输出:
3
说明:
最大递增子序列可以是 {2,4,5} 也可以是 {1,4,5},所以最大递增子序列的长度为 3.
输出 3

动态规划法

可以使用 动态规划 的思想来设计该算法。定义数组 dp[n]dp[i] 表示以第 i 个元素结尾的最长递增子序列的长度。(有关动态规划,参考往期文章 【一篇小短文,理解动态规划问题 DP (Dynamic Programming)】)

两个 for 循环嵌套,外层循环遍历数组中的第 i 个元素,内层循环遍历第 i 个元素之前的所有元素。

每次内层循环结束,则计算出当前 第 i 个元素结尾的最长递增子序列长度。

后面的每次循环都基于前面已经解决的子问题。

状态转移方程:

  • arr 是长度为 n 的数组,arr[i] 表示第 i 个元素。
  • dp[i] 为以 arr[i] 结尾的最长递增子序列的长度

对于每个元素 arr[i],它的递增子序列可以通过之前的元素 arr[0], arr[1], ..., arr[i-1] 来扩展,因此我们可以利用之前的状态来更新 dp[i]

转移方程为:

d p [ i ] = max ⁡ ( d p [ i ] , d p [ j ] + 1 ) for 0 ≤ j < i and a r r [ i ] > a r r [ j ] dp[i] = \max(dp[i], dp[j] + 1) \quad \text{for} \quad 0 \leq j < i \quad \text{and} \quad arr[i] > arr[j] dp[i]=max(dp[i],dp[j]+1)for0j<iandarr[i]>arr[j]

即,对于每个 i,我们检查所有 jj 小于 i),如果 arr[i] 大于 arr[j],那么 arr[i] 可以作为以 arr[j] 结尾的递增子序列的后继,从而更新 dp[i]

C语言代码实现:

#include <stdio.h>
#include <stdlib.h>

int main() {
	int n;
	scanf("%d", &n);   // 接收数据 n
	int *arr = (int*)malloc(sizeof(int) * n); // 动态申请 n 个 int 型数据,数组 arr[n]
	int *dp = (int*)malloc(sizeof(int) * n);  // 动态申请数组 dp[n]

	for (int i = 0; i < n; i++) {
		scanf("%d", (arr + i));    //接收数组数据
		*(dp + i) = 1;      //初始化 dp 数组,即每个 arr[i] 结尾的数据至少可以形成一个以自身为结尾的递增子序列,长度为1
	}
	
	//进行动态规划
	for (int i = 1; i < n; i++) {
		for (int j = 0; j < i; j++) {
			if (arr[i] > arr[j]) {
				dp[i] = dp[i] > dp[j] + 1 ? dp[i] : dp[j] + 1;
			}
		}
	}

	int maxlength = 1;
	for (int i = 0; i < n; i++) {     //遍历 dp[i] 找到最大的一个
		if (dp[i] > maxlength)
			maxlength = dp[i];
	}

	printf("%d", maxlength);    //输出最大递增子序列的长度。
	
	free(arr);   // 释放 arr 内存
	free(dp);
	return 0;
}

如果 arr[i] > arr[j],则可以构成以递增关系,dp[j] 保存的是以 arr[j] 结尾的最大递增子序列的长度。dp[j] + 1 是因为当前找到的第 i 个元素也被算进来。dp[i] = dp[i] > dp[j] + 1 ? dp[i] : dp[j] + 1; 条件运算符,选择较大的一个更新 dp[i] 的值。



以输入的数据 4 3 1 2 5 6 为例,代码的执行过程:

1. 外层循环 i = 1
  • arr[1] = 3 查看 i前面的元素:

    • j = 0arr[0] = 4,因为 arr[1] (3) 小于 arr[0] (4),无法构成递增子序列,所以 dp[1] 保持为 1。
dp = [1, 1, 1, 1, 1, 1]
2. 外层循环 i = 2
  • arr[2] = 1 查看 i 前面的元素:

    • j = 0arr[0] = 4arr[2] (1) 小于 arr[0] (4),无法构成递增子序列,dp[2] 保持为 1。
    • j = 1arr[1] = 3arr[2] (1) 小于 arr[1] (3),无法构成递增子序列,dp[2] 保持为 1。
dp = [1, 1, 1, 1, 1, 1]
3. 外层循环 i = 3
  • arr[3] = 2 查看 i 前面的元素:

    • j = 0arr[0] = 4arr[3] (2) 小于 arr[0] (4),无法构成递增子序列,dp[3] 保持为 1。
    • j = 1arr[1] = 3arr[3] (2) 小于 arr[1] (3),无法构成递增子序列,dp[3] 保持为 1。
    • j = 2arr[2] = 1arr[3] (2) 大于 arr[2] (1),我们可以在 arr[2] 后面加上 arr[3],更新 dp[3] = dp[2] + 1 = 2
dp = [1, 1, 1, 2, 1, 1]
4. 外层循环 i = 4
  • arr[4] = 5 查看 i 前面的元素:

    • j = 0arr[0] = 4arr[4] (5) 大于 arr[0] (4),我们可以在 arr[0] 后面加上 arr[4],更新 dp[4] = dp[0] + 1 = 2
    • j = 1arr[1] = 3arr[4] (5) 大于 arr[1] (3),我们可以在 arr[1] 后面加上 arr[4],更新 dp[4] = dp[1] + 1 = 2(不需要更新,dp[4] 仍然为 2)。
    • j = 2arr[2] = 1arr[4] (5) 大于 arr[2] (1),我们可以在 arr[2] 后面加上 arr[4],更新 dp[4] = dp[2] + 1 = 2(不需要更新,dp[4] 仍然为 2)。
    • j = 3arr[3] = 2arr[4] (5) 大于 arr[3] (2),我们可以在 arr[3] 后面加上 arr[4],更新 dp[4] = dp[3] + 1 = 3
dp = [1, 1, 1, 2, 3, 1]
5. 外层循环 i = 5
  • arr[5] = 6 查看 i 前面的元素:

    • j = 0arr[0] = 4arr[5] (6) 大于 arr[0] (4),我们可以在 arr[0] 后面加上 arr[5],更新 dp[5] = dp[0] + 1 = 2
    • j = 1arr[1] = 3arr[5] (6) 大于 arr[1] (3),我们可以在 arr[1] 后面加上 arr[5],更新 dp[5] = dp[1] + 1 = 2(不需要更新,dp[5] 仍然为 2)。
    • j = 2arr[2] = 1arr[5] (6) 大于 arr[2] (1),我们可以在 arr[2] 后面加上 arr[5],更新 dp[5] = dp[2] + 1 = 2(不需要更新,dp[5] 仍然为 2)。
    • j = 3arr[3] = 2arr[5] (6) 大于 arr[3] (2),我们可以在 arr[3] 后面加上 arr[5],更新 dp[5] = dp[3] + 1 = 3
    • j = 4arr[4] = 5arr[5] (6) 大于 arr[4] (5),我们可以在 arr[4] 后面加上 arr[5],更新 dp[5] = dp[4] + 1 = 4
dp = [1, 1, 1, 2, 3, 4]
最终结果:
  • 最终 dp 数组为 [1, 1, 1, 2, 3, 4],表示每个位置上以该元素为结尾的最长递增子序列的长度。
  • 最长递增子序列的长度是 4,即 dp 数组中的最大值。


使用动态规划方法解决该问题,由于使用了双层 for 循环嵌套,所以代码的时间复杂度为 O ( n 2 ) \text{O}(n^2) O(n2),适用于中小规模数据。

贪心算法 + 二分查找

贪心算法(Greedy Algorithm) 是一种在求解问题时采取局部最优解的策略,目的是通过一步一步地做出选择,期望通过局部最优选择达到全局最优。换句话说,贪心算法在每一步都选择当前看起来最好的(最优的)选项,而不考虑这些选择是否会影响到后续的决策。

贪心算法的特点:

  1. 局部最优选择:每次决策时,选择当前最优解,假设局部最优能够带来全局最优解。
  2. 不回溯:一旦作出选择,就不会改变或回头考虑先前的选择。贪心算法通常不需要回溯到前一步的决策。
  3. 无法保证全局最优解:贪心算法的一个缺点是它并不总是能找到全局最优解,因为局部最优并不意味着全局最优。但是,对于某些问题,贪心算法能够给出全局最优解。

贪心算法工作步骤:

  1. 选择:在当前状态下做出一个选择,使得该选择是局部最优的。
  2. 可行性检查:检查当前的选择是否符合问题的约束。
  3. 解决子问题:做出选择后,递归地解决问题的子问题。
  4. 结束条件:当没有更多的选择可做时,结束算法。

C语言实现:

#include <stdio.h>
#include <stdlib.h>

// 二分查找函数,返回尾部元素的位置
int binarySearch(int* tails, int size, int target) {
    int left = 0, right = size - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (tails[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return left;
}

int main() {
    int n;
    scanf("%d", &n);
    int* arr = (int*)malloc(n * sizeof(int));
    int index = 0;
    while (scanf("%d", (arr + index++)) != EOF);

    // tails 数组,表示递增子序列的尾部元素
    int* tails = (int*)malloc(n * sizeof(int));
    int size = 0;  // 记录递增子序列的长度

    // 贪心 + 二分查找
    for (int i = 0; i < n; i++) {
        int pos = binarySearch(tails, size, arr[i]);
        tails[pos] = arr[i];  // 更新尾部元素
        if (pos == size) {
            size++;  // 如果当前位置是尾部的最末位置,子序列的长度加1
        }
    }

    printf("%d\n", size);  // 输出最长递增子序列的长度

    free(arr);
    free(tails);
    return 0;
}

代码解释:

	// 输入部分
    int n;
    scanf("%d", &n);
    int* arr = (int*)malloc(n * sizeof(int));   //申请数组内存
    int index = 0;
    while (scanf("%d", (arr + index++)) != EOF);     //使用指针操纵数组 arr ,一直读到 End Of File (EOF)

初始化 tails 数组:

int* tails = (int*)malloc(n * sizeof(int));
int size = 0;  // 记录递增子序列的长度

tails 数组用来存储当前所有递增子序列的尾部元素。在开始时,我们的递增子序列长度为 0,因此 size 初始为 0。

二分查找函数:

int binarySearch(int* tails, int size, int target) {
    int left = 0, right = size - 1;  // 初始化左边界和右边界
    while (left <= right) {  // 当搜索区间内有元素时,继续查找
        int mid = left + (right - left) / 2;  // 计算中间位置
        if (tails[mid] < target) {  // 如果 mid 位置的元素小于 target
            left = mid + 1;  // 说明 target 可能在 mid 右边,所以更新左边界
        } else {  // 如果 mid 位置的元素大于或等于 target
            right = mid - 1;  // 说明 target 可能在 mid 左边,所以更新右边界
        }
    }
    return left;  // 返回插入位置
}

通过二分查找在一个有序的数组 tails 中找到一个位置,使得如果插入 target,数组依然保持有序。函数的返回值是 target 应该插入的位置。

二分查找的基本思想是:通过反复折半查找范围来逐渐缩小搜索区间,从而提高查找效率。

  • binarySearch(int *tails, int size, int target) 二分查找函数,目的是找到 tails 中第一个大于或等于 target 的位置。
  • tails[mid] < target 时,表示 target 可以放到 mid 右边,因此我们将 left 移动到 mid + 1
  • tails[mid] >= target 时,表示我们要寻找更小的值,因此将 right 移动到 mid - 1
  • 最终返回的 left 就是 target 应该插入的位置。

遍历数组并更新 tails 数组:

for (int i = 0; i < n; i++) {
    int pos = binarySearch(tails, size, arr[i]);
    tails[pos] = arr[i];  // 更新尾部元素
    if (pos == size) {
        size++;  // 如果当前位置是尾部的最末位置,子序列的长度加1
    }
}
  • 对于每个输入数组 arr[i],我们通过二分查找找出它应该插入 tails 数组的位置 pos
  • 如果 tails[pos] 是该元素,说明我们已经可以更新该位置的尾部元素,否则我们在 tails 数组中找到一个位置并将其更新为 arr[i]
  • 如果 pos 等于当前 tails 数组的长度(即 size),意味着我们发现了一个比当前 tails 数组中的任何尾部元素都要大的元素,此时可以将 tails 数组的长度加 1,表示找到了一个新的递增子序列的末尾。
算法的核心思想
  1. 贪心算法
    • 我们试图尽可能让每个新元素扩展已有的递增子序列,或者替换掉某个尾部元素,以便为后续的更大的元素腾出空间。
    • 通过不断更新 tails 数组,我们能确保 tails 数组中保持着当前所有递增子序列的最小尾部元素。这样,tails 数组越长,代表最长递增子序列的长度越长。
  2. 二分查找
    • 我们用二分查找来快速找到 tails 数组中第一个大于或等于当前元素的位置。这是该算法的关键,利用二分查找来确保每次更新 tails 数组的时间复杂度为 O(log n),从而把总的时间复杂度降到了 O(n log n)
例子分析

假设输入序列为:4, 3, 1, 2, 5, 6

  • 初始化:
    • arr = [4, 3, 1, 2, 5, 6]
    • tails = []size = 0
  • 第 1 个元素 4:
    • binarySearch(tails, 0, 4) 返回位置 0tails 为空,4 应该放到第一个位置)。
    • 更新 tails = [4]size = 1
  • 第 2 个元素 3:
    • binarySearch(tails, 1, 3) 返回位置 0tails[0] = 43 比它小,插入位置是 0)。
    • 更新 tails = [3]size = 1
  • 第 3 个元素 1:
    • binarySearch(tails, 1, 1) 返回位置 0tails[0] = 31 比它小,插入位置是 0)。
    • 更新 tails = [1]size = 1
  • 第 4 个元素 2:
    • binarySearch(tails, 1, 2) 返回位置 1tails[0] = 12 比它大,插入位置是 1)。
    • 更新 tails = [1, 2]size = 2
  • 第 5 个元素 5:
    • binarySearch(tails, 2, 5) 返回位置 2tails[0] = 1tails[1] = 25 比它们都大,插入位置是 2)。
    • 更新 tails = [1, 2, 5]size = 3
  • 第 6 个元素 6:
    • binarySearch(tails, 3, 6) 返回位置 3tails[0] = 1tails[1] = 2tails[2] = 56 比它们都大,插入位置是 3)。
    • 更新 tails = [1, 2, 5, 6]size = 4

最后,tails = [1, 2, 5, 6],最长递增子序列的长度是 4


第二个算法的时间复杂度为 O ( n ⋅ log  n ) \text{O}(n \cdot \text{log}\ n) O(nlog n) ,适用于大规模数据。在输出最大递增子序列的长度的同时,也找出了具体的最大递增子序列 tails




END

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/957695.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Elasticsearch】 Ingest Pipeline `processors`属性详解

在Elasticsearch中&#xff0c;Ingest Pipeline 的 processors 属性是一个数组&#xff0c;包含一个或多个处理器&#xff08;processors&#xff09;。每个处理器定义了一个数据处理步骤&#xff0c;可以在数据索引之前对数据进行预处理或富化。以下是对 processors 属性中常见…

架构思考与实践:从通用到场景的转变

在当今复杂多变的商业环境中&#xff0c;企业架构的设计与优化成为了一个关键议题。本文通过一系列随笔&#xff0c;探讨了业务架构的价值、从通用架构到场景架构的转变、恰如其分的架构设计以及如何避免盲目低效等问题。通过对多个实际案例的分析&#xff0c;笔者揭示了架构设…

消息队列实战指南:三大MQ 与 Kafka 适用场景全解析

前言&#xff1a;在当今数字化时代&#xff0c;分布式系统和大数据处理变得愈发普遍&#xff0c;消息队列作为其中的关键组件&#xff0c;承担着系统解耦、异步通信、流量削峰等重要职责。ActiveMQ、RabbitMQ、RocketMQ 和 Kafka 作为市场上极具代表性的消息队列产品&#xff0…

win32汇编环境,怎么得到磁盘的盘符

;运行效果 ;win32汇编环境,怎么得到磁盘的盘符 ;以下代码主要为了展示一下原理&#xff0c;应用GetLogicalDrives、GetLogicalDriveStrings函数、屏蔽某些二进制位、按双字节复制内容等。以下代码最多查8个盘&#xff0c;即返回值中的1个字节的信息 ;直接抄进RadAsm可编译运行。…

微软预测 AI 2025,AI Agents 重塑工作形式

1月初&#xff0c;微软在官网发布了2025年6大AI预测&#xff0c;分别是&#xff1a;AI模型将变得更加强大和有用、AI Agents将彻底改变工作方式、AI伴侣将支持日常生活、AI资源的利用将更高效、测试与定制是开发AI的关键以及AI将加速科学研究突破。 值得一提的是&#xff0c;微…

网络编程套接字(二)

目录 TCP网络程序 服务端初始化 创建套接字 服务端绑定 服务端监听 服务端启动 服务端获取连接 服务端处理请求 客户端初始化 客户端启动 发起连接 发起请求 网络测试 多进程版TCP网络程序 捕捉SIGCHLD信号 孙子进程提供服务 多线程版TCP网络程序 线程池版TC…

网站HTTP改成HTTPS

您不仅需要知道如何将HTTP转换为HTTPS&#xff0c;还必须在不妨碍您的网站自成立以来建立的任何搜索排名权限的情况下进行切换。 为什么应该从HTTP转换为HTTPS&#xff1f; 与非安全HTTP于不同&#xff0c;安全域使用SSL&#xff08;安全套接字层&#xff09;服务器上的加密代…

渗透测试--攻击常见的Web应用

本文章咱主要讨论&#xff0c;常见Web应用的攻击手法&#xff0c;其中并不完全&#xff0c;因为Web应用是在太多无法囊括全部&#xff0c;但其中的手法思想却值得我们借鉴&#xff0c;所以俺在此做了记录&#xff0c;希望对大家有帮助&#xff01;主要有以下内容&#xff1a; 1…

外包公司名单一览表(成都)

大家好&#xff0c;我是苍何。 之前写了一篇武汉的外包公司名单&#xff0c;评论区做了个简单统计&#xff0c;很多人说&#xff0c;在外包的日子很煎熬&#xff0c;不再想去了。 有小伙伴留言说有些外包会强制离职&#xff0c;不行就转岗&#xff0c;让人极度没有安全感。 这…

2025 最新flutter面试总结

目录 1.Dart是值传递还是引用传递&#xff1f; 2.Flutter 是单引擎还是双引擎 3. StatelessWidget 和 StatefulWidget 在 Flutter 中有什么区别&#xff1f; 4.简述Dart语音特性 5. Navigator 是什么&#xff1f;在 Flutter 中 Routes 是什么&#xff1f; 6、Dart 是不是…

Spring Boot安全加固:基于Spring Security的权限管理

引言 在当今数字化时代&#xff0c;随着企业信息化程度的不断提高&#xff0c;应用程序的安全性成为了一个至关重要的问题。Spring Boot 作为 Java 生态系统中广泛使用的开发框架&#xff0c;以其简洁、高效的特点深受开发者的喜爱。然而&#xff0c;仅仅依靠 Spring Boot 的默…

论文笔记(六十二)Diffusion Reward Learning Rewards via Conditional Video Diffusion

Diffusion Reward Learning Rewards via Conditional Video Diffusion 文章概括摘要1 引言2 相关工作3 前言4 方法4.1 基于扩散模型的专家视频建模4.2 条件熵作为奖励4.3 训练细节 5 实验5.1 实验设置5.2 主要结果5.3 零样本奖励泛化5.4 真实机器人评估5.5 消融研究 6 结论 文章…

工业缺陷检测实战——基于深度学习YOLOv10神经网络PCB缺陷检测系统

基于深度学习YOLOv10神经网络PCB缺陷检测系统&#xff0c;其能识别六种PCB缺陷&#xff1a;names {0:missing_hole, 1:mouse_bite, 2:open_circuit, 3:short, 4:spur, 5:spurious_copper} CH_names [缺失孔,鼠标咬伤,开路,短路,杂散,伪铜] 具体图片见如下&#xff1a; 第一步…

React+AntDesign实现类似Chatgpt交互界面

以下是一个基于React和Ant Design搭建的简单ChatGPT风格前端交互界面代码框架示例&#xff0c;该示例实现了基本的用户输入、发送请求以及展示回复的功能。假设后端有一个模拟接口来处理请求并返回回复。 1. 项目初始化&#xff1a; 确保你已经安装了Node.js和npm。通过以下命…

FANUC机器人系统镜像备份与恢复的具体步骤(图文)

FANUC机器人系统镜像备份与恢复的具体步骤(图文) 镜像备份: 如下图所示,进入文件—工具—切换设备,找到插入的U盘UT1, 如下图所示,进入U盘目录后,创建目录,这里目录名称为11, 如下图所示࿰

MongoDB 备份与恢复综述

目录 一、基本概述 二、逻辑备份 1、全量备份 2、增量备份 3、恢复 三、物理备份 1、cp/tar/fsync 2、WiredTiger 热备份 3、恢复 四、快照备份 一、基本概述 MongoDB 是一种流行的 NoSQL 数据库&#xff0c;它使用文档存储数据&#xff0c;支持丰富的查询语言和索引…

【Qt 常用控件】显示类控件——QLabel

目录 1.QLabel 1.1 textFormat 文本类型 普通文本和富文本 Markdown格式 1.2 alignment 文本对齐方式 1.3 wordWrap 自动换行 1.4 indent 文本缩进 1.5 margin 边距 1.6 buddy&#xff0c;qlabel伙伴 1.7 pixmap图片 和 scaledContents自动填充 1.QLabel 功能&#x…

npm install 报错:Command failed: git checkout 2.2.0-c

[TOC](npm install 报错&#xff1a;Command failed: git checkout 2.2.0-c) npm install 报错&#xff1a;Command failed: git checkout 2.2.0-c export NODE_HOME/usr/local/node-v14.14.0-linux-x64 npm config set registry https://registry.npmmirror.com 使用如上环…

Oracle 创建并使用外部表

目录 一. 什么是外部表二. 创建外部表所在的文件夹对象三. 授予访问外部表文件夹的权限3.1 DBA用户授予普通用户访问外部表文件夹的权限3.2 授予Win10上的Oracle用户访问桌面文件夹的权限 四. 普通用户创建外部表五. 查询六. 删除 一. 什么是外部表 在 Oracle 数据库中&#x…

戴尔电脑用u盘重装系统_戴尔电脑用u盘重装win10系统教程

戴尔电脑用u盘重装系统&#xff1f;戴尔电脑这几年默认预装win10家庭版和win11家庭版。有的用户用上了预装win11家庭版的戴尔电脑&#xff0c;使用一段时间依然不习惯&#xff0c;于是想退回win10。但不知道怎么重装win10&#xff0c;这几年的戴尔电脑建议采用U盘方式安装系统比…