最近在学习动态规划的算法,决定将动态规划的经典问题研究一下,于是便从最长非递减非连续子序列问题着手了,下面就将自己学到的一些东西和大家共享一下,希望对于研究同样问题的朋友有所帮助。
首先,该问题可描述为:给出一个由n个数组成的序列seq[1..n],找出它的最长非递减非连续序列。即求最大的 m 和a1,a2……,am,使得a1<a2<……<am且seq[a1]<=seq[a2]<=……<=seq[am]。顺便提一下,这里得到的子序列在原序列中不一定是连续的。
希望新手能先将上面的题目描述读上两三遍,然后再往下看,以免因对题意模糊不清影响而理解。
下面我就来讲两种用动态规划思想来解此题的方法,其中第一种是原始的方法,虽然比较容易理解,但是算法效率不高,时间复杂度为O(n^2),而第二种算法是在第一种算法基础上进行优化得到的,其时间复杂度为O(n log n),虽然算法效率得到了提高,但是算法的理解难度也变大了。
先从简单的算法入手,首先我们用seq[1....n]表示要找最长子序列的序列,sub_seq[]就代表要找的最长子序列。和一般的用动态规划思想解决的问题一样,我们可以这样来分析问题,如果以seq[k]为尾元素的序列seq[1....k](1<=k<n)的最长子序列已知(这句话可能会产生点歧义,其实就是说序列seq[1..2]、seq[1...3]、seq[1..4]……seq[1.....k]的最长子序列都已经知道),那么序列seq[1.....k + 1]的最长最序列就可以很容易的知道了。可以这样得到,对于所有已知的最长子序列看看可不可以和元素seq[k+1]组成更长的子序列,从组合得到的子序列中取最长的一个,作为序列seq[1...k+1]的最长子序列。
基本思想明白了之后,就开始给该问题定义状态,我们定义一个数组dp[1....n]来表示状态,dp[i]的含义是序列seq[1.....i]的最长子序列的长度,好了,那么dp[k+1]可以这样来求,对seq[1...k]从前往后扫描,如果seq[i](1<= i <= k)小于等于seq[k+1],那么就比较dp[i]+1和dp[k+1](dp[k+1]初始化为一个很小的数)的大小,如果dp[i] + 1大的话,就把dp[i] +1赋给dp[k+1],因为seq[1....i]的最长子串的长度为dp[i],和seq[k+1]组合后可形成长度为dp[i]+1的子串。最后,dp[1.....n]都求出来以后,从dp[]中选出最大的值,就是序列seq[1...n]的最长子序列。如果不止要求长度,还要得到具体的子序列的话,可以用一个数组pre[]来保存上一个结点,dp[i]的含义就是seq[i]的上一个元素,这个不是这里讲的重点,就不多说了。下面给出的是代码。实现的时候用了很多STL中的东西,但是如果读者不了解STL的话,用基本的数组也可以实现。代码中的函数LNDSS就是用来求最长序列的。
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cstdlib>
#include<vector>
#include<algorithm>
using namespace std;
/*
同态规划法求最长非递减非连续子序列
状态转移方程:dp[i] = 1 + max{ dp[k] | k < i 并且 seq[k] <= seq[i] }
时间复杂度:O(n lgn)
*/
void LNDSS(vector<int>& seq, vector<int>& sub_seq)
{
vector<int> dp(seq.size(), 0);//dp[i]的值代表以seq数组中的第i个元素为尾的非连续序列的 最大 长度
vector<int> pre(seq.size(), -1);//pre[i]的值代表在非连续序列中第i个元素的前一个元素的下标
int i,j;
for(i = 0; i < seq.size(); i ++)
{
for(j = 0; j < i; j ++)
{
if(seq[i] >= seq[j] && dp[j] + 1 > dp[i])
{
dp[i] = dp[j] + 1;
pre[i] = j;
}
}
}
int tmp = max_element(dp.begin(), dp.end()) - dp.begin();
do
{
sub_seq.push_back(seq[tmp]);
tmp = pre[tmp];
}while(-1 != tmp);
reverse(sub_seq.begin(), sub_seq.end());
}
int main(void)
{
vector<int> seq;
int tmp;
while(scanf("%d",&tmp), tmp)
{
seq.push_back(tmp);
}
vector<int> sub_seq;
LNDSS(seq, sub_seq);
for(int i = 0; i < sub_seq.size(); i ++)
{
printf("%d\n", sub_seq[i]);
}
printf("\n");
return 0;
}
好的,下面来讲第二种比较复杂的方法。这种方法的是深入分析了问题后得来的。再来回顾上一种方法,其实我们可以发现,上面在求每个状态dp[i]的时候,都必须遍dp[1...i-1]这i-1种状态,在动态规划的过程中要求n个状态,每个状态又要做i-1个动态转移操作,所以总共要做的基本操作个数是1+2+3+....+ (i-1) + (n -1),是一个等差级数,所以上面的算法时间规模是O(n^2)。那么如果要对算法进行优化的话,需要那个地方优化呢?我们可以这样分析,首先,求n个状态这一步很难再优化了,但是我们仍然可能在求状态的时候减少决策所产生的操作。第一种算法求每个状态,比如求dp[n]的时候要做n-1个状态转移操作,那么是不是可以只进行一次状态转移呢?其实第二种算法的优化就是这样的。
在讲原理之前先把算法的大致操作给大家描述一下。你在求状态值dp[i]的时候,要是按照第一种方法,需要参考前面i-1个元素的状态值,但是其实前面的这i-1种状态值并不是都需要参考,有一些注定了对dp[i]这个状态值没有影响(至于为什么没有影响,下面会讨论)。第二种算法是这样的,给你一个类似于集合的容器set(可以用数组、栈、平衡树等实现),假如你在求状态值dp[i]的时候,这个容器里面存的东西就是你做状态转移时所需要参考的前面的状态,注意里面的状态个数要小于或者等于i-1。所以你要求的dp[i]取决于这个容器里面状态。这个容器里面的东西你可以先认为它们是一个个的结构体变量,结构体的里面的三个成员为①、seq[1...n]中某个元素的下标,不妨设为i,②序列seq[1...n]中第i个元素的值seq[i]。③序列seq[1...n]中第i个元素所对应的状态值dp[i]。请读者注意一下,并不是seq[1...n]中每个元素对应的状态值dp[i]都在set中。set中存放的元素值才是我们求状态值dp[i]的时候做决策需要参考的,一些不需要参考的前面已经求出的状态值不会出现在集合中(至于哪个dp[i]才是我们做决策时用到的,在下面会讲到),而且我们每求一个状态值dp[i],只需要在集合set中做复杂度为O(logn)的操作(至于怎么操作,下面讲),这样的话,算法的总体规模降为O(nlogn)。
在算法的大体运行机制了解之后,我们分析算法的原理。来看这个特殊的集合set,在集合中任取两个元素,如第i个和第j个,(注意i、j的大小关系不定,任取)可知:如果seq[i] < seq[j] (再次提一下:i和j没有先后顺序),那么dp[i] >= dp[j]的情况不会出现,即dp[i] < dp[j],为什么呢?①因为如果dp[i] > dp[j] 那么在求dp[k]的时候,k > i > j,则dp[k]既可以从dp[i]转移,也可以选择从dp[j]转移,但是从dp[i]转移得到的dp[k] = dp[i] + 1,而从dp[j]转移得到的dp[k] = dp[j] + 1,但是dp[i] > dp[j],所以从dp[i]转移就可以,dp[j]根本没用处。②如果dp[i] = dp[j] ,那么dp[k]从dp[i] 转移和从dp[j]转移是一样的,所以dp[j]没有必要再set中。其实通过上面的分析,我们能够知道set里面的元素如果以seq[i]为主键按升序排好序的话,dp[i]也是升序的,这样就为我们做状态转移提供了一种大大提高效率的可能。
接下来就是状态转移了,任意求一个状态值dp[k],在求dp[k]之前dp[1]、dp[2]、.....‘dp[k-1]都已经求好了,并且对dp[k]有影响的前面的状态都放到了集合set中。我们要求dp[k],其实就是求得如何和前面已经得到的字串进行结合得到最长的字串,首先前面得到的子串的最后一个元素必须小于等于seq[k],因为只有这样才可以和seq[k]组成一个以seq[k]为最后一个元素的非递减非连续子序列。而set里面的值是按dp值(也是按seq值)递增排列的,我们要找的最适合转移的状态就是set中最后一个不小于seq[k]的值(不妨设为q),所以dp[k] = dp[q] + 1;
好了,下面就给大家奉上一段写的非常简练的代码,大家好好琢磨琢磨吧!!!因本人知识水平有限,所言难免有许多不正确的地方,还有很多没有说明白的地方,欢迎大家批评指正。如果有联系的必要的话可以加我的QQ: 774267423
int LNDSS(int a[], int n)
{
int i, j, * b = new int[n + 1], ans = 0;
b[ans] = - 0x3f3f3f3f;
for(i = 0; i < n; i ++)
{
j = upper_bound(b, b + ans + 1, a[i]) - b;
if(j > ans)
b[++ans] = a[i];
else if(a[i] < b[j])
b[j] = a[i];
}
delete b;
return ans;
}
这是第二种算法的参考资料,(如果我的你看不懂,可以看看这个试试)毛子青论文<<动态规划的优化>>,一个最长子序列的算法