编程之美: 第二章 数字之魅 2.16求数组中最长递增子序列

/*
求数组中最长递增子序列:(这里不要求相邻)
写一个时间复杂度尽可能低的程序,求一个一维数组(N个元素)中最长递增子序列的长度。
例如,在序列1,-1,2,-3,4,-5,6,-7中,最长递增子序列的长度为4(如1,2,4,6)

分析:
也就是找一个标号的序列,b[0],b[1],...,b[m] (0<= b[0] < b[1] <... <b[m]<N),使得array[ b[0] ] < array[ b[1] ] < ... < array[ b[m] ]

解法1:
将各阶段按照一定的次序排列好以后,对于某个给定的阶段状态来说,它以前各阶段的状态无法直接影响它未来的决策,只能间接地通过当前状态来影响。
换句话说,每个状态都是过去历史的一个完整总结。
我们以序列1,-1,2,-3,4,-5,6,-7为例,我们在找到4之后,并不关心4之前的两个值具体是怎样,因为它对找到6,没有直接影响。因此,这个问题满足无后效
性,可以使用动态规划来解决。

可以通过数字的规律来分析目标串:
1,-1,2,-3,4,-5,6,-7
使用i来表示当前遍历的位置:
当i=1时,显然,最长的递增序列为(1),序列长度为1
当i=2时,由于-1 < 1。因此,必须丢弃第一个值然后重新建立新串。当前的递增序列为(-1),长度为1
当i=3时,由于2>1,2>-1。因此最长的递增序列为(1,2),(-1,2),长度为2。2前面是1还是-1对求出后面的递增序列没有直接影响。
结论:
假设在目标数组array[]的前i个元素中,最长递增子序列的长度为LIS[i]。那么,
LIS[i+1] = max{1,LIS[k]+1},array[i+1] > array[k],for any k <= i
即如果array[i+1]>array[k],那么第i+1个元素可以接在LIS[k]长的子序列后面构成一个更长的子序列,与此同时,array[i+1]本身至少可以构成一个长度为1
的子序列。

解法2:
当考察第i+1个元素时,我们呢不考虑前面i个元素的分布情况。当考察第i+1的元素的时候考虑前面i个元素的情况。
对于前面i个元素的任何一个递增子序列,如果这个子序列的最大的元素比array[i+1]小,那么就可以将array[i+1]加在这个子序列之后,构成一个新的递增子序列。
比如当i=4的时候,目标序列为:1,-1,2,-3,4,-5,6,-7。最长递增序列为(1,2),(-1,2)。只要4>2,就可以把4直接增加到前面的子序列中形成一个新的递增子序列。
因此,我们希望找到前i个元素中的一个递增子序列,使得这个递增子序列的最大元素比array[i+1]小,且长度尽量长。这样将array[i+1]加在该递增子序列之后,便可以
找到array[i+1]为最大元素的最长递增子序列。
仍然假设在数组的前i个元素中,以array[i]为最大元素的递增子序列长度为LIS[i],同时假设:
长度为1的递增子序列最大元素的最小值为MaxV[1],长度为2的递增子序列最大元素的最小值为MaxV[2]
...
长度为LIS[i]的递增子序列最大元素的最小值为MaxV[LIS[i]]

解法3:
能否把递增序列中间的关系挖掘出来?分析一下临时存储下来的最长递增序列信息。
在递增序列中,如果i<j,那么就会有MaxV[i] < MaxV[j]。如果出现MaxV[j] < MaxV[i]的情况,则跟定义矛盾。
根据单调递增的关系,可以将穷举部分进行如下修改:
for(j = LIS[i-1] ; j >= 1 ; j--)  原来      for(j = iLisLen ; j >= 0 ; j--)
{                                           {
  if(array[i] > MaxV[j])                      if(array[i] > iMaxV[j])
  {                                           {
    LIS[i] = j + 1;                              LIS[i] = j + 1;
	break;                                       break;
  }                                           }
}                                           }

输入:
8
1 -1 2 -3 4 -5 6 -7
输出:
4
*/

/*
关键:
1 	for(int i = 0 ; i < iLen ; i++)
	{
		iLisArr[i] = 1;
		for(int j = 0 ; j < i ; j++)//从后向前寻找前面最长的序列
		{
			if(pArr[i] > pArr[j] && iLisArr[j] + 1 > iLisArr[i])//当前元素大于前面任何一个元素并且当前长度+1大于1,则更新最大长度
			{
				iLisArr[i] = iLisArr[j] + 1;//LIS[i] = max{1,LIS[j] + 1},array[i+1] > array[k],k<= i
2 	int iLisLen = 1;//数组最长递增子序列长度
	for(int i = 1 ; i < iLen ; i++)//穷举遍历,时间复杂度仍然为O(n^2)
	{
		int j;
		for(j = iLisLen ; j >= 0 ; j--)//遍历历史最长递增序列信息
		{
			if(pArr[i] > iMaxV[j])//如果当前元素比递增子序列中的最大元素还要大,说明我们应该更新最长递增子序列中的长度
			{
				iLisArr[i] = j + 1;
				break;//注意,找到后立即跳出
			}
		}
		if(iLisArr[i] > iLisLen)//如果当前序列大于最长递增序列长度,更新最长信息,并且将该元素添加到最长递增子序列中,作为最后面一个元素
		{
			iLisLen = iLisArr[i];
			iMaxV[ iLisArr[i] ] = pArr[i];
		}
		else if(iMaxV[j] < pArr[i] && pArr[i] < iMaxV[j+1])//如果当前元素大于前一个元素 并且 当前元素小于后一个元素。其实就是更新最大元素的最小值
		{
			iMaxV[ j + 1 ] = pArr[i];
3 upper_bound:当v存在时返回它出现的最后一个位置的后面的一个位置,如果不存在,返回这样一个下标i:在此处插入
v,原来A[i],A[i+1]...等元素全部后移一个位置后,仍然有序
4 		int j = upper_bound(0,iLisLen + 1,pArr[i],iMaxV);
		//由于返回的下标比它本身要大,把这里的查询部分改为二分搜索来降低时间复杂度,因为iMaxV刚好是一个有序数组。凡是有查询的地方均可以改成二分搜索
		iLisArr[i] = j;
		j--;//获取从后向前第一个小于当前元素的下标
*/

