【狂热算法篇】解锁数据潜能:探秘前沿 LIS 算法

简介: 【狂热算法篇】解锁数据潜能:探秘前沿 LIS 算法

本篇简介:
本篇以LIS算法展开细致介绍,不同方法的实现如动态规划,贪心二分实现等;并配合实际例题进行应用;望对读者学习LIS算法有帮助。

一·LIS算法:
1.1算法定义及目的:
LIS(Longest Increasing Subsequence)算法,即最长递增子序列算法。

它的目标是在一个给定的序列(可以是数字序列、字符序列等)中,找到一个子序列,这个子序列中的元素是按照递增顺序排列的,并且在所有符合递增条件的子序列中,长度是最长的。

例如,在序列 [1, 3, 2, 4, 5] 中,最长递增子序列是 [1, 2, 4, 5],长度为 4。这个算法在很多领域都有应用,比如数据挖掘中分析数据的趋势,或者在文本处理中分析文本的某种递增模式。

1.2实现方法:
1.2.1简单的暴力解法:
又称枚举法;下面说一下它的思路:

这种方法是最直观的。它会枚举所有可能的子序列,然后检查每个子序列是否是递增的,并记录下最长的递增子序列。

具体来说,对于一个长度为 n 的序列,它有 2^n 个子序列,需要逐个检查这些子序列是否满足递增条件。

但是它有个致命的缺点:

时间复杂度非常高,达到了指数级别 O (2^n)。在实际应用中,当序列长度稍微变大时,计算量会变得极其庞大,效率极低。例如,当 n = 20 时,2^20 约等于 100 万次运算,这会导致程序运行时间过长。

代码实现:

// 暴力解法
int lis_brute_force(const std::vector& nums) {
int n = nums.size();
int max_length = 0;
// 枚举所有可能的子序列
for (int mask = 0; mask < (1 << n); ++mask) {
std::vector subseq;
for (int i = 0; i < n; ++i) {
if (mask & (1 << i)) {
subseq.push_back(nums[i]);
}
}
bool is_increasing = true;
for (size_t i = 1; i < subseq.size(); ++i) {
if (subseq[i] <= subseq[i - 1]) {
is_increasing = false;
break;
}
}
if (is_increasing) {
max_length = std::max(max_length, static_cast(subseq.size()));
}
}
return max_length;
}

代码解释:

使用位运算枚举所有可能的子序列。mask 从 0 到 (1 << n) - 1,对于每个 mask,将对应位为 1 的元素添加到 subseq 向量中。

检查 subseq 是否是递增序列,如果是,更新 max_length。

时间复杂度为 o(2^n),效率低,适合较短序列。

因此我们极大不推荐这种解法。

1.2.2动态规划解法:
原理:

1.2.2.1从左往右填表:
动态规划的核心思想是将一个复杂的问题分解为一系列相互关联的子问题,并通过记录子问题的解来避免重复计算。

对于 LIS 算法,我们定义一个状态数组 dp,其中 dp [i] 表示以第 i 个元素结尾的最长递增子序列的长度。

状态转移方程:

dp [i]=max (dp [j]) + 1,其中 j < i 且 nums [j]<nums [i]。

这意味着要找到在 i 之前的元素 j,使得 nums [j] 小于 nums [i],并且 dp [j] 是最大的,然后将 dp [i] 更新为 dp [j]+1。

时间复杂度分析:

时间复杂度为 O (n^2),相比于暴力解法有了很大的提升。

例如,对于长度为 100 的序列,暴力解法可能需要计算 2^100 次左右,而动态规划只需要计算 100^2 = 10000 次左右,大大减少了计算量。

代码实现:

首先,定义完了dp状态,先明确我们的任务:即从这这段数组中(其实是参差不齐),但是我们忽略降序的数,只从升序的那条路看起,找最长的。

