最长递增子序列问题的求解
最长递增子序列问题是一个很基本、较常见的小问题,但这个问题的求解方法却并不那么显而易见,需要较深入的思考和较好的算法素养才能得出良好的算法。由于这个问题能运用学过的基本的算法分析和设计的方法与思想,能够锻炼设计较复杂算法的思维,我对这个问题进行了较深入的分析思考,得出了几种复杂度不同算法,并给出了分析和证明。
一, 最长递增子序列问题的描述
设L=<a1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。
二, 第一种算法:转化为LCS问题求解
设序列X=<b1,b2,…,bn>是对序列L=<a1,a2,…,an>按递增排好序的序列。那么显然X与L的最长公共子序列即为L的最长递增子序列。这样就把求最长递增子序列的问题转化为求最长公共子序列问题LCS了。
最长公共子序列问题用动态规划的算法可解。设Li=< a1,a2,…,ai>,Xj=< b1,b2,…,bj>,它们分别为L和X的子序列。令C[i,j]为Li与Xj的最长公共子序列的长度。则有如下的递推方程:
这可以用时间复杂度为O(n2)的算法求解,由于这个算法上课时讲过,所以具体代码在此略去。求最长递增子序列的算法时间复杂度由排序所用的O(nlogn)的时间加上求LCS的O(n2)的时间,算法的最坏时间复杂度为O(nlogn)+O(n2)=O(n2)。
三, 第二种算法:动态规划法
设f(i)表示L中以ai为末元素的最长递增子序列的长度。则有如下的递推方程:
这个递推方程的意思是,在求以ai为末元素的最长递增子序列时,找到所有序号在L前面且小于ai的元素aj,即j<i且aj<ai。如果这样的元素存在,那么对所有aj,都有一个以aj为末元素的最长递增子序列的长度f(j),把其中最大的f(j)选出来,那么f(i)就等于最大的f(j)加上1,即以ai为末元素的最长递增子序列,等于以使f(j)最大的那个aj为末元素的递增子序列最末再加上ai;如果这样的元素不存在,那么ai自身构成一个长度为1的以ai为末元素的递增子序列。
这个算法由Java实现的代码如下:
public void lis(float[] L)
{
int n = L.length;
int[] f = new int[n];//用于存放f(i)值;
f[0]=1;//以第a1为末元素的最长递增子序列长度为1;
for(int i = 1;i<n;i++)//循环n-1次
{
f[i]=1;//f[i]的最小值为1;
for(int j=0;j<i;j++)//循环i 次
{
if(L[j]<L[i]&&f[j]>f[i]-1)
f[i]=f[j]+1;//更新f[i]的值。
}
}
System.out.println(f[n-1]);
}
这个算法有两层循环,外层循环次数为n-1次,内层循环次数为i次,算法的时间复杂度
所以T(n)=O(n2)。这个算法的最坏时间复杂度与第一种算法的阶是相同的。但这个算法没有排序的时间,所以时间复杂度要优于第一种算法。
四, 对第二种算法的改进
在第二种算法中,在计算每一个f(i)时,都要找出最大的f(j)(j<i)来,由于f(j)没有顺序,只能顺序查找满足aj<ai最大的f(j),如果能将让f(j)有序,就可以使用二分查找,这样算法的时间复杂度就可能降到O(nlogn)。于是想到用一个数组B来存储“子序列的”最大递增子序列的最末元素,即有
B[f(j)] = aj
在计算f(i)时,在数组B中用二分查找法找到满足j<i且B[f(j)]=aj<ai的最大的j,并将B[f[j]+1]置为ai。下面先写出代码,再证明算法的证明性。用Java实现的代码如下:
lis1(float[] L)
{
int n = L.length;
float[] B = new float[n+1];//数组B;
B[0]=-10000;//把B[0]设为最小,假设任何输入都大于-10000;
B[1]=L[0];//初始时,最大递增子序列长度为1的最末元素为a1
int Len = 1;//Len为当前最大递增子序列长度,初始化为1;
int p,r,m;//p,r,m分别为二分查找的上界,下界和中点;
for(int i = 1;i<n;i++)
{
p=0;r=Len;
while(p<=r)//二分查找最末元素小于ai+1的长度最大的最大递增子序列;
{
m = (p+r)/2;
if(B[m]<L[i]) p = m+1;
else r = m-1;
}
B[p] = L[i];//将长度为p的最大递增子序列的当前最末元素置为ai+1;
if(p>Len) Len++;//更新当前最大递增子序列长度;
}//此程序好像存在问题。
System.out.println(Len);
}
现在来证明这个算法为什么是正确的。要使算法正确只须证如下命题:
命题1:每一次循环结束数组B中元素总是按递增顺序排列的。
证明:用数学归纳法,对循环次数i进行归纳。
当i=0时,即程序还没进入循环时,命题显然成立。
设i<k时命题成立,当i=k时,假设存在j1<j2,B[j1]>B[j2],因为第i次循环之前数组B是递增的,因此第i次循环时B[j1]或B[j2]必有一个更新,假设B[j1]被更新为元素ai+1,由于ai+1=B[j1]> B[j2],按算法ai+1应更新B[j2]才对,因此产生矛盾;假设B[j2]被更新,设更新前的元素为s,更新后的元素为ai+1,则由算法可知第i次循环前有B[j2]=s< ai+1< B[j1],这与归纳假设矛盾。命题得证。
命题2:B[c]中存储的元素是当前所有最长递增子序列长度为c的序列中,最小的最末元素,即设当前循环次数为i,有B[c]={aj| f(k)=f(j)=c∧k,j≤i+1→aj≤ak}(f(i)为与第二种算法中的f(i)含义相同)。
证明:程序中每次用元素ai更新B[c]时(c=f(i)),设B[c]原来的值为s,则必有ai<s,不然ai就能接在s的后面形成长度为c+1的最长递增子序列,而更新B[c+1]而不是B[c]了。所有B[c]中存放的总是当前长度为c的最长递增子序列中,最小的最末元素。
命题3:设第i次循环后得到的p为p(i+1),那么p(i)为以元素ai为最末元素的最长递增子序列的长度。
证明:只须证p(i)等于第二种算法中的f(i)。显然一定有p(i)<=f(i)。假设p(i)<f(i),那么有两种情况,第一种情况是由二分查找法找到的p(i)不是数组B中能让ai接在后面成为新的最长递增子序列的最大的元素,由命题1和二分查找的方法可知,这是不可能的;第二种情况是能让ai接在后面形成长于p(i)的最长递增子序列的元素不在数组B中,由命题2可知,这是不可能的,因为B[c]中存放的是最末元素最小的长度为c的最长递增子序列的最末元素,若ai能接在长度为L(L> p(i))的最长递增子序列后面,就应该能接在B[L]后面,那么就应该有p(i)=L,与L> p(i)矛盾。因此一定有p(i)=f(i),命题得证。
算法的循环次数为n,每次循环二分查找用时logn,所以算法的时间复杂度为O(nlogn)。这个算法在第二种算法的基础上得到了较好的改进。
附:
例题:http://acm.fzu.edu.cn/problem.php?pid=1130
我的程序:
#include <stdio.h>
#include <memory.h>
int n,t;
int a[40005];
int b[40005];
int f[40005];
int m;//最长递增子序列的长度
int max(int x,int y)
{
return x>y?x:y;
}
int main()
{
scanf("%d",&n);
while(n--)
{
scanf("%d",&t);
for(int i=0;i<t;i++)
{
scanf("%d",&a[i]);
//b[i]=a[i];
}
memset(f,0,sizeof(f));
for(int k=0;k<=t;k++)
b[k]=t+3;
b[0]=-10;
f[0]=1;
b[1]=a[0];
m=f[0];
int p,r,q;
for(int j=1;j<t;j++)
{
if(b[m]<a[j])
{
f[j]=m+1;
b[f[j]]=a[j];
}
else
{
p=0;q=m;
r=(p+q)/2;
while(p<=q)
{
if(b[r]<a[j])
p=r+1;
else
q=r-1;
r=(p+q)/2;
}
f[j]=r+1;
if(b[f[j]]>a[j])
b[f[j]]=a[j];
}
if(m<f[j])
m=f[j];
}
printf("%d\n",m);
}
return 0;
}
分享到:
相关推荐
在本实验中,我们关注的是“最长递增子序列”(Longest Increasing Subsequence, LIS)这一经典问题,它是算法课程中的一个核心课题,尤其在动态规划的应用上有着重要的地位。中科大软件学院的这个实验旨在让学生...
最长递增子序列(Longest Increasing Subsequence, LCS)是计算机科学中一种经典的动态规划问题,常见于算法和数据结构的学习。在这个问题中,我们给定一个无序整数序列,目标是找到序列中的一个子序列,使得这个子...
【最长递增子序列(LIS)问题】 最长递增子序列(Longest Increasing Subsequence,简称LIS)是计算机科学中的一个重要问题,特别是在算法设计和分析中。它要求找到一个给定序列中的一个子序列,使得这个子序列是...
在算法领域,最长上升子序列(Longest Increasing Subsequence,LIS)问题是一个经典的问题,它在计算机科学中有着广泛的应用,例如在数据结构、排序算法以及动态规划等方面。本篇将详细介绍如何找出一个序列中长度...
- LCIS问题在寻找两个序列的最长递增子序列,要求这个子序列在两个序列中都是递增的。 - 解决方法结合LCS和LIS的思想,首先计算两个序列的LCS长度,然后分别计算两个序列的LIS,最后找到LCS与LIS的交集作为LCIS。 ...
对于编程竞赛来说,掌握这种基于动态规划的方法至关重要,因为它不仅适用于最长公共子序列问题,还可以广泛应用于其他问题,如最长公共子串、最长递增子序列等问题。 在少儿编程教育中,此问题同样具有较高的教育...
最长上升子序列(Longest Increasing Subsequence, 简称LIS)问题是寻找一个序列中最长的递增子序列。这里提到的“子序列”指的是原序列中的一些元素按原顺序抽取出来形成的序列,并不要求连续性。LIS问题不仅在算法...
##### 案例1:最长递增子序列问题 **问题描述**:给定一个整数序列,找出其中最长的递增子序列的长度。 **经典动态规划解法**: 设 `F[i]` 表示以第 `i` 个元素结尾的最长递增子序列的长度,则有状态转移方程: \...
在计算机科学中,数组是最长递增子序列(Longest Increasing Subsequence,LIS)问题是一个经典的动态规划问题。此问题旨在找出一个给定数组中,所有递增子序列中最长的那个。在C++中,我们可以采用两种不同的方法来...
动态规划问题,使用一个数组记录以每个元素结尾的最长递增子序列的长度。 以上问题涵盖了动态规划的基础概念、状态转移方程设计、优化技巧等多个方面,通过这些题目,程序员可以系统地提升动态规划能力,更好地应对...
在LeetCode的第300题“最长上升子序列”中,目标是找出给定无序整数数组中的最长上升子序列(Longest Increasing Subsequence, LIS)的长度。这是一道经典的动态规划(Dynamic Programming, DP)和二分查找(Binary ...
可以定义dp数组,dp[i]表示以序列第i个元素结尾的最长递增子序列的长度,通过比较当前元素与前面所有元素的关系来更新dp数组。 4. **最⻓公共⼦序列(LCS)**:LCS问题寻找两个序列中,最长的相同子序列,不考虑相对...
这个问题是关于寻找一个整数数组中的最长递增子序列(Longest Increasing Subsequence, ...理解和掌握动态规划、最长递增子序列的解法对提升编程竞赛水平至关重要。通过练习和实战,可以更好地掌握这类问题的解决技巧。
- **最长递增子序列**:找到一个序列的最长递增子序列,利用子问题的最优解性质。 - **矩阵链乘法**:寻找最小代价的矩阵乘积顺序。 5. **学习资源** - `DP.ppt` 可能是一个关于动态规划的PPT教程,涵盖了基本...
在LeetCode等在线编程平台上,动态规划被用于解决各种问题,如最长公共子序列、最长递增子序列、剪绳子、爬楼梯等。这些问题的关键在于识别问题的状态和状态转移方程,以及确定最优子结构,即解决问题的最优解可以...
例如,可能会讲解到背包问题、最长递增子序列、最小编辑距离等问题的动态规划解法,还可能涉及如何构造状态空间图、理解状态之间的关系以及如何写出正确且高效的状态转移方程等技巧。 动态规划的学习不仅对ACM竞赛...
对于这个问题,可以考虑使用单调队列来记录当前已知最长递增子序列的末尾元素及其长度。每当遍历到一个新的元素时,就根据其值来更新队列。通过这种方式,可以保证队列中存储的都是当前最长递增子序列的相关信息,...
最大上升子序列和是计算机科学和算法设计中的一个重要概念,特别是在解决动态规划问题时经常被用到。在信息学竞赛中,这样的问题尤为常见,帮助参赛者锻炼逻辑思维和问题解决能力。本话题将深入探讨最大上升子序列和...