1. 程式人生 > >動態規劃專題之最長上升子序列

動態規劃專題之最長上升子序列

專題四:最長上升子序列

/*

       Name:動態規劃專題之最長上升子序列

       Author:巧若拙

       Description:1759_最長上升子序列

描述:一個數的序列bi,當b1 < b2 < ... < bS的時候,我們稱這個序列是上升的。對於給定的一個序列(a1, a2,..., aN),我們可以得到一些上升的子序列(ai1, ai2, ..., aiK),這裡1 <= i1 < i2 < ... < iK <= N。

比如,對於序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。這些子序列中最長的長度是4,比如子序列(1, 3,5, 8).

你的任務,就是對於給定的序列,求出最長上升子序列的長度。

輸入

輸入的第一行是序列的長度N (1 <= N <= 1000)。第二行給出序列中的N個整數,這些整數的取值範圍都在0到10000。

輸出:最長上升子序列的長度

樣例輸入:

7

1 7 3 5 9 4 8

樣例輸出

4

*/

#include<iostream>

#include<cstdio>

 

using namespace std;

 

const int MAX = 1001;

int A[MAX];

int S1[MAX]; //記錄到元素i為止的最長上升子序列的長度

int S2[MAX]; //記錄到元素i為止的最長上升子序列的長度

int S3[MAX]; //記錄到元素i為止的最長上升子序列的長度

int S4[MAX]; //記錄到元素i為止的最長上升子序列的長度

int S5[MAX+1]; //記錄最長上升子序列,下標從1開始

 

int DP_1(int i); //記憶化搜尋

int DP_2(int n); //與記憶化搜尋相對應的動態規劃演算法

int DP_3(int n); //動態規劃:逆序搜尋,返回最長上升子序列第一個元素的下標

int DP_4(int n); //動態規劃:逆序搜尋,返回最長上升子序列長度

int DP_5(int n); //順序處理,二分插入

int Pos(int low, int high, int x);//二分查詢,返回第一個比x大的元素下標

void Print(int len, int i);//遞迴輸出子序列

void Print2(int n); //輸出序列

void Print3(int n); //輸出序列

 

int main()

{

       intn;

       cin>> n;

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

       {

              cin>> A[i];

       }

             

       intmaxLen = DP_1(n-1);//記憶化搜尋,需要用到全域性變數A[MAX],另有S1[MAX]初始化為0。

       for(int i=n-2; i>=0; i--)//遞減比遞增效率應該要高些

       {

              if(maxLen < S1[i])

                     maxLen= S1[i];

       }

       cout<< maxLen << endl;

       Print(maxLen,n-1);

       cout<< endl;

      

       cout<< DP_2(n) << endl;//順序處理,需要用到全域性變數A[MAX],另有S2[MAX]初始化為0。

      

       intpos = DP_3(n);//逆序處理,返回最長上升子序列第一個元素的下標,需要用到全域性變數A[MAX],另有S3[MAX]初始化為0。

       cout<< S3[pos] << endl;

       Print2(n);

       Print3(n);

      

       cout<< DP_4(n) << endl;//逆序處理,返回最長上升子序列長度,需要用到全域性變數A[MAX],另有S4[MAX]初始化為0。

      

       cout<< DP_5(n) << endl;//順序處理,二分插入,需要用到全域性變數A[MAX],另有S4[MAX]初始化為0。

      

   return 0;

}

 

演算法1:記憶化搜尋,需要用到全域性變數A[MAX],另有S1[MAX]初始化為0。

int DP_1(int i)

{

       if(S1[i] != 0)  

              return   //語句1

       if(i == 0)  

       {

              S1[i]=   //語句2

       }

       else

       {

              intlen = 0;

              for(int j=i-1; j>=0; j--)

              {

                     if(len < DP_1(j) && A[i] > A[j])  //語句3

                            len= DP_1(j);

              }

              S1[i]= len + 1;

       }

       returnS1[i];

}

問題1:將語句1和語句2補充完整。

