时间复杂度O(nlgn)求解最长递增子序列问题

#include <iostream>
using namespace std;
const int maxn = 100;
int min_end[maxn]; //min_end[i]表示长度为i的子串的最小末端
int pre[maxn];//pre[i]表示第i个结点的前驱结点的索引
int index[maxn];  //当前找到的长度为i的递增子序列的最小末尾的索引
int search(int x, int low, int high)
{
	//循环不变式保证每次循环后都有low左边小于等于x,high右边大于x
	while (low <= high)
	{
		int mid = (low + high) / 2;
		if (min_end[mid] > x)
			high = mid - 1;
		else if (min_end[mid] <= x)
			low = mid + 1;
	}
	return low;
}
void LIS(int A[], int n)
{
	int k = 0;   //min_end[]数组的长度,数组从1开始, min_end[k]是末尾元素
	//对于min_end,index数组,下表是1到k有意义
	//对于pre数组,下标是0到n-1有意义
	min_end[0] = -999999;
	index[0] = -1;  //-1是结束标志
	pre[0] = -1;
	for (int i = 0; i < n; i++)
	{
		int pos = search(A[i], 1, k);
		min_end[pos] = A[i];
		index[pos] = i;
		pre[i] = index[pos - 1];
		if (pos > k) k++;
	}
	//打印最长递增子序列
	int result[maxn];
	int result_n = 0; //result数组的长度
	for (int i = index[k]; i != -1; i = pre[i])
		result[result_n++] = A[i];
	for (int i = result_n-1; i >= 0; i--)
		cout << result[i] << " ";
	cout << endl;
}

int main()
{
	while (1)
	{
		cout << "输入数组大小:" << endl;
		int n;
		cin >> n;
		cout << "输入数组:" << endl;
		int A[maxn];
		for (int i = 0; i < n; i++)
			cin >> A[i];
		LIS(A, n);
	}
	return 0;
}

      代码如上,输入数组A[], 打印出任意一个最长递增子序列。

      第一部分:先计算该最长子序列长度,用到一个辅助数组min_end[], min_end[i]表示长度为i的子序列的最小末尾。通过遍历数组A,更新数组min_end, 数组min_end的最后的长度就是最长递增子序列的长度了,下面会证明。(注意这里数组A的元素是从0

开始,而数组min_end从索引1开始才有意义,表示长度为1的子序列的最小末尾。所以初始化的时候设置min_end[0]= -99999, 一个很大的负数。

     在这一部分中,主要有以下几步:

          1)遍历数组A,当数组A遍历完了下标i-1,开始进行i次遍历时候,假设数组min_end长度为k。也就是说,前i次对数组A的遍历,已经找到了当前长度为x的子序列的最小末尾( 1 <= x <= i)

             2)通过调用search(A[i], 1, k)函数二分查找插入点,这个插入点pos很讲究,是min_end数组中第一个大于A[i]的数组值的索引,然后用A[i]代替该值。如果数组min_end所有的值都小于A[i],则将A[i]加入到数组末端。

     通过步骤1) 和 2), min_end数组的长度就表示数组中的最长递增子序列。

     下面用循环不变式来证明, 构造循环不变式如下:

      在LIS的for循环i+1迭代之前有(i的值是任意的):

