動態規劃專題之最長上升子序列
專題四:最長上升子序列
/*
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