問題2:根據樣例輸入:1 7 3 5 9 4 8,寫出對應陣列S1[]的值。

問題3:語句3能否改為if (A[i] > A[j] && len < DP_1(j))?:

 

參考答案:

問題1:語句1:return S1[i];  語句2:S1[i] = 1;

問題2:陣列S1[]的值為:1 2 2 3 4 3 4。

問題3:不能。語句3中需要先遞迴計算出子問題的解,再判斷是否得到滿足條件的子序列,若先判斷A[i]> A[j],則該條件不成立時,永遠無法呼叫遞迴函式計運算元問題的解。

 

演算法2:與記憶化搜尋相對應的動態規劃演算法,需要用到全域性變數A[MAX],另有S2[MAX]初始化為0。

int DP_2(int n)  

{

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

       {

              intlen = 0;

              for(int j=i-1; j>=0; j--)  //語句1

              {

                     if(A[i] > A[j] && len < S2[j])

                            len= S2[j];

              }

              S2[i]= len + 1;

       }

      

       intmaxLen = S2[n-1]; //記錄最長上升子序列的長度

       for(int i=n-2; i>=0; i--)

       {

              if(maxLen < S2[i])

                     maxLen= S2[i];

       }

      

       returnmaxLen;

}

 

問題1:根據樣例輸入:1 7 3 5 9 4 8,寫出對應陣列S2[]的值。

問題2:語句1能否改為:for (int j=0; j<i; j++) ?哪種寫法效率更高?

問題3:若把題目改為:求出最長不下降子序列的長度。該如何修改DP_2的程式碼?

 

參考答案:

問題1:陣列S2[]的值為:1 2 2 3 4 3 4。

問題2:可以。因為語句1所在迴圈體的作用是在A[0...i-1]中,找出一個比A[i]小且最長的上升子序列,故順序查詢和逆序查詢均可。但是對於上升子序列來說,S2[j]的值是遞增的,逆序查詢能更快地找到最大的S2[j],故語句1的寫法效率更高。

問題3:把語句if (A[i] > A[j] &&S2[j] > S2[i])改為if (A[i]>= A[j] && S2[j] > S2[i])即可。

 

演算法3:動態規劃,逆序處理,需要用到全域性變數A[MAX],另有S3[MAX]初始化為0。

int DP_3(int n) //動態規劃:逆序搜尋,返回最長上升子序列第一個元素的下標

{

       intpos = n - 1; //記錄最長上升子序列最後一個元素的下標

      

       for(int i=n-1; i>=0; i--)

       {

              intlen = 0; //記錄A[i]的後繼子序列的長度

              for(int j=i+1; j<n; j++)

              {

                     if(A[j] > A[i] && S3[j] > len)

                            len= S3[j];

              }

              S3[i]= len + 1;

             

              if(S3[i] > S3[pos]) 

                     pos= i;

       }

      

       returnpos;

}

 

問題1:根據樣例輸入:1 7 3 5 9 4 8,寫出對應陣列S3[]的值。

問題2:修改DP_3(),使其返回最長上升子序列的長度。

問題3:DP_3()返回的不是最長上升子序列的長度,而是其首個元素的下標,目的是為了方便輸出該最長上升子序列,請在DP_3()的基礎上,編寫一段程式碼,輸出該最長上升子序列。

 

參考答案:

問題1:陣列S3[]的值為:4 2 3 2 1 2 1。

問題2:程式碼如下:

演算法4:動態規劃,逆序處理,需要用到全域性變數A[MAX],另有S4[MAX]初始化為0。

int DP_4(int n) //逆序搜尋,返回最長上升子序列長度

{

       intmaxLen = 0; //記錄最長上升子序列的長度

      

       for(int i=n-1; i>=0; i--)

       {

              for(int j=i+1; j<n; j++)//在A[i]後面的元素中查詢最大的S4[j]

              {

                     if(A[j] > A[i] && S4[j] > S4[i])

                            S4[i] = S4[j];

              }

              S4[i]++;

              if(maxLen < S4[i])

                     maxLen= S4[i];

       }

      

       returnmaxLen;

}

