最长非降子序列、非升子序列(问题)

问题大致是给出一组数列,求出数列中最长不降子序列或者最长不升子序列,这里的子序列在原序列中可以不连续。

     顾名思义,不降可理解为递增,不升亦可解释为递减。

     显而易见,动态规划是一种容易让人想到的算法。以下先用动态规划求取最长递增子序列中的元素个数。

     我们可以举出一些例子,如:

     23,54,16,43,67,23,79分别储存在数组input[i]中;

     另外再定义一数组flag[i]储存下标从0到i这些元素中存在的最长递增子序列的元素个数。如flag[5]表示从input[0]到input[5]之间存在的最长递增子序列长度。由于flag[0]标识区间中只有input[0],故flag[0]=1;接下来进入循环,再次发觉数学归纳对算法编写起着不可言喻的精妙作用!我们假设flag[0]到flag[i-1]已经求出,接下来求flag[i]。那么flag[i]与flag[0]到flag[i-1]又有什么关联呢?接下来看例子:

    input[0]=23——>flag[0]=1;

    input[1]=54——>flag[1]=flag[0]+1,这里的前提是input[0]<=input[1];

    input[2]=16——>flag[2]=1;

    input[3]=43——>flag[3]=flag[0]+1;

    input[4]=67——>flag[4]=flag[1]+1,前提是input[1]<=input[4];

     ...

     由以上部分模拟可以归纳出flag[i]的求法:即在input[k]<=input[i]的前提下,其中k<i,找到最大的flag[k],置flag[i]=flag[k]+1即可,代码如下:  


flag[0]=1;
 for(int j=1;j<n;j++)
 {
  for(int k=0;k<j;k++)
  {
   if(input[k]<=input[j]&&flag[j]<flag[k]+1)
    flag[j]=flag[k]+1;
  }

 }

最后再找到最大的flag即可。本算法的时间复杂度为(o^2)。

(附上用O(n^2)找出一个长子序列的代码,如下:)

[code]

#include<iostream>
#include<cstring>
using namespace std;

#define MAX_N 100

int dp[MAX_N],s[MAX_N],path[MAX_N];

void output(int k){
	if(k==-1) return; 
	output(path[k]);
	cout<<s[k]<<" ";
}
int main(){
	int n;

	while(cin>>n){

		fill_n(dp,n+1,1);
		memset(path,-1,sizeof(path));

		int i=0,j,k;

		while(i<n)
			cin>>s[i++];
		k=0;

		for(i=1;i<n;i++){
			for(j=0;j<i;j++){
				if(s[i]>s[j] && dp[j]+1>dp[i]){
					dp[i]=dp[j]+1;
					path[i]=j;
					if(dp[k]<dp[i])
						k=i;
				}
			}
		}

		cout<<"最长公共子序列长度为:"<<dp[k]<<endl<<endl;

		cout<<"最长公共子序列为(其中一个):"<<endl;

		output(k);

		cout<<endl;
	}
	return 0;
}


      类似的题目还有另外一种不容易想到的算法。这种算法本人认为其精巧度与KMP算法有的一比。下面用这种算法解决递减问题。

    给出如下序列,求最长递减子序列:

   21,42,27,64,66,9,,24,38

   同样这些数据存储在input数组中,另外再定义一组数组store[i],存储递减子序列长度为i的子序列中的最后一个元素。比如store[5]表示递减子序列长度为5的序列的最后一个元素。初始化所有store为0,看完后就会发现置为0的因果所在。

接下来:store[1]=input[0]=21;用集合表示则store={21};由于求的是递减子序列,则每次读取一个新元素时:用这个元素替换掉原集合中的第一个比其小的元素(这样后面才更有可能插入更多的元素),此时子序列长度len不变;另一种情况,如果集合中没有比这个元素小的元素的话,就将该元素插入集合末尾,此时len显然比原来+1;

下面模拟每次读取一个数据,集合改变一次:

读入42:store={42};

读入27:store={42,27};len++

读入64:store={64,27};len不变

读入66:store={66,27};len不变

读入9:store={66,,27,9};len++

...


最后还有一个要点就是上边提到的如何找到第一个比所读取到得元素小的元素!当然用个for循环可以办到,但是二分查找似乎更能节省时间。于是在有序的store数组中用二分法查找到第一个比读入的数据小的元素。二分查找代码如下:

int nearBiggerIndex(int* s,int sL,int sR,int compareNum) /*s[x]<=compareNum<s[x-1],return x */
{
 int left,right,mid;
 left=sL;
 right=sR;
 mid=(left+right)>>1;
 while(left<=right)
 {
  if(s[mid]>compareNum)
   left=mid+1;
  else if(s[mid]<compareNum)
   right=mid-1;
  else
   return mid;
  mid=(left+right)>>1;
 }
 return left;
}

while(left<=right)这个循环也让我琢磨良久,其实left==right才能保证答案的精确,因为此时无非三种情况:

1、s[left==right==mid]==compareNum,此时直接返回mid就可以;

2、s[left==right==mid]>compareNum,此后left=mid+1,则此时compareNum>s[left],返回下标left即可;

3、s[left==right==mid]<compareNum,这时mid已经在compareNum右边,进行right=mid-1,对最后返回下标left是没有影响的。

以上能够成立,均因二分性质所致,导致s[mid]成为在整个区间中与compareNum相距最近的数组元素!

接下来就可以写出剩余的关键代码:

for(int j=1;j<n;j++)
 {
  k=nearBiggerIndex(store,1,len+1,input[j]);   /*这里传入len+1与前边初始化store为0休戚相关,是为了可以给集合增加元素,换句话说是在store数组中添入新的input[j],因为0是最小的,如果前边没有比input[j]小的数的话,store[len+1]将被替换,实为加入input[j] */
  store[k]=input[j];
  if(k>len)
   len=k;     //store中增加一个元素
 }

转自:http://hi.baidu.com/zongwobuwang/blog/item/e48745c479072bc6d10060c5.html

另外附上一种STL算法


#include <iostream>

#include <set>

 

using namespace std;

 

int main()

{

       size_t n;

 

       while (cin>>n)

       {

              set<long> Last;

             

              for (size_t i=0; i<n; i++)

              {

                     long Temp;

                     set<long>::iterator Index;

 

                     cin>>Temp;

                     Last.insert(Temp);

                     Index=Last.find(Temp);

                     if (++Index!=Last.end()) Last.erase(Index);

              }

              cout<<Last.size()<<endl;

       }

       return 0;

}

来自:http://hi.baidu.com/hpagent/blog/item/c4e90d8572a9ad3fc75cc348.html


展开阅读全文

没有更多推荐了,返回首页