动态规划--最长单调子序列问题

1.问题描述:

求一个正整数序列的最长单调自增子序列,子序列不要求是连续的。例如

Input:5

5 2 4 3 1

Output:2

 

2. 算法复杂度是O(N*N)

确定状态转移方程,设f[i]是以a[i]为结尾的最大值的子序列的长度,那么\[\max \{ f[i]\} \]的最大值就是要的结果。

所以转移方程为:

\[f(i) = \max \{ f(x)|x < i,{a_i} > {a_x}\}  + 1\]

所以代码可以为:

void main(void)
{
	int arr[] = {10,4,20,10,15,13};
	int dist[6];
	int path[6];
	int num = 6;
	int i = 0;

	for(i = 0; i < 6; i++)
	{
		dist[i] = 0;
		//-1表示前面没有元素了,用于回头求解的时候定界
		path[i] = -1;
	}

	for(i = 0; i < num; i++)
	{
		int temp = 0;
		int index = -1;

		for(int j = i - 1; j >= 0; j--)
		{
			if(arr[i] > arr[j] && dist[j] > temp)
			{
				temp = dist[j];
				index = j;
			}//if
		}//for

		dist[i] = temp + 1;
		path[i] = index;

	}//for

	//找到最大的那个值
	int max = 0;
	int maxIndex = -1;
	for(int m = 0; m < num; m++)
	{
		if(dist[m] > max)
		{
			max = dist[m];
			maxIndex = m;
		}
	}//for
	printf("最长单曾子序列的长度是%d.\n", max);

	while(path[maxIndex] != -1)
	{
		printf("%d->", arr[maxIndex]);
		maxIndex = path[maxIndex];
	}//while
	printf("%d\n", arr[maxIndex]);

}

  很显然时间复杂度是O(N*N),那么有没有更快的算法呢?按照正常的思路更快的复杂度应该就是O(N*logN),那么就要涉及到二分了。

 

3. 算法复杂度是O(N*logN)

建立一个辅助数组c[n],c[i]=j存储的是子序列长度为i的序列最后一个值j(实际上子序列长度为i的子序列有多个,要的是子序列最后一个值最小的)。

这时要遍历要处理的数组arr[n]。

for(i=0;i<n;i++)
{
  j=find(c,n+1,arr[i]);//find是一个二分查找
  c[j]=arr[i];
  dist[i]=j;
}

请看一下上面的例子实际执行的情况:C数组变化的情况

-1 5

-1 2

-1 2 4

-1 1 2

-1 1 4

-1 1 3

arr数组遍历是从前往后的,处理arr[i-1]时arr[i]以及后面的值肯定还没有处理,前面的值都处理过了,看c数组,每个arr数组中的值和c数组中值进行比较,找到合适的位置插入(若插入到c数组的末尾,那么就属于最长递增子序列长度加1,实际上c数组的长度就是最后的最长单调递增子序列的长度。),否则这就替换掉了c数组中原来位置存储的值,这种替换是有意义的,主要是为了后来的arr数组中的值计算dist用(dist[i]中保存的是以arr[i]为最后一个元素的最长单调递增子序列。)好处是若arr[i] <arr[j],dist[i]=dist[j],那么在c中肯定要保存arr[i]呀!!(注意c数组的下标代表的是子序列的长度,c数组中的值也是按递增顺序排列的。这才可能用二分呢,亲)。和O(N*N)的主要区别就是巧妙的借用了c数组,本题的关键就是理解c数组的意义。可以手动模拟一下算法执行的步骤,重要模拟c和b数组的变化情况。c数组是以-1位开头的有序的递增数列。

 

下面给出完整的算法

 

#include <stdio.h>

#define MAX 100
void fill(int a[], int len);
int find(int a[], int cLastIndex, int x);

int main()
{
	int arr[MAX] = {0};
	int dist[MAX] = {0};
	int c[MAX];
	int num;	//原始数字序列的长度

	int i = 0;

	freopen("in.txt", "r", stdin);
	freopen("out.txt", "w", stdout);

	printf("读取数字序列长度...\n");
	scanf("%d", &num);
	printf("读取数字序列...\n");
	for(i = 0; i < num; i++)
	{
		scanf("%d", &arr[i]);
	}//for

	//初始化c数组,长度是arr数组+1
	fill(c, num + 1);

	c[0] = -1;	//使得递增数列的最小值为-1

	
	for(i = 0; i < num; i++)
	{
		int j = find(c, i + 1, arr[i]);
		c[j] = arr[i];
		dist[i] = j;
	}//for

	//c数组中保存的就是结果的数字序列,打印一下
	i = 1;
	while(c[i] != 100)
	{
		printf("%d ", c[i]);
		i++;
	}


	fclose(stdin);
	fclose(stdout);
	return 0;

}

void fill(int a[], int len)
{
	for(int i = 0; i < len; i++)
	{
		a[i] = 100;	////这就是一个初始化,无所谓,目的就是是这个数列递增
	}
}

//在数组a中,寻找数字x
//如果存在返回其下表,如果不存在返回其该插入的位置,会覆盖掉原来在该位置的数字
int find(int a[], int cLastIndex, int x)
{
	int left = 0;
	int right = cLastIndex;
	int mid = (left + right) / 2;

	while(left <= right)
	{
		if(x > a[mid])
		{
			left = mid + 1;
		}
		else if(x < a[mid])
		{
			right = mid - 1;
		}
		else
		{
			return mid;
		}

		mid = (left + right) / 2;
	}//while

	return left;
}//find()

  通过上面的代码可以看到,其实dist数组根本没有用到,只用c数组就能求解问题,其实这种算法已经不是动态规划了,虽然它的时间复杂度比动态规划的要好。这个算法的根本思想就是想到一个c数组,这个有序递增的数组用来存储最长的子序列。确实是一种很不错的手法。

测试数据:

6
10 4 20 10 15 13

运行结果:

读取数字序列长度...
读取数字序列...
4 10 13 

 

posted @ 2015-06-24 09:43  stemon  阅读(1226)  评论(0编辑  收藏  举报