#include <stdio.h>
const int MAXSIZE = 10000;
const int INT_MAX = 0x7fffffff;
const int INT_MIN = -INT_MAX;
int LIS(int* pArr,int iLen)
{
	int iLisArr[MAXSIZE];
	for(int i = 0 ; i < iLen ; i++)
	{
		iLisArr[i] = 1;
		for(int j = 0 ; j < i ; j++)//从后向前寻找前面最长的序列
		{
			if(pArr[i] > pArr[j] && iLisArr[j] + 1 > iLisArr[i])//当前元素大于前面任何一个元素并且当前长度+1大于1,则更新最大长度
			{
				iLisArr[i] = iLisArr[j] + 1;//LIS[i] = max{1,LIS[j] + 1},array[i+1] > array[k],k<= i
			}
		}
	}
	int iMax = iLisArr[0];
	for(int k = 1 ; k < iLen ; k++)
	{
		if(iLisArr[k] > iMax)
		{
			iMax = iLisArr[k];
		}
	}
	return iMax;
}

int LIS_max(int* pArr,int iLen)
{
	int iMaxV[MAXSIZE];//记录数组中的递增序列信息
	iMaxV[0] = INT_MIN;//数组中的最小值,边界值
	iMaxV[1] = pArr[0];//数组中的第一个值,边界值
	int iLisArr[MAXSIZE];
	for(int i = 0 ; i < MAXSIZE ; i++)//初始化最长递增序列信息
	{
		iLisArr[i] = 1;
	}
	int iLisLen = 1;//数组最长递增子序列长度
	for(int i = 1 ; i < iLen ; i++)//穷举遍历,时间复杂度仍然为O(n^2)
	{
		int j;
		for(j = iLisLen ; j >= 0 ; j--)//遍历历史最长递增序列信息
		{
			if(pArr[i] > iMaxV[j])//如果当前元素比递增子序列中的最大元素还要大,说明我们应该更新最长递增子序列中的长度
			{
				iLisArr[i] = j + 1;
				break;//注意,找到后立即跳出
			}
		}
		if(iLisArr[i] > iLisLen)//如果当前序列大于最长递增序列长度,更新最长信息,并且将该元素添加到最长递增子序列中,作为最后面一个元素
		{
			iLisLen = iLisArr[i];
			iMaxV[ iLisArr[i] ] = pArr[i];
		}
		else if(iMaxV[j] < pArr[i] && pArr[i] < iMaxV[j+1])//如果当前元素大于前一个元素 并且 当前元素小于后一个元素。其实就是更新最大元素的最小值
		{
			iMaxV[ j + 1 ] = pArr[i];
		}
	}
	return iLisLen;
}

/*
upper_bound:当v存在时返回它出现的最后一个位置的后面的一个位置,如果不存在,返回这样一个下标i:在此处插入
v,原来A[i],A[i+1]...等元素全部后移一个位置后,仍然有序
upper_bound上界:如果iVal > iArr[mid],则mid不可能,应该是将下标往后找low = mid + 1
                    iVal < iArr[mid],则mid可能,下标往前high = mid
					iVal = iArr[mid],则mid不可能,下标往后找 low = mid + 1
					总体上是: iVal >= iArr[mid],low = mid + 1
					          iVal < iArr[mid],high = mid
*/
int upper_bound(int low,int high,int iVal,int* pArr)//这里采用寻找上界值,找到的下标返回之后用于LIS[i]长度的更新
{
	int mid;
	while(low < high)
	{
		mid = low + (high - low)/2;
		if(iVal >= pArr[mid])
		{
			low = mid + 1;
		}
		else
		{
			high = mid;
		}
	}
	return low;
}

int LIS_binarySearch(int* pArr,int iLen)
{
	int iMaxV[MAXSIZE];
	iMaxV[0] = INT_MIN;
	iMaxV[1] = pArr[0];
	int iLisArr[MAXSIZE];
	for(int i = 0 ; i < MAXSIZE; i++)
	{
		iLisArr[i] = 1;
	}
	int iLisLen = 1;
	for(int i = 1 ; i < iLen ; i++)
	{
		int j = upper_bound(0,iLisLen + 1,pArr[i],iMaxV);
		//由于返回的下标比它本身要大,把这里的查询部分改为二分搜索来降低时间复杂度,因为iMaxV刚好是一个有序数组。凡是有查询的地方均可以改成二分搜索
		iLisArr[i] = j;
		j--;//获取从后向前第一个小于当前元素的下标
		if(iLisArr[i] > iLisLen)
		{
			iLisLen = iLisArr[i];
			iMaxV[ iLisArr[i] ] = pArr[i];
		}
		else if(iMaxV[j] < pArr[i] && pArr[i] < iMaxV[j+1])
		{
			iMaxV[j+1] = pArr[i];
		}
	}
	return iLisLen;
}

void process()
{
	int n;
	while(EOF != scanf("%d",&n))
	{
		int iArr[MAXSIZE];
		for(int i = 0 ; i < n ; i++)
		{
			scanf("%d",&iArr[i]);
		}
		printf("%d\n",LIS_binarySearch(iArr,n));
	}
}

int main(int argc,char* argv[])
{
	process();
	getchar();
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值