最長不下降子序列 NlogN && 輸出序列


  剛入學時學的算法,已經忘的差不多了,回顧一下。

  對於普通的最長不下降子序列,每個數都要從頭開始遍歷,復雜度 $O(n^2)$,只能處理 $10^4$ 以內的數據。

  剛剛學弟問我,就寫了一下普通版的,順便貼一下,這是 $openjudge$ 上的最長上升序列。

 

 

 

  廢話不多說,$nlogn$ 的算法如何實現?

  利用序列的單調性

  對於任意一個單調序列,如 $1\ 2\ 3\ 4\ 5$(是單增的),若這時向序列尾部增添一個數 $x$,我們只會在意 $x$ 和 $5$ 的大小,若 $x>5$,增添成功,反之則失敗。由於普通代碼是從頭開始比較,而 $x$ 和 $1,2,3,4$ 的大小比較是沒有用處的,這種操作只會造成時間的浪費,所以效率極低。對於單調序列,只需要記錄每個序列的最后一個數,每增添一個數 $x$,直接比較 $x$ 和末尾數的大小。只有最后一個數才是有用的,它表示該序列的最大限度值。

  實現方法就是新開一個數組 $d$,用它來記錄每個序列的末尾元素,以求最長不下降為例,$d[k]$ 表示長度為k的不下降子序列的最小末尾元素。

  我們用 $len$ 表示當前湊出的最長序列長度,也就是當前 $d$ 中的最后那個位置。

  這樣就很 $easy$ 了,每讀入一個數 $x$,如果 $x$ 大於等於 $d[len]$,直接讓 $d[len+1]=x$,然后 $len++$,相當於把 $x$ 接到了最長的序列后面;

  如果 $x$ 小於 $d[len]$,說明 $x$ 不能接到最長的序列后面,那就找 $d[1...len-1]$ 中末尾數小於等於 $x$ 的的序列,然后把 $x$ 接到它后面。舉個例子,若當前 $x==7,len==8$:

1

2

3

4

5

6

7

8

2

3

4

7

7

10

12

29

  $d[1]\cdots d[5]$ 均小於等於 $x$,若在 $d[1]$ 后接 $x$,則 $d[2]$ 應換成 $x$,但 $d[2]==3$,比 $x$ 小,能接更多的數,用 $7$ 去換 $3$ 顯然是不划算的,所以 $x$ 不能接 $d[1]$ 后。同理,$d[2]\cdots d[4]$ 均不能接 $x$。由於 $d[5]\le x$ 且 $x<d[6]$,$7$ 能比 $10$ 接更多的數,所以選擇在 $d[5]$ 后接 $x$,用 $x$ 替換 $10$。

  根據這個操作過程,易知數組 $d$ 一定是單調的序列,所以查找的時候可以用二分!二分效率是 $logn$ 的,所以整個算法的效率就是 $nlogn$ 的啦~

  對於最長不下降,可以用 $stl$ 中的 $upperbound()$ 函數,比如上述操作可以寫為:

1      for (int i=2;i<=n;i++)
2      {
3         if (a[i]>=d[len]) d[++len]=a[i];  //如果可以接在len后面就接上 
4         else  //否則就找一個最該替換的替換掉 
5         {
6             int j=upper_bound(d+1,d+len+1,a[i])-d;//找到第一個大於它的d的下標 
7             d[j]=a[i]; 
8         }
9      }

  但是,對於其他的單調序列,比如最長不上升等等,需要根據情況來手寫二分。

  注意 $upperbound$ 是找單增序列中第一個大於 $x$ 的,$lowerbound$ 是找單增序列中第一個大於等於 $x$ 的,只要不是這兩個,都需要手寫二分。

  代碼:

 1 //最長不下降子序列nlogn  Song 
 2 
 3 #include<cstdio>
 4 #include<algorithm>
 5 using namespace std;
 6 
 7 int a[40005];
 8 int d[40005];
 9 
10 int main()
11 {
12     int n;
13     scanf("%d",&n);
14     for (int i=1;i<=n;i++) scanf("%d",&a[i]);
15     if (n==0)  //0個元素特判一下 
16     {
17         printf("0\n");
18         return 0;
19     }
20     d[1]=a[1];  //初始化 
21     int len=1;
22     for (int i=2;i<=n;i++)
23     {
24         if (a[i]>=d[len]) d[++len]=a[i];  //如果可以接在len后面就接上 
25         else  //否則就找一個最該替換的替換掉 
26         {
27             int j=upper_bound(d+1,d+len+1,a[i])-d;  //找到第一個大於它的d的下標 
28             d[j]=a[i]; 
29         }
30     }
31     printf("%d\n",len);    
32     return 0;
33 }
最長不下降子序列 - NlogN

 

  關於最長序列的序列輸出,網上有講解說 $nlogn$ 算法的缺點是不能輸出序列,當時剛上高中的我很不服氣,於是花了一天自己搞出來了 $nlogn$ 的序列輸出代碼。

  當時寫的:

  “想了好久,認為 $nlogn$ 做法也是可以輸出序列的,這時候需要增加一個 $c$ 數組 用來記錄每個元素在最長序列中的位置,即 c[i] 表示 a[i] 被放到了序列的第幾個位置。

  輸出時,從 數組 a 的尾部開始,逆序依次找出 c 為 len, len-1, len-2 … 3, 2, 1 的元素,並且找到一個就接着尋找 c[i]-1,直到找到 c[i] 為 1 的數。

  舉個例子:

a: 13 7 9 16 38 24 37 18 44 19 21 22 63 15
c: 1 1 2 3 4 4 5 4 6 5 6 7 8 3

  len = 8;

  我們從 15 開始倒着找 c 為 8 的元素,找到 63,接着找 c 為 7 的,找到 22,再找 c 為 6 的,找到 21,再找 c 為 5 …… 以此類推。

  從而,我們得出的序列為 63,22,21,19,18,16,9,7

  逆序輸出來,就是 7,9,16,18,19,21,22,63

  為什么這個方法是對的呢?倒序查找保證了兩個條件:

    - 如果 c 中有多個相同的數,后面的一定是最新更新的;

    - 在保證一條件的前提下,倒序找,后面的數一定可以接到前面數的后面。”

  代碼:

 1 //From - Milky Way
 2 
 3 #include <cstdio>
 4 #include <algorithm>
 5 #include <stack>
 6 using namespace std;
 7 
 8 int d[100], c[100], a[100], len = 1;
 9 
10 int main() {
11     int n; scanf("%d", &n);
12     for (int i = 1; i <= n; ++ i) {
13         scanf("%d", &a[i]);
14     }
15 
16     d[1] = a[1], c[1] = 1;
17     for (int i = 2; i <= n; ++ i) {
18         if (d[len] <= a[i]) {
19             d[++ len] = a[i], c[i] = len;
20         } else {
21             int j = upper_bound(d + 1, d + len + 1, a[i]) - d;
22             d[j] = a[i], c[i] = j;
23         }
24     }
25 
26     stack<int> sta;
27     for (int i = n, j = len; i >= 1; -- i) {
28         if (c[i] == j) {
29             sta.push(a[i]); --j;
30         }
31         if (j == 0) break;
32     }
33 
34     printf("%d\n", len);
35     while (!sta.empty()) {
36         printf("%d ", sta.top());
37         sta.pop();
38     }
39 
40     return 0;
41 }
最長不下降之輸出子序列 - NlogN

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM