#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的值是任意的):
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的递增子序列。