最长单调序列的动态规划优化问题
求一个数组的最长递减子序列,比如{9,4,3,2,5,4,3,2}的最长递减子序列为{9,5,4,3,2}
常见的解法是:遍历数组序列,每遍历一个数组元素,则求序列到当前位置最长的递减序列数,用temp[i]存储。注意,当前的最长递减子序列受已经遍历的最长递减子序列影响,从序列头再遍历到当前位置的前一个位置,挨个比较 a[j]与a[i](当前元素)大小,更新temp数组。此种解法时间复杂度为 o(n^2)。
int LIS(int array[],int n)
{
int temp[n];//存放当前遍历位置最长序列
for(int i=0;i<n;++i)
{
temp[i]=1; //初始化默认长度
for(int j=0;j<i;++j) //找出前面最长的序列
{
if(array[i]<array[j] && temp[j]+1 > temp[i] )
{
temp[i] = temp[j] + 1;
}
}
}
return max(temp);
}
最长单调序列是动态规划解决的经典问题。现在以求最长下降序列(严格下降)为例,来说明怎样用O(nlogn)来解决它。设问题处理的对象是序列a[1..n]。整个动态规划算法是这样实现的:
procedure longest-decreasing-subsequence
begin
ans ←0 //最长单调递减序列长度
for i←1 to n do
begin
j←1,k←ans
while (j≤k) do
begin
m ← (j+k) div 2
if b[m]>a[i] j←m+1
else k←m-1
end
if j>ans ans←ans+1
b[j]←a[i]
end
return ans
end
这一程序非常短小精悍,其中的奥妙还是不少的。为了理解这个过程,还是从最基本的解决方法开始分析。
首先我们都知道求最长下降序列的算法:
a0=+无穷大;
M[0]=0;
M[i]=MAX{M[j]+1,0<=j<i && a[j]>a[i]}
P[i]=j(j是上式中取MAX时的j值)
ans=MAX{M[i]}
在这个公式中P表示了决策。专门考虑这个P,可能有多个决策都可以得到M[i]得到最大值,这些决策都是等价的(例如数组{9,4,3,2,9,8,7,6,10,9}中元素10之前的8个元素中序列{9,4,3,2}和{9,8,7,6}都是单调递减的4个元素)。那么我们当然可以对P进行特殊的限制,即,在所有等价的决策j中,P选择递减序列中末尾元素(即最小元素)中最大的那一个(比如上述例子中我们选择元素6,因为6>2)。
P的选择跟我们得到结果并没有任何关系,但是希望对P的解释说明这样的问题:对于第x个数来说,它可以组成长度为M[x]的最长下降序列,它的子问题是在a[1..x-1]中的一个长度为M[x]-1的最长下降序列,并且这个序列的最后一个数大于a[x]。我们让P选择这些所有可能解中末尾数最大的,也就是说在处理完a[1..x-1]之后,对于所有长度为M[x]-1的下降序列,P[x]的决策只跟其中末尾最大的一个有关,其余的同样长度的序列我们不需要关心它了。
由此想到,用另外一个动态变化的数组b,当我们计算完了a[x]之后,a[1..x]中得到的所有下降序列按照长度分为L个等价类,每一个等价类中只需要一个作为代表,这个代表在这个等价类中末尾的数最大,我们把它记为b[j],1≤j≤L。b[j]是所有长度为j的下降序列中,末尾数最大的那个序列的代表。
由于我们把a[1..x-1]的结果都记录在了b中,那么处理当前的一个数a[x],我们无需和前面的a[j](1≤j≤x-1)作比较,只需要和b[j](1≤j≤L)进行比较。
对于a[x]的处理,我们简单地说明。
首先,如果a[x]<b[L],也就是说在a[1..x-1]中只存在长度为1到L的下降序列,其中b[L]是作为长度为L的序列的代表。由于a[x]<b[L],显然把a[x]接在这个序列的后面,形成了一个长度为L+1的序列。这时b[L+1]=a[x],即a[x]作为长度为L+1的序列的代表,同时L应该增加1。
另一种可能是a[x]>=b[1],显然这时a[x]是a[1..x]中所有元素中最大的,它仅能构成长度为1的下降序列,同时它又必然是最大的,所以它作为b[1]的代表,b[1]=a[x]。
如果前面的情况都不存在,我们肯定可以找到一个j,2≤j≤L,有b[j-1]>a[x],b[j]≤a[x],这时分析,a[x]必然接在b[j-1]后面,新成一个新的长度为j的序列。这是因为,如果a[x]接在任何b[k]后面,1≤k<j-1,那么都有b[k+1]>a[x],a[x]不能作为代表。而对于任何的b[k],其中j≤k≤L,b[k]<=a[x],a[x]不能延长这个序列。由于a[x]≥b[j],所以我们就将b[j]更新为a[x]。
在任何一种情况完成之后,b[1..L]显然是个下降的序列,但它并不表示长度为L的下降序列,这点不可混淆。
这几种情况实际上都可以归结为:处理a[x],令b[L+1]为无穷小,从左到右找到第一个位置j,使b[j]≤a[x],然后则只要将b[j]=a[x],如果j=L+1,则L同时增加。x处以前对应的最长下降序列长度为M[x]=j。
这样的程序段先描述为:
procedure longest-decreasing-subsequence’
begin
L ←0
for x←1 to n do
begin
b[L+1]←无穷小
j←1
while (b[j]>a[x]) j←j+1
b[j]←a[x]
if j>L then L←j
end
return L
end
注意while循环部分,它容易退化,当a本身是下降序列时,它退化为O(n2)的算法。
这时我们就要利用本节提到的二分法了。斜体部分是找到最小的j,满足b[j]≤a[x],由于b[1..L]一定是一个单调下降的有序序列,我们只需要用二分查找找到这个位置。其原理就等同于在二叉树上进行查找。于是就有了我们一开始给出的经典程序段。它的关键部分是:
j←1,k←ans
while (j≤k) do
begin
m ← (j+k) div 2
if b[m]>a[i] j←m+1
else k←m-1
end
if j>ans ans←ans+1
b[j]←a[i]
以上解决了最长下降序列的长度,其实解决上升序列,或者最长不上升序列,只要将算法中的不等号略做修改。
现在我们找到了一个数组中最长递减子序列的长度,那么如何输出这个子序列呢?这里先挖个坑……
本题还有有趣的地方,在用b求得最长下降序列长度的同时,我们也完成了对a序列用最少的不下降序列进行覆盖的构造。换句话说,我们可以通过这个方法来说明一个序列的不下降最小覆盖数等于最长下降序列的长度。这是一个有趣的命题。
例如序列 {1,-1, 9,-3, 4,-5, 6,-7}求得的最长递减子序列为 {1,-1,-3,-5,-7},长度为5,则它至少需要5个不下降覆盖得到:{1}{-1,9}{-3,4}{-5,6}{-7}。
附C++示例源码:
#include <iostream>
using namespace std;
/* 最长递增子序列 LIS
* 设数组长度不超过 30
*DP + BinarySearch
*/
int MaxV[30]; /* 存储长度i+1(len)的子序列最大元素的最小值 */
int len; /* 存储子序列的最大长度 即MaxV当前的下标*/
/* 返回MaxV[i]中刚刚大于x的那个元素的下标 */
int BinSearch(int * MaxV, int size, int x)
{
int left = 0, right = size-1;
while(left <= right)
{
int mid = (left + right) / 2;
if(MaxV[mid] <= x)
{
left = mid + 1;
}
else
{
right = mid - 1;
}
}
return left;
}
int LIS(int * arr, int size)
{
MaxV[0] = arr[0]; /* 初始化 */
len = 1;
for(int i = 1; i < size; ++i) /* 寻找arr[i]属于哪个长度LIS的最大元素 */
{
if(arr[i] > MaxV[len-1]) /* 大于最大的自然无需查找,否则二分查其位置 */
{
MaxV[len++] = arr[i];
}
else
{
int pos = BinSearch(MaxV,len,arr[i]);
MaxV[pos] = arr[i];
}
}
return len;
}
void main()
{
int arr[] = {1,-1,2,-3,4,-5,6,-7};
/* 计算LIS长度 */
printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));
}
参考:
1、http://blog.csdn.net/tianshuai1111/article/details/7887810