導彈攔截——單調佇列
洛谷p1020
前言
這道題雖然我在很久之前已經在別的地方做過了,但現在看到這道題,還是覺得有一些不得不提的知識盲區。比如一直讀入到沒輸入,lower_bound的用法等等。總之,想寫一點自己的體會。
題目大意
給出一些入侵導彈的高度(不知道數量),我們要發射攔截導彈到與侵入導彈的高度相等才能攔截掉這一顆導彈,並且每套系統每次發射的導彈的高度都不能超過前一顆,問一套系統最多能攔截幾顆導彈,攔截所有導彈最少需要多少套系統。
題目分析
對於第一問,顯然是求最長不上升子序列。第二問可能有點難,據說根據Dilworth定理,把一個數列劃分為最少的最長不上升序列的數目等於最長上升子序列的長度(這個定理超出了我的理解範圍)。我想用我的方法解釋一下為什麼是求最長上升子序列。首先,考慮這樣一個數列:3 5 4 2 6 6。打掉3,需要一套系統;打掉5,需要第2套系統。我們可以用c[i]表示第i套系統打掉的最後一顆導彈的高度。則c[1]=3,c[2]=5。考慮4,只能被第二套系統打掉,而被第二套系統打掉肯定比使用第三套系統優,所以更新c[2]=4。現在考慮2,既能被第一套系統打掉,又能被第二套系統打掉,但顯然被第一套系統打掉更優,所以更新c[1]=2。最後考慮第一個6,它不能被前兩套系統打掉,所以需要第三套系統,c[3]=6。考慮最後一個6,它能被第三套系統打掉,所以不需要多用一套系統。思考可知,c陣列構成了一個單調上升的序列(若不是,則前一個系統可以打掉這顆導彈,就不需要多用一個系統了),而且是原數列中最長的上升子序列,由此,命題得證。反之,最長上升子序列也是用上述方法求的,維護c陣列,使之保持是一個單調上升序列,每次做到一個新的數,可以二分查詢這個數在c陣列中位置,然後更新。這樣複雜度就是O(nlogn)。
詳細看程式碼:
#include<iostream>
#include<algorithm>
using namespace std;
int a[100005],b[100005],c[100005],t1,t2,n;//b陣列用來求第一問的最長不上升子序列,c陣列用來求第二問的最長上升子序列
bool cmp(int x,int y)
{
return x>y;
}
int main()
{
int i=1;
while (cin>>a[i]) i++;//讀入導彈高度,若有東西讀,則cin的返回值為true,不然返回值為false
n=i-1;//i多加了一次,n表示導彈的數量
// cin>>n;
// for (int i=1;i<=n;i++) cin>>a[i];
t1=t2=1;
b[1]=c[1]=a[1];//這樣就不用賦初值了
for (int i=2;i<=n;i++)
{
int x;//x表示a[i]應該插入的位置
if (a[i]<=b[t1]) b[++t1]=a[i];
else
{
x=upper_bound(b+1,b+t1+1,a[i],cmp)-b;//找到第一個嚴格小於這個數的位置
b[x]=a[i];//更新
}
if (a[i]>c[t2]) c[++t2]=a[i];
else
{
x=lower_bound(c+1,c+t2+1,a[i])-c;//找到第一個大於等於這個數的位置
c[x]=a[i];//更新
}
}
cout<<t1<<'\n'<<t2;
}
一些細節
讀入
此題要求讀到檔案結束,可以用cin,若讀得到,則返回值為true,不然是false。也可以用scanf:
while (scanf("%d",&a[++n]));
或者
while (scanf("%d",&a[++n])!=EOF);
二分查詢
我的二分是用STL庫裡的lower_bound和upper_bound實現的。這兩個函式都是對於單調遞增的序列來說的,如果要對單調遞減的序列使用的話,可以加一個cmp比較函式(一定要寫大於號,實現看程式碼),優先順序反轉,就可以認為是一個單調遞增序列了。lower_bound(a+1,a+n+1,x)-a表示a數組裡第一個大於等於x的數的位置, upper_bound(a+1,a+n+1,x)-a就表示a數組裡第一個大於x的數的位置。單調遞減序列要格外注意。初學者還需要多加思考與練習。
單調佇列
我所使用的單調佇列複雜度為O(nlogn),因為是對於每個數進行二分查詢位置,然後更新。當然,還有O()的演算法,寫起來比較簡潔。
總結
這道題其實並不難,不過它需要你對單調佇列的深刻理解。