問題3:有兩種實現方式:

void Print2(int n)

{

       intpos = DP_3(n);

       intlen = S3[pos]; //子序列的長度

       for(int i=pos; len>0; i++) //總共輸出len個元素

       {

              if(S3[i] == len) 

              {

                     cout<< A[i] << " ";

                     len--;

              }

       }

       cout<< endl;

}

 

void Print3(int n)

{

       intpos = DP_3(n);

       inti, j;

       for(i=pos; i<n;) //總共輸出len個元素

       {

              cout<< A[i] << " ";

              for(j=i+1; j<n; j++)

              {

                     if(S3[j] == S3[i]-1)

                            break;

              }

              i= j;

       }

       cout<< endl;

}

 

演算法5:順序處理,二分插入,需要用到全域性變數A[MAX],另有S5[MAX+1]初始化為0。

int DP_5(int n) //順序搜尋,二分插入

{

       intm = 0; //記錄最長不下降子序列的長度

      

       S5[++m]= A[0];

       for(int i=1; i<n; i++)

       {

              if(A[i] > S5[m]) 

              {

                     S5[++m]= A[i];

              }

              else 

              {

                     S5[Pos(1,m-1, A[i])] = A[i];  //語句1

              }

       }

      

       returnm;

}

 

int Pos(int low, int high, int x)//二分查詢,返回第一個比x大的元素下標

{

       intmid;

      

       while(low <= high)

       {

              mid= (low + high)/2;

              if(S5[mid] > x)

              {

                     high=   //語句2

              }

              else

              {

                     low=   //語句3

              }

       }

      

       return  //語句4

}

 

問題1:分別根據樣例輸入:1 7 3 5 9 4 8 和 1 7 3 5 9 2 8,寫出對應陣列S5[]的值。

問題2:語句1能否改為:S5[Pos(0, m, A[i])] = A[i];?為什麼?

問題3:將語句2,語句3和語句4補充完整。

 

參考答案:

問題1:陣列S5[]的值為:1 3 4 8 和 1 2 5 8。由此我們可以看到,演算法5雖然能夠獲得最長子序列的長度,但是不一定能獲得正確的子序列。

問題2:不能。因為陣列S5的下標從1開始,故不能寫作S5[Pos(0, m, A[i])] = A[i];雖然可以寫作S5[Pos(1, m, A[i])] = A[i];但是因為我們已經知道A[i] > S5[m],故在S5[1...m-1]中二分查詢第一個比x大的元素下標即可,因此語句1的寫法更好。

問題3:語句2:high = mid - 1; 語句3:low = mid +1; 語句4:return low;

 

拓展練習:

原題只要求計算出最長上升子序列的長度,並未要求輸出該最長上升子序列,在演算法3的問題3中,我們要求在S[3]的基礎上輸出該最長上升子序列。現在要求在演算法1或演算法2的基礎上,編寫函式void Print(int len, int i)//遞迴輸出子序列。

 

參考答案:

void Print(int len, int i)//遞迴輸出子序列

{

       if(len == 0)

              return;

       while(S1[i] != len) //不能寫S1[i]<len,因為有可能出現長度相同的子序列

       {

              i--;

       }

       Print(len-1,i-1);

       cout<< A[i] << " ";

}

 

課後練習:

練習1:1044_攔截導彈  1999年NOIP全國聯賽提高組

題目描述:某國為了防禦敵國的導彈襲擊,發展出一種導彈攔截系統。但是這種導彈攔截系統有一個缺陷:雖然它的第一發炮彈能夠到達任意的高度,但是以後每一發炮彈都不能高於前一發的高度。

某天,雷達捕捉到敵國的導彈來襲。由於該系統還在試用階段,所以只有一套系統,因此有可能不能攔截所有的導彈。

輸入描述 InputDescription

輸入導彈依次飛來的高度(雷達給出的高度資料是不大於30000的正整數)

