动态规划求最长递增子序列(longest increasing subsequence)

1,什么是动态规划?

在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果。当然,各个阶段决策的选取不是任意确定的,它依赖于当前面临的状态,又影响以后的发展,当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线,这种把一个问题看作是一个前后关联具有链状结构的多阶段过程就称为多阶段决策过程,这种问题就称为多阶段决策问题。多阶段的决策问题,就是要在所有可能采取的策略中选取一个最优的策略,以便得到最佳的效果。动态规划是一种求解多阶段决策问题的系统技术!

一般来说,只要问题可以划分成规模更小的子问题,并且原问题的最优解中包含了子问题的最优解,则可以考虑用动态规划解决。

动态规划与分治的异同?

相同点:把一个问题分割成子问题,通过组合子问题的解而求得原问题的解。

不同点:分治的子问题不重叠,而动态规划的子问题重叠。

动态规划与贪心的异同?

相同点:所求问题都具有最优子结构。

不同点:贪心自顶向下使用最优子结构 ,先做选择,在当时看起来是最优的选择,最终求得一个最优解。

                动态规划自底向上,先求解子问题的最优解,再做选择,递归求问题的最优解。

2,什么是最长递增子序列?

比如数组:5,6,7,0,10的最长递增子序列就是6,6,7,10它是可以不连续的。

3,怎么利用动态规划?

想当然:要求A[0,,,i]的最长递增子序列,则先要求A[0,,,j-1]的最长递增子序列,然后再判断A[0,,,j-1]的最长递增子序列的最大元素是否大于A[i],然后再判断使用!

这样是不行的:想想数组5,6,7,0,1,2,3

正确想法:对于动态规划问题,往往存在递推解决方法,这个问题也不例外。要求长度为i的序列的Ai{a1,a2,……,ai}最长递增子序列,需要先求出序列Ai-1{a1,a2,……,ai-1}中以各元素(a1,a2,……,ai-1)作为最大元素的最长递增序列,然后把所有这些递增序列与ai比较。

基本方法:时间代价为O(n2);

#include "stdafx.h"
#include <iostream>
#include <ctime>
#include <Windows.h>
using namespace std;
//基本方法O(n2)
void LongestISA(int *a, int length)
{
	int *tail = new int[length];//tail[j]表示以j为尾的LISA的长度
	int *pre = new int[length];//pre[i]表示以i元素为最大元素的LISA的前驱元素
	int max = 1;//表示最长长度
	int k;//表示LISA的末尾元素的位置
	int i;
	for (i = 0; i < length; i++)
	{
		tail[i] = 1;
		pre[i] = i;
	}
	for (i = 1; i < length; i++)
	{
		for (int j = 0; j < i; j++)
		{
			if (a[j] < a[i] && tail[j] + 1 > tail[i])//比如7,8,9,0,10这个数组,
			{                               //若不加第二个条件,求tail[4]会是什么样?
				tail[i] = tail[j] + 1;
				pre[i] = j;
				if (max < tail[i])
				{
					max = tail[i];
					k = i;
				}
			}
		}
	}
	cout << "最长递增子序列长度为:" << max << endl;
	//int *Result = new int[max];
	//i = max - 1;
	//while (pre[k] != k)
	//{
	//	Result[i--] = a[k];
	//	k = pre[k];
	//}
	//Result[i] = a[k];
	//cout << "最长递增子序列为:" << endl;
	//for (i = 0; i < max; i++)
	//{
	//	cout << Result[i] << ' ';
	//}
	delete[] pre;
	delete[] tail;
	//delete[] Result;
}

改进方法:我们会发现下面一个特性:

用min_tail[i]表示长度为i+1的递增序列中的最大元素最小的序列末尾元素的位置,

假设当前输入数组用A表示,则有A[min_tail[0]] < A[min_tail[1]] < .... < A[min_tail[max - 1]]这一性质,

为什么呢?

因为长度为i的递增序列中肯定有长度为i-1的递增序列,而min_tail[i-1]表示的是长度为i-1的递增序列中最大元素最小的那个的位置,所以这个位置所在的数肯定要小于或等于长度为i的递增序列中的那个长度为i-1的递增序列的最大元素,即肯定要小于长度为i的递增序列中的最大元素。

有了这个特性,我们只要求出所有的min_tail元素,就得到了最长递增子序列。

怎么利用这一性质呢?

比如当前求到的最大长度用max表示,已经求出的min_tail有min_tail[0],,,,min_tail[max - 1];

现在来了一个数组元素A[i],我们先用二分查找在A[min_tail[0]]到A[min_tail[max - 1]]中找到A[i]所在位置,

分几种情况:

若A[i]大于当前的A[min_tail[max - 1]]表示递增序列的长度增加了,变成了max+1,新的min_tail[max - 1]应当为i;