比如我们到了i位置,想要得到的是? 以i位置为结尾的最长(包括i位置);因此我们需要在i之前找一条升序最长路L(末尾元素一定要比i小)--->这里正好就是我们的dp[j](j是由0~i-1);因此我们只需要遍历得到以j为结尾的dp的最大值就好;如果是结尾元素值比我们标定i对应值大;那么这个j就不能作为L的结尾元素;直接else跳过。

故,上面所说的我们最后是遍历得到L然后再+1得到dp[i];因此我们让它更加贴近一下dp状态方程的写法-->下面我们只用维护L+1(dp[i])的最大值即可了

int lis_dp(const std::vector& nums) {
int n = nums.size();
std::vector dp(n, 1);
int max_length = 1;
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[i] > nums[j]) {
// 状态转移方程 dp[i] = max(dp[i], dp[j] + 1)
dp[i] = std::max(dp[i], dp[j] + 1);
}
}
max_length = std::max(max_length, dp[i]);
}
return max_length;
}
代码解释:

使用 dp 数组存储以每个元素结尾的最长递增子序列的长度。

对于每个元素 i,遍历 i 之前的元素 j,若 nums[i] > nums[j],根据状态转移方程 dp[i] = std::max(dp[i], dp[j] + 1) 更新 dp[i]。

最终 max_length 存储了最长递增子序列的长度,时间复杂度为 。

1.2.2.2从右向左填表:
这里还可以是dp[i]表示以第i个元素为开始的 最长递增子序列的长度;这里只不过就是和上面的填表顺序颠倒一下。

代码展示:

这里的实现思路其实大差不多;我们只是定义的dp状态不同;只是所谓的推导从前变成了后了;

i为开始元素:因此我们要找的是i的右侧的大于i位置值的升序最长路L;因此从i+1开始看(必须要包含i+1为起点的最大路L)【->故我们是当填充i从i右侧找;因此这个是逆向填表(从右往左填表)】;注:遍历时,这里我们符合的要求(L)首先是j位置的值一定要大于i位置;其次就是找最长;因此遍历到长的L;因此我们为了让它像dp状态方程写法,故每次是求L+1作为dp[i]暂定值,然后求个max。

int LIS(vector d) {
int maxn = 1;
vector dp(d.size(),1);
for (int i = d.size() - 2; i >= 0; i--) {
for (int j = i + 1; j < d.size(); j++) {
if (d[i] < d[j]) dp[i] = max(dp[i],dp[j] + 1);
}
maxn = max(dp[i],maxn);
}
return maxn;
}
这两种定义方法都可以。

1.2.3贪心算法 + 二分查找解法:
贪心思想引入:

贪心算法的策略是在每一步都做出当前看起来最优的选择。对于 LIS 算法,我们维护一个辅助数组 tail,它存储了当前找到的最长递增子序列。当扫描到一个新元素时,我们尽量将它插入到 tail 数组中合适的位置,使得 tail 数组仍然保持递增。

二分查找:

为了高效地将新元素插入到 tail 数组中,我们使用二分查找。每次插入元素时,在 tail 数组中找到第一个大于等于新元素的位置,然后用新元素替换它。这样可以保证 tail 数组始终是递增的,并且长度尽可能长。

时间复杂度:

这种方法的时间复杂度可以达到 O (nlogn),这是一种比较高效的解法。例如,对于长度为 1000 的序列,其计算量比动态规划的 O (n^2) 解法又减少了很多,能够更快地得到结果。

代码实现:

int lis_greedy(const std::vector& nums) {
std::vector tail;
for (int num : nums) {
auto it = std::lower_bound(tail.begin(), tail.end(), num);
if (it == tail.end()) {
tail.push_back(num);
} else {
*it = num;
}
}
return tail.size();
}
代码解释:

维护 tail 向量存储当前找到的最长递增子序列。

对于每个元素 num,使用 std::lower_bound 找到 tail 中第一个大于等于 num 的位置。

如果 it 等于 tail.end(),说明 num 比 tail 中所有元素都大,添加到 tail 末尾;否则,将 tail 中 it 位置的元素更新为 num。