扫描二维码关注公众号,回复: 6157445 查看本文章

           1.  设此时数组min_end的长度为k,则函数已经找到了序列A[0 ... i]中长度为x的递增子序列的最小末尾(1 <= x <= k)

           2. 序列A[1 ... i] 中不存在长度大于k的递增子序列。

           3. 数组min_end的元素是递增的。

     初始时:在i等于1之前,for循环已经循环一次,A[0] 赋值给 min_end[1], 数组min_end的长度k等于1。 序列A[0]只有一个元素,所以min_end[1]就是序列A[0]的长度为1的最小末尾,循环不变式1满足。并且数组A当前长度是1,自然不存在大于1的子序列,循环不变式2也满足,一个元素肯定是递增,循环不变式3也满足。

     保持:设第i+1次迭代之前循环不变式1和2均满足,现执行第i+1次迭代。

                分两种情况讨论,第一种,若A[i+1]大于等于数组min_end中的所有元素,则将A[i+1]插入到min_end[k+1], min_end长度变成k+1。

                1)第一,因为A[i+1]大于min_end[1 ... k]中所有的元素,所以在序列A[0 ... i]中,长度小于k+1的递增子序列的最小末尾不会更新。也就是说哪怕将A[i+1]考虑进来,他们的最小末尾还是原来的。第二,因为min_end[k] < A[i+1], 所以可以在序列A[0 ... i]中找到一个长度为k的递增子序列,并且序列的末尾小于A[i+1], 现在把A[i+1]加入到这个序列的末尾,就构成了一个长度为k+1的递增子序列。又由循环不变式2可知序列A[0 ... i ]中不存在长度为k+1的序列,所以A[i+1]就是序列A[0 ... i+1]中长度为k+1的递增子序列的最小末尾,所以经过第i+1次迭代后, 循环不变式1成立。

                  2)第i+1次迭代后,假设A[0 ... i+1 ]存在长度大于k+1的递增子序列,则A[0 .. i]中存在长度大于k的递增子序列,这与迭代前的循环不变式2矛盾,所以假设不成立。即i+1次迭代后,循环不变式2仍然保持。

                  3) 因为A[i+1]大于min_end[1 ... k]中所有的元素且A[i+1]插入到min_end[1 ... k]的末尾,所以循环不变式3仍然保持。

                  第二种情况, 通过二分查找在数组min_end[1 ... k]中找到一个位置pos,min_end[pos]是第一个大于A[i+1]的元素,用A[i+1]替换掉原来的min_end[pos] . 

                 1) 此时min_end[1 ... k]数组的长度还是k,由于A[i+1] >= min_end[pos-1], 所以在A[1 ... i]中可以找到一个长度为pos-1的递增子序列,且末尾小于等于A[i+1], 那么将A[i+1]加入到该子序列的末尾,构成了一个长度为pos的递增子序列。并且由于A[i+1] < min_end[pos], 所以在原来的序列A[0 ... i]中的长度为pos的递增子序列长度的最小末尾大于A[i+1], 现在将A[i+1]加入进来后构成了一个长度为pos的新的递增子序列,且末尾比原来的小,所以被替代后的min_end[pos]是序列A[0 ... i+1]的长度为pos的递增子序列的最小末尾。在序列A[0 ... i+1]中,长度小于pos的递增子序列的最小末尾本来就小于A[i+1]了,所以不会更新,仍然是最小末尾,所以当x<pos时, min_end[x]还是长度为x的子序列的最小末尾。当x>pos时,假设第i+1次迭代后,序列A[0 ... i+1]中存在一个长度为x的递增子序列的最小末尾为A[i+1] , 那么也将存在一个长度为x-1的序列的最小末尾小于等于A[i+1],即min_end[x-1] <= A[i+1]。可是由循环不变式2可知数组min_end[1 ... k]是递增的,由x-1 >= pos可知应该有min_end[x-1] > A[i+1], 两个不等式矛盾,所以假设不成立,序列A[0 ... i+1]中长度为x的子序列的最小末尾不会更新为A[i+1]。所以当x>pos时, min_end[x]还是长度为x的子序列的最小末尾。 循环不变式1保持。

                 2)由循环不变式2可知序列A[0 ... i]中不存在长度大于k的子序列,由min_end[k] > A[i+1]可知A[i+1]不能插入到任意一个长度为k的子序列的末尾构成长度为k+1的子序列,所以i+1次迭代后,循环不变式2保持。

                 3)由插入的位置的选择即可知数组min_end[1 ... k]仍然递增。循环不变式3保持。

   终止:n次迭代结束后,循环不变式保持。设此时min_end数组的长度为k,由循环不变式2可知, 序列A[1...n]中不存在长度大于k的递增子序列,所以该函数确实能找到最长递增子序列,得证。

      数组index和pre用来记录索引信息,用于输出一个长度为n的递增子序列。

猜你喜欢

转载自blog.csdn.net/qq_25974431/article/details/81588320
今日推荐