首页 > 算法 数据结构 > 最长递增子序列(LIS)

最长递增子序列(LIS)

2012年6月17日 发表评论 阅读评论
文章作者:Yx.Ac   文章来源:勇幸|Thinking (http://www.ahathinking.com)   转载请注明,谢谢合作。

---

最长递增子序列又叫做最长上升子序列;子序列,正如LCS一样,元素不一定要求连续。本节讨论实现三种常见方法,主要是练手。

题:求一个一维数组arr[i]中的最长递增子序列的长度,如在序列1,-1,2,-3,4,-5,6,-7中,最长递增子序列长度为4,可以是1,2,4,6,也可以是-1,2,4,6。

方法一:DP

像LCS一样,从后向前分析,很容易想到,第i个元素之前的最长递增子序列的长度要么是1(单独成一个序列),要么就是第i-1个元素之前的最长递增子序列加1,可以有状态方程:

LIS[i] = max{1,LIS[k]+1},其中,对于任意的k<=i-1,arr[i] > arr[k],这样arr[i]才能在arr[k]的基础上构成一个新的递增子序列。

代码如下:在计算好LIS长度之后,output函数递归输出其中的一个最长递增子序列。

#include <iostream>
using namespace std;

/* 最长递增子序列 LIS
 * 设数组长度不超过 30
 * DP
*/

int dp[31]; /* dp[i]记录到[0,i]数组的LIS */
int lis;    /* LIS 长度 */

int LIS(int * arr, int size)
{
	for(int i = 0; i < size; ++i)
	{
		dp[i] = 1;
		for(int j = 0; j < i; ++j)
		{
			if(arr[i] > arr[j] && dp[i] < dp[j] + 1)
			{
				dp[i] = dp[j] + 1;
				if(dp[i] > lis)
				{
					lis = dp[i];
				}
			}
		}
	}
	return lis;
}

/* 输出LIS */
void outputLIS(int * arr, int index)
{
	bool isLIS = 0;
	if(index < 0 || lis == 0)
	{
		return;
	}
	if(dp[index] == lis)
	{
		--lis;
		isLIS = 1;
	}

	outputLIS(arr,--index);

	if(isLIS)
	{
		printf("%d ",arr[index+1]);
	}
}

void main()
{
	int arr[] = {1,-1,2,-3,4,-5,6,-7};

	/* 输出LIS长度; sizeof 计算数组长度 */
	printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));

	/* 输出LIS */
	outputLIS(arr,sizeof(arr)/sizeof(int) - 1);
	printf("\n");
}

这个方法也最容易想到也是最传统的解决方案,对于该方法和LIS,有以下两点说明:

  1. 由LIS可以衍生出来最长非递减子序列,最长递减子序列,道理是一样的。
  2. 对于输出序列,也是可以再申请一数组pre[i]记录子序列中array[i]的前驱,道理跟本节的实现也是一样的

方法二:排序+LCS

这个方法是在Felix’blog(见参考资料)中看到的,因为简单,他在博文中只是提了一句,不过为了练手,虽然懒,还是硬着头皮写一遍吧,正好再写一遍快排,用quicksort + LCS,这个思路还是很巧妙的,因为LIS是单调递增的性质,所以任意一个LIS一定跟排序后的序列有LCS,并且就是LIS本身。代码如下:

#include <iostream>
using namespace std;

/* 最长递增子序列 LIS
 * 设数组长度不超过 30
 * quicksort + LCS
*/

void swap(int * arr, int i, int j)
{
	int tmp = arr[i];
	arr[i] = arr[j];
	arr[j] = tmp;
}

void qsort(int * arr, int left, int right)
{
	if(left >= right)	return ;
	int index = left;
	for(int i = left+1; i <= right; ++i)
	{
		if(arr[i] < arr[left])
		{
			swap(arr,++index,i);
		}
	}
	swap(arr,index,left);
	qsort(arr,left,index-1);
	qsort(arr,index+1,right);
}

int dp[31][31];

int LCS(int * arr, int * arrcopy, int len)
{
	for(int i = 1; i <= len; ++i)
	{
		for(int j = 1; j <= len; ++j)
		{
			if(arr[i-1] == arrcopy[j-1])
			{
				dp[i][j] = dp[i-1][j-1] + 1;
			}else if(dp[i-1][j] > dp[i][j-1])
			{
				dp[i][j] = dp[i-1][j];
			}else
			{
				dp[i][j] = dp[i][j-1];
			}
		}
	}
	return dp[len][len];
}

void main()
{
	int arr[] = {1,-1,2,-3,4,-5,6,-7};
	int arrcopy [sizeof(arr)/sizeof(int)];

	memcpy(arrcopy,arr,sizeof(arr));
	qsort(arrcopy,0,sizeof(arr)/sizeof(int)-1);

	/* 计算LCS,即LIS长度 */
	int len = sizeof(arr)/sizeof(int);
	printf("%d\n",LCS(arr,arrcopy,len));
}

方法三:DP+二分查找

编程之美》对于这个方法有提到,不过它的讲解我看得比较难受,好长时间才明白,涉及到的数组也比较多,除了源数据数组,有LIS[i]和MaxV[LIS[i]],后来看了大牛Felix的讲解,我才忽然发现编程之美中的这个数组MaxV[LIS[i]]在记录信息上其实是饶了弯的,因为我们在寻找某一长度子序列所对应的最大元素最小值时,完全没必要通过LIS[i]去定位,即没必要与数据arr[i]挂钩,直接将MaxV[i]的下标作为LIS的长度,来记录最小值就可以了(表达能力太次,囧。。。),一句话,就是不需要LIS[i]这个数组了,只用MaxV[i]即可达到效果,而且原理容易理解,代码表达也比较直观、简单。