輸出描述 OutputDescription

輸出這套系統最多能攔截多少導彈,如果要攔截所有導彈最少要配備多少套這種導彈攔截系統。

樣例輸入 SampleInput

389 207 155 300 299 170 158 65

樣例輸出 SampleOutput

6(最多攔截導彈數)

2(要攔截所有導彈最少要配備的系統數)

 

資料範圍及提示 DataSize & Hint

導彈的高度<=30000,導彈個數<=20

 

練習2:3532_最大上升子序列和

描述:一個數的序列bi,當b1 < b2 < ... < bS的時候,我們稱這個序列是上升的。對於給定的一個序列(a1, a2,...,aN),我們可以得到一些上升的子序列(ai1, ai2, ..., aiK),這裡1 <= i1 < i2 < ... < iK <= N。比如,對於序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。這些子序列中序列和最大為18,為子序列(1, 3, 5, 9)的和.

你的任務,就是對於給定的序列,求出最大上升子序列和。注意,最長的上升子序列的和不一定是最大的,比如序列(100, 1, 2, 3)的最大上升子序列和為100,而最長上升子序列為(1,2, 3)

輸入

輸入的第一行是序列的長度N (1 <= N <= 1000)。第二行給出序列中的N個整數,這些整數的取值範圍都在0到10000(可能重複)。

輸出

最大上升子序列和

樣例輸入

7

1 7 3 5 9 4 8

樣例輸出

18

 

練習3:4977_怪盜基德的滑翔翼

描述:怪盜基德是一個充滿傳奇色彩的怪盜,專門以珠寶為目標的超級盜竊犯。而他最為突出的地方,就是他每次都能逃脫中村警部的重重圍堵,而這也很大程度上是多虧了他隨身攜帶的便於操作的滑翔翼。

有一天,怪盜基德像往常一樣偷走了一顆珍貴的鑽石,不料卻被柯南小朋友識破了偽裝,而他的滑翔翼的動力裝置也被柯南踢出的足球破壞了。不得已,怪盜基德只能操作受損的滑翔翼逃脫。

假設城市中一共有N幢建築排成一條線,每幢建築的高度各不相同。初始時,怪盜基德可以在任何一幢建築的頂端。他可以選擇一個方向逃跑,但是不能中途改變方向(因為中森警部會在後面追擊)。因為滑翔翼動力裝置受損,他只能往下滑行(即:只能從較高的建築滑翔到較低的建築)。他希望儘可能多地經過不同建築的頂部,這樣可以減緩下降時的衝擊力,減少受傷的可能性。

請問,他最多可以經過多少幢不同建築的頂部(包含初始時的建築)?

輸入

輸入資料第一行是一個整數K(K < 100),代表有K組測試資料。

每組測試資料包含兩行:第一行是一個整數N(N < 100),代表有N幢建築。

第二行包含N個不同的整數,每一個對應一幢建築的高度h(0 < h < 10000),按照建築的排列順序給出。

輸出

對於每一組測試資料,輸出一行,包含一個整數,代表怪盜基德最多可以經過的建築數量。

樣例輸入

3

8

300 207 155 299 298 170 158 65

8

65 158 170 298 299 155 207 300

10

2 1 3 4 5 6 7 8 9 10

樣例輸出

6

6

9

 

練習4:友好城市

【問題描述】 Palmia國有一條橫貫東西的大河,河有筆直的南北兩岸,岸上各有位置各不相同的N個城市。北岸的每個城市有且僅有一個友好城市在南岸,而且不同城市的友好城市不相同。每對友好城市都向政府申請在河上開闢一條直線航道連線兩個城市,但是由於河上霧太大,政府決定避免任意兩條航道交叉,以避免事故。

程式設計幫助政府做出一些批准和拒絕申請的決定,使得在保證任意兩條航線不相交的情況下,被批准的申請儘量多。