若A[i]在中间位置,比如A[min_tail[j]] < A[i] < A[min_tail[j+1]]则表示存在一个长度为j+1的递增序列,且它的最大元素要比当前的A[min_tail[j+1]]小,所以要更新min_tail[j+1]为i;

若A[i]小于A[min_tail[0]],则更新A[min_tail[0]];

若找到有等于A[i]的,则不做处理,因为求得是递增序列。

具体代码如下:

//下边是改进的算法O(nlogn)
int Bsearch(int *a, int *b, int low, int high, int key)
{
	int i = low, j = high;
	while (i < j)
	{
		int mid = (i + j)/2;
		if (key > a[b[mid]])
		{
			i = mid + 1;
		}
		else
			if (key < a[b[mid]])
			{
				j = mid - 1;
			}
			else
			{
				return -1;//表示相等
			}
	}
	if (a[b[i]] == key)
	{
		return -1;
	}
	else
	{
		return i;
	}
}
//关键就是求min_tail数组
void LISA(int *a, int length)
{
	int *min_tail = new int[length];//min_tail[i]表示长度为i+1的递增序列中末尾值最小的
	                                //递增序列末尾元素的位置
	int *pre = new int[length];//pre[i]表示i元素所在递增序列中的前驱元素
	int max = 1;
	min_tail[0] = 0;
	int i;
	for (i = 0; i < length; i++)
	{
		pre[i] = i;
	}
	for (i = 1; i < length; i++)
	{
		if (max == 1)
		{
			if (a[i] < a[min_tail[0]])
			{
				min_tail[0] = i;
			}
			else
				if (a[i] > a[min_tail[0]])
				{
					min_tail[1] = i;
					max++;
					pre[i] = min_tail[0];
				}
		}
		else
		{
			int Result = Bsearch(a, min_tail, 0, max - 1, a[i]);//传入的参数是数组下标
			//比较的是a[min_tail[下标]],因为a[min_tail[i]]是严格有序数组,返回的是下标
			if (Result == 0)//返回情况分四种情况
			{
				if (a[min_tail[Result]] > a[i])
				{
					min_tail[0] = i;
				}
				else
				{
					min_tail[1] = i;
					pre[i] = min_tail[0];
				}
			}
			else
				if (Result == max - 1)
				{
					if (a[min_tail[Result]] > a[i])
					{
						min_tail[Result] = i;
						//pre[i] = min_tail[Result - 1];//注意前驱元素位置是这个
					}
					else
					{
						pre[i] = min_tail[max - 1];
						max++;
						min_tail[max - 1] = i;
					}
				}
				else
					if (Result == -1)
					{
					}
					else
						if (a[min_tail[Result]] < a[i])
						{
							min_tail[Result + 1] = i;
							pre[i] = min_tail[Result];
						}
						else
						{
							min_tail[Result] = i;
							pre[i] = min_tail[Result - 1];
						}
		}
	}
	cout << "最长递增子序列长度为:" << max << endl;
	//int *Result = new int[max];
	//i = max - 1;
	//int j = max - 1;
	//int k = min_tail[j];
	//while (pre[k] != k)
	//{
	//	Result[i--] = a[k];
	//	k = pre[k];
	//}
	//Result[i] = a[k];
	//cout << "最长递增子序列为:" << endl;
	//for (i = 0; i < max; i++)
	//{
	//	cout << Result[i] << ' ';
	//}
	delete[] pre;
	delete[] min_tail;
	//delete[] Result;
}
int _tmain(int argc, _TCHAR* argv[])
{
	//int a[] = {35,36,39,3,15,27,6,42};
	cout << "输入数组规模!" << endl;
	int num;
	cin >> num;
	int *a = new int[num];
	srand((int)time(0));
	for (int i = 0; i < num; i++)
	{
		a[i] = rand()%5001;
		//cout << a[i] << ' ';
	}
	cout << "基本方法结果为:" << endl;
	double start1 = GetTickCount();
	LongestISA(a, num);
	double end1 = GetTickCount();
	cout << "时间为:" << end1 - start1 << "毫秒!" << endl;
	cout << "改进方法结果为:" << endl;
	double start2 = GetTickCount();
	LISA(a, num);
	double end2 = GetTickCount();
	cout << "时间为:" << end2 - start2 << "毫秒!" << endl;
	system("pause");
	return 0;
}
100000个随机数两个方法的比较结果为:

还有一些可用动态规划解决的为题:

比如:装配线调度问题:即怎么是机车最快出去;

有了这个递归解,就可以自底向上一步一步求解,知道最终求出机车所走路线。

还有就是矩阵链乘法问题和求最长公共子序列问题。
 

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值