时间复杂度为 o(nlogn),是最有效的实现方式

这种也是比较推荐的!!!

2·算法应用场景:
2.1数据分析与挖掘:
在分析数据的趋势变化时,LIS 算法可以帮助找到数据中的上升趋势部分,例如股票价格的上涨阶段、气温的上升周期等。

2.2资源分配优化:
在任务调度或者资源分配场景中,如果任务有先后顺序要求或者资源有递增的利用顺序,LIS 算法可以辅助找到最优的分配方案。

2.3文本处理:
在文本编辑软件中,用于分析文本段落中句子的长度递增模式,或者单词的某种语义递增模式,辅助进行文本排版或者语义分析。

二·LIS算法例题:
下面我们就用一道例题,来应用上面所述的LIS算法解答吧:

测试用例:

输入:6 1 4 2 2 5 6

输出:4

题目链接: 蓝桥账户中心

首先我们先看数据范围:

这里我们由题意可以看出:

总结下:就是当选了大的数值后就无法在选择比它小的数值;因此我们可以 选择把它排好升序,但是此时要注意符合刚才说的要求即可,这时就联想到了LIS算法了,但是前两种即暴力,动态规划实现是不合适的(大数据范围),因此后面我们就用贪心+二分来实现。

这里也就是要求我们要是升序,并且不能重复 (去重复:1·利用lower_bound的特性找到相同的会覆盖掉,2·当发现找不到返回的是区间最后+1位置的迭代器也是会覆盖,不记录len(如上图的情况))。

因此直接上手代码:

include

using namespace std;
const int N=3e5+5;
int r[N]={0};
int main()
{ //最长递增子序列问题(贴近):lis
int n;
cin>>n;
vectorv(n+1);
for(int i=1;i<=n;i++) cin>>v[i];
int len=0;//r答案数组的长度:确保r数组始终是升序的
//(不是一般的sort类的升序,而是确保了在r数组中出现的升序元素相对位置符合原数组的相对位置)
for(int i=1;i<=n;i++){
if(v[i]>r[len]) r[++len]=v[i];//比它大直接放在后面(多可以个0位置,方便比较)
else {
//返回大于它的数完成覆盖:
//找不到也就是r数组存在于当前值第一大的值,放在len后面的位置,之后会覆盖(相当于相等的数据就是无效的)
// int pos=lower_bound(r+1,r+len+1,v[i])-r;
// r[pos]=v[i];

    //或者
    *lower_bound(r+1,r+len+1,v[i])=v[i];

 }

}
cout<<len;
return 0;
}

这道题也是不n能用o(N^2)复杂度的动态规划局解法;故就用复杂度为o(nlogn)的贪心二分来解决。

最后也是通过了。

三·本篇小结:
本篇介绍了LIS算法;下面介绍一下具体怎么用来小结一下:

当我们发现要求的是最长地址子序列,就可以选择它;具体有三种情况;但是合适的要么就动归,要么就二分;根据数据范围选择即可了。

如果我们只要这个序列的长度:两种都可。

但是,如果是还要它的数据那么就只能选二分来模拟了。

二分的话,就要保证:在lis数组中出现的升序元素相对位置符合原数组的相对位置。