下面说说原理:

目的:我们期望在前i个元素中的所有长度为len的递增子序列中找到这样一个序列,它的最大元素比arr[i+1]小,而且长度要尽量的长,如此,我们只需记录len长度的递增子序列中最大元素的最小值就能使得将来的递增子序列尽量地长。

方法:维护一个数组MaxV[i],记录长度为i的递增子序列中最大元素的最小值,并对于数组中的每个元素考察其是哪个子序列的最大元素,二分更新MaxV数组,最终i的值便是最长递增子序列的长度。这个方法真是太巧妙了,妙不可言。

代码如下:

#include <iostream>
using namespace std;

/* 最长递增子序列 LIS
 * 设数组长度不超过 30
 * DP + BinarySearch
*/

int MaxV[30]; /* 存储长度i+1(len)的子序列最大元素的最小值 */
int len;      /* 存储子序列的最大长度 即MaxV当前的下标*/

/* 返回MaxV[i]中刚刚大于x的那个元素的下标 */
int BinSearch(int * MaxV, int size, int x)
{
	int left = 0, right = size-1;
	while(left <= right)
	{
		int mid = (left + right) / 2;
		if(MaxV[mid] <= x)
		{
			left = mid + 1;
		}else
		{
			right = mid - 1;
		}
	}
	return left;
}

int LIS(int * arr, int size)
{
	MaxV[0] = arr[0]; /* 初始化 */
	len = 1;
	for(int i = 1; i < size; ++i) /* 寻找arr[i]属于哪个长度LIS的最大元素 */
	{
		if(arr[i] > MaxV[len-1]) /* 大于最大的自然无需查找,否则二分查其位置 */
		{
			MaxV[len++] = arr[i];
		}else
		{
			int pos = BinSearch(MaxV,len,arr[i]);
			MaxV[pos] = arr[i];
		}
	}
	return len;
}

void main()
{
	int arr[] = {1,-1,2,-3,4,-5,6,-7};

	/* 计算LIS长度 */
	printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));
}

这个方法的实现巧妙而直观,让人有种“啊,原来还可以这样”的感慨,感谢Felix

本文相关代码可以到这里下载。

(全文完)

参考资料:

编程之美》 2.16

Felix’s Blog:最长递增子序列 O(NlogN)算法

勘误:

第三种方法细节有错误,见下面评论,感谢@冰上游鱼

  • leisurelyrcxf

    这个代码有很多错误

    • Yx.Ac

      比如呢?呵呵

  • avatar

    如果不是求长度,而是输出最长递增子序列,可能就行不通了

  • http://weibo.com/csdbreadstealer 诚实嘚偷包贼

    我忍不住又进行评论了,因为看到了方法三,太聪明了!!!!

  • rensq

    第一种DP方法的打印也是有缺陷的,不能仅仅判断长度相等就打印出结果。还要看当前元素是否比刚才选择的元素要小。

    • Yx.Ac

      有反例么?:-)

      • http://zhangruichang.com Ruichang Zhang

        我觉得这里确实需要判断的,因为他可能是由另一个dp[j]构造出的最优解,不然可能打印出错误的解

  • rensq

    方法三中最后的序列好像没法得到了,只得到了最后的长度

    • Yx.Ac

      这个就是求长度

  • Moon

    Paper看不下去了呀。。。@Yx.Ac

  • Moon

    方法三也是算法导论15.4-6的题目,”记录长度为i的递增子序列中最后一个元素的最小值“我也想到啦,就是不明白复杂度为啥是O(nlog n),原来是二分查找~

    • Yx.Ac

      @Moon 不好好读paper,看什么算法导论,哼哼

  • ak

    这个程序怎么感觉有点问题呢??
    当序列是5 5 5 6时候 答案怎么成2了??

    • Yx.Ac

      @ak 就是2,最长递增,不是最长非递减

  • Yx.Ac

    哈,假期还在A题啊,赞一个,:-), 的确如你所说,加等号就成了求最长非递减子序列了,细节很重要,嘿,谢谢你!@冰上游鱼

  • http://mazheng.org 冰上游鱼

    呵呵 学长,我来“找事儿”来了。
    这个博客里的 方法三:DP+二分查找 中二分查找的代码不对!
    /* 返回MaxV[i]中刚刚大于x的那个元素的下标 */
    int BinSearch(int * MaxV, int size, int x)
    {
    int left = 0, right = size-1;
    while(left <= right)
    {
    int mid = (left + right) / 2;
    if(MaxV[mid] <= x)
    {
    left = mid + 1;
    }else
    {
    right = mid - 1;
    }
    }
    return left;
    }
    应该改为:
    /* 返回MaxV[i]中 不小于x 的那个元素的下标 */ (不是刚刚大于)
    int BinSearch(int * MaxV, int size, int x)
    {
    int left = 0, right = size-1;
    while(left <= right)
    {
    int mid = (left + right) / 2;
    if(MaxV[mid] < x) //没有等号
    {
    left = mid + 1;
    }else
    {
    right = mid - 1;
    }
    }
    return left;
    }

    比如测试用例:
    给定数列为{5 2 1 4 5 4 5 3 6} ,则原来的代码求出来的是5,而正确的答案应该是4.

    下午在做poj 3903,一直错,做了一下午,直到刚才才发现原来是这儿的错误。

    如果是求最长非递归子序列的话,原代码就对了。
    请学长明鉴。