算法导论15.4-5、6 最长递增子序列(n平方)和(nlogn)

(N平方) 

15.4-5

[cpp]  view plain copy
  1. #include <iostream>  
  2. #include <string>  
  3. using namespace std;  
  4.   
  5. #define N 10  
  6. int c[N+1] = {0};//c[i]表示A[1..i]的最长递增子序列  
  7. int pre[N+1];//pre[i]表示若要得到A[1..i]的最长递增子序列,i的前一个是哪个  
  8. //求最长递增子序列  
  9. void Length(int *A)  
  10. {  
  11.     int i, j;  
  12.     //A[i] = max{A[j]+1} if A[j]>A[i] and j<i  
  13.     for(i = 1; i <= N; i++)  
  14.     {  
  15.         //初始化  
  16.         c[i] = 0;  
  17.         pre[i] = 0;  
  18.         for(j = 0; j < i; j++)  
  19.         {  
  20.             if(A[i] > A[j])  
  21.             {  
  22.                 if(c[j] + 1 > c[i])  
  23.                 {  
  24.                     c[i] = c[j]+1;  
  25.                     pre[i] = j;  
  26.                 }  
  27.             }  
  28.         }  
  29.     }  
  30.     cout<<c[N]<<endl;  
  31. }  
  32. //若要输出A[1..n]中的最长单调子序列,先输出A[1..pre[n]]中的最长单调子序列  
  33. void Print(int *A, int n)  
  34. {  
  35.     if(pre[n])  
  36.         Print(A, pre[n]);  
  37.     cout<<A[n]<<' ';  
  38. }  
  39. int main()  
  40. {  
  41.     //因为从第开始记数,所以数组中的第一个数不算,只是占一个位置  
  42. //  int A[N+1] = {0,1,2,3,4,5,6,7,8,9,10};  
  43.     int A[N+1] = {0,11,2,13,4,15,6,17,8,19,10};  
  44.     Length(A);  
  45.     Print(A, N);  
  46.     return 0;  
  47. }  

(nlogn):

设序列X(n),最长递增子序列长度为m,考虑长度为i的递增子序列,这种序列有多个,最小的末尾元素记为L(i),可以得到 L(1) <= L(2) <= ... <= L(m),这个证明较简单,使用反证法即可。在这个递增的序列中使用十分法查找,则可以实现O(nlogn)的算法。

从左到右扫描序列X(n),L(1) 初始为x(1),再引入一个当前最大长度K,初始为1,K表示目前扫描过的序列包含的最长递增子序列的长度,此时L(1)...L(K)有意义。读入一个数据x,如果L(i)是大于x中最小的,则x也是一个递增子序列的末尾元素,按照L(i)的定义,则L(i) = x,如此可以将数据x替换L(i),以下是函数实现:

[cpp]  view plain copy
  1. #define NO_STRICTLY  
  2. static inline int find_pos(int *B, int len, int value)  
  3. {  
  4.     int left = 0, right = len - 1, middle = 0;  
  5.   
  6.     while (left <= right) {  
  7.         middle = (left + right)>>1;  
  8.         if (B[middle] < value)  
  9.             left = middle + 1;  
  10. #ifdef NO_STRICTLY  
  11.         else if (B[middle] == value)  
  12.             left = middle + 1;  
  13. #endif  
  14.         else  
  15.             right = middle - 1;  
  16.     }  
  17.       
  18.     return left;  
  19. }  


用构造法证明,由于L(i)是大于x中最小的,则x>=L(i-1),按照L(i-1)的定义,将x添加到L(i-1)的i-1 长度的递增子序列的末尾,就构造了一个长度为i的递增子序列。

形象的来讲,每读入一个数,就尝试将此数放置到一个递增序列的末尾,构造出一个更长的递增序列。

 

详细步骤参考如下的例子:

序列 2 1 3 0 4 1 5 2 7

 

L1

L2

L3

L4

L5

开始

2

 

 

 

 

读入1

1

 

 

 

 

读入3

1

3

 

 

 

读入0

0

3

 

 

 

读入4

0

3

4

 

 

读入1

0

1

4

 

 

读入5

0

1

4

5

 

读入2

0

1

2

5

 

读入7

0

1

2

5

7

 

以上过程有如下几个特征

1.        L的值只会越变越小,这是最自然的,因为L(i)就是长度为i的递增子序列中的最小末尾元素