相关文章
|
1月前
|
算法 编译器 C++
【狂热算法篇】探秘图论之Dijkstra 算法:穿越图的迷宫的最短路径力量(通俗易懂版)
【狂热算法篇】探秘图论之Dijkstra 算法:穿越图的迷宫的最短路径力量(通俗易懂版)
|
1月前
|
机器学习/深度学习 算法 搜索推荐
联邦学习的未来:深入剖析FedAvg算法与数据不均衡的解决之道
随着数据隐私和数据安全法规的不断加强,传统的集中式机器学习方法受到越来越多的限制。为了在分布式数据场景中高效训练模型,同时保护用户数据隐私,联邦学习(Federated Learning, FL)应运而生。它允许多个参与方在本地数据上训练模型,并通过共享模型参数而非原始数据,实现协同建模。
|
13天前
|
资源调度 算法 数据可视化
基于IEKF迭代扩展卡尔曼滤波算法的数据跟踪matlab仿真,对比EKF和UKF
本项目基于MATLAB2022A实现IEKF迭代扩展卡尔曼滤波算法的数据跟踪仿真,对比EKF和UKF的性能。通过仿真输出误差收敛曲线和误差协方差收敛曲线,展示三种滤波器的精度差异。核心程序包括数据处理、误差计算及可视化展示。IEKF通过多次迭代线性化过程,增强非线性处理能力;UKF避免线性化,使用sigma点直接处理非线性问题;EKF则通过一次线性化简化处理。
|
26天前
|
算法 图形学 数据安全/隐私保护
基于NURBS曲线的数据拟合算法matlab仿真
本程序基于NURBS曲线实现数据拟合,适用于计算机图形学、CAD/CAM等领域。通过控制顶点和权重,精确表示复杂形状,特别适合真实对象建模和数据点光滑拟合。程序在MATLAB2022A上运行,展示了T1至T7的测试结果,无水印输出。核心算法采用梯度下降等优化技术调整参数,最小化误差函数E,确保迭代收敛,提供高质量的拟合效果。
|
1月前
|
算法 Serverless 数据处理
从集思录可转债数据探秘:Python与C++实现的移动平均算法应用
本文探讨了如何利用移动平均算法分析集思录提供的可转债数据,帮助投资者把握价格趋势。通过Python和C++两种编程语言实现简单移动平均(SMA),展示了数据处理的具体方法。Python代码借助`pandas`库轻松计算5日SMA,而C++代码则通过高效的数据处理展示了SMA的计算过程。集思录平台提供了详尽且及时的可转债数据,助力投资者结合算法与社区讨论,做出更明智的投资决策。掌握这些工具和技术,有助于在复杂多变的金融市场中挖掘更多价值。
58 12
|
25天前
|
存储 监控 算法
公司监控上网软件架构:基于 C++ 链表算法的数据关联机制探讨
在数字化办公时代,公司监控上网软件成为企业管理网络资源和保障信息安全的关键工具。本文深入剖析C++中的链表数据结构及其在该软件中的应用。链表通过节点存储网络访问记录,具备高效插入、删除操作及节省内存的优势,助力企业实时追踪员工上网行为,提升运营效率并降低安全风险。示例代码展示了如何用C++实现链表记录上网行为,并模拟发送至服务器。链表为公司监控上网软件提供了灵活高效的数据管理方式,但实际开发还需考虑安全性、隐私保护等多方面因素。
23 0
公司监控上网软件架构:基于 C++ 链表算法的数据关联机制探讨
|
1月前
|
存储 算法 iOS开发
【狂热算法篇】并查集:探秘图论中的 “连通神器”,解锁动态连通性的神秘力量(通俗易懂版)
【狂热算法篇】并查集:探秘图论中的 “连通神器”,解锁动态连通性的神秘力量(通俗易懂版)
|
1月前
|
存储 算法 测试技术
【狂热算法篇】探秘图论之 Floyd 算法:解锁最短路径的神秘密码(通俗易懂版)
【狂热算法篇】探秘图论之 Floyd 算法:解锁最短路径的神秘密码(通俗易懂版)
|
1月前
|
存储 算法 索引
【狂热算法篇】探秘差分数组:算法星河中闪耀的区间掌控之星
【狂热算法篇】探秘差分数组:算法星河中闪耀的区间掌控之星
|
4月前
|
存储 编解码 负载均衡
数据分片算法
【10月更文挑战第25天】不同的数据分片算法适用于不同的应用场景和数据特点,在实际应用中,需要根据具体的业务需求、数据分布情况、系统性能要求等因素综合考虑,选择合适的数据分片算法,以实现数据的高效存储、查询和处理。

热门文章

最新文章