【輸入格式】

  第1行,一個整數N(1<=N<=5000),表示城市數。

  第2行到第n+1行,每行兩個整數,中間用1個空格隔開,分別表示南岸和北岸的一對友好城市的座標。(0<=xi<=10000)

【輸出格式】

  僅一行,輸出一個整數,政府所能批准的最多申請數。

【輸入樣例】

  7

  22 4

  2 6

  10 3

  15 12

  9 8

  17 17

  4 2

【輸出樣例】

4

 

練習5:1058_合唱隊形  2004年NOIP全國聯賽提高組

描述:N位同學站成一排,音樂老師要請其中的(N-K)位同學出列,使得剩下的K位同學排成合唱隊形。合唱隊形是指這樣的一種隊形:設K位同學從左到右依次編號為1,2…,K,他們的身高分別為T1,T2,…,TK,  則他們的身高滿足T1<...<Ti>Ti+1>…>TK(1<=i<=K)。

你的任務是,已知所有N位同學的身高,計算最少需要幾位同學出列,可以使得剩下的同學排成合唱隊形。

輸入描述 InputDescription

輸入檔案chorus.in的第一行是一個整數N(2<=N<=100),表示同學的總數。第一行有n個整數,用空格分隔,

第i個整數Ti(130<=Ti<=230)是第i位同學的身高(釐米)。

輸出描述 OutputDescription

輸出檔案chorus.out包括一行,這一行只包含一個整數,就是最少需要幾位同學出列。

樣例輸入 SampleInput

8

186 186 150 200 160 130 197 220

樣例輸出 SampleOutput

4

 

練習6:1996_登山

描述:五一到了,PKU-ACM隊組織大家去登山觀光,隊員們發現山上一個有N個景點,並且決定按照順序來瀏覽這些景點,即每次所瀏覽景點的編號都要大於前一個瀏覽景點的編號。同時隊員們還有另一個登山習慣,就是不連續瀏覽海拔相同的兩個景點,並且一旦開始下山,就不再向上走了。隊員們希望在滿足上面條件的同時,儘可能多的瀏覽景點,你能幫他們找出最多可能瀏覽的景點數麼?

輸入

Line 1: N (2 <= N <= 1000) 景點數

Line 2: N個整數,每個景點的海拔

輸出

最多能瀏覽的景點數

樣例輸入

8

186 186 150 200 160 130 197 220

樣例輸出

4

 

練習7:5294_挖地雷

題目描述:在一個地圖上有N個地窖(N<=20),每個地窖中埋有一定數量的地雷。同時,給出地窖之間的連線路徑。當地窖及其連線的資料給出之後,某人可以從第一個地窖開始挖地雷,然後可以沿著指出的連線往下挖(僅能選擇一條路徑),當無連線時挖地雷工作結束。

設計一個挖地雷的方案,使某人能挖到最多的地雷。

 

輸入描述 InputDescription

第1行只有一個數字,表示地窖的個數N。

第2行有N個數,分別表示每個地窖中的地雷個數。

第3行至第N+1行表示地窖之間的連線情況:

第3行有n-1個數(0或1),表示第一個地窖至第2個、第3個、…、第n個地窖有否路徑連線。

如第3行為1 1 0 0 0 … 0,則表示第1個地窖至第2個地窖有路徑,至第3個地窖有路徑,至第4個地窖、第5個、…、第n個地窖沒有路徑。

第4行有n-2個數,表示第二個地窖至第3個、第4個、…、第n個地窖有否路徑連線。

… …

第n+1行有1個數,表示第n-1個地窖至第n個地窖有否路徑連線。(為0表示沒有路徑,為1表示有路徑)。

 

輸出描述 OutputDescription

第一行表示挖得最多地雷時的挖地雷的順序,各地窖序號間以一個空格分隔,不得有多餘的空格。

第二行只有一個數,表示能挖到的最多地雷數。

 

樣例輸入 SampleInput

5

10 8 4 7 6

1 1 1 0

0 0 0

1 1

1

樣例輸出 SampleOutput

1 3 4 5

27