2.        读入一个数据x,会覆盖某个L,这个L是>=x中最小的,如果x大于所有L,则新生成一个L。读入每个数据,L都是一个递增序列

3.        由于L是递增序列,插入数据时可以使用二分法进行,所以每个输入字符时间复杂度为O(logn),整体时间复杂度为O(nlogn)

4.        L的长度即为序列X(n)的最长递增子序列的长度

5.        从最左下角的L开始,按照“往上、往左”方向就会输出最长递增子序列的内容。优先往上,如果上方数据和当前数据相同,如果上方数据不同则转向左,上图最后输出的递增子序列为“1 3 4 5 7”

代码如下:

[cpp]  view plain copy
  1. static int lis_nlogn_old(int *p, int len, int *inc_seq)  
  2. {  
  3.     int i = 0;  
  4.     int *L = NULL;  
  5.     int **seq = NULL;  
  6.     int pos = 0, curr_len = 0;  
  7.   
  8.     seq = malloc(len * sizeof(int *));  
  9.     seq[0] = malloc(len * sizeof(int) * len);  
  10.     for (i = 1;i < len;i++) {  
  11.         seq[i] = seq[0] + len * i;  
  12.     }  
  13.       
  14.     L = malloc((len + 1) * sizeof(int));  
  15.   
  16.     L[0] = p[0];  
  17.     curr_len = 1;  
  18.     seq[0][0] = p[0];  
  19.   
  20.     for (i = 1;i < len;i++) {  
  21.         pos = find_pos(L, curr_len, p[i]);  
  22.         L[pos] = p[i];  
  23.         if (pos > 0) {  
  24.             memcpy(seq[pos], seq[pos - 1], pos * sizeof(int));  
  25.             seq[pos][pos] = p[i];  
  26.         }  
  27.         else {  
  28.             seq[0][0] = p[i];  
  29.         }  
  30.         if (pos + 1 > curr_len)  
  31.             curr_len++;  
  32.     }  
  33.   
  34.     free(L); free(seq[0]);free(seq);  
  35.   
  36.     memcpy(inc_seq, seq[curr_len - 1], curr_len * sizeof(int));  
  37.     return curr_len;  
  38. }  

很遗憾,虽然上述算法可以在O(nlogn)的时间内得到最长子序列的长度,但无法得到整个子序列,原因是每读入一个新数据,就需要将前一个数据保存的L值复制过来,考虑这部分的时间,就会发现整体复杂度为O(n*n)

 

注意整个子序列不能通过L数组来获得,因为某个L[i]会在后续被修改过了,为获取整个子序列,需要保存每个元素的前驱元素,即当前元素所属于的递增子序列的前一个元素,注意到每个元素的前驱元素一旦确定,就不会改变,所以可以根据这个前驱关系确定最长子序列。下面是更新后的代码

[cpp]  view plain copy
  1. static int lis_nlogn(int *p, int len, int *inc_seq)  
  2. {  
  3.     int i = 0, pos = 0, curr_len = 0;  
  4.     int *L = NULL, *prev = NULL, *M = NULL;  
  5.   
  6.     L = malloc(len * sizeof(int));  
  7.     M = malloc(len * sizeof(int));      
  8.     prev = malloc(len * sizeof(int));  
  9.   
  10.     L[0] = p[0];  
  11.     M[0] = 0;  
  12.     prev[0] = -1;               /* the prev of the p[0] is NULL */   
  13.     curr_len = 1;  
  14.   
  15.     /* Caculate prev and M */  
  16.     for (i = 1;i < len;i++) {  
  17.         pos = find_pos(L, curr_len, p[i]);  
  18.         L[pos] = p[i];  
  19.         M[pos] = i;  
  20.         if (pos > 0)  
  21.             prev[i] = M[pos - 1];  
  22.         else  
  23.             prev[i] = -1;  
  24.         if (pos + 1 > curr_len)  
  25.             curr_len++;  
  26.     }  
  27.   
  28.     /* Output increasing sequence */  
  29.     pos = M[curr_len - 1];  
  30.     for (i = curr_len - 1;i >= 0 && pos != -1;i--) {  
  31.         inc_seq[i] = p[pos];  
  32.         pos = prev[pos];  
  33.     }  
  34.   
  35.     return curr_len;  
  36. }  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值