最長遞增子序列問題(LIS):給定一個長度爲N的數組,找出一個最長的單調遞增子序列。例如一個長度爲7的序列A={5,6,7,4,2,8,3},它最長的單調遞增子序列爲{5,6,7,8},長度爲4。注意:此處所說樹狀dp(樹狀數組優化的dp)並非樹形dp。
LIS問題解決方法有很多,而樹狀DP是其中一種比較高效的方法,樹狀DP的實現要從基礎DP開始。先引入一個模板題:
題目:“最少攔截系統”
某國有一種導彈攔截系統,這種導彈攔截系統有一個缺陷:雖然它的第1發炮彈能夠到達任意高度,但是以後每一發炮彈都不能超過前一發的高度。某天,雷達捕捉到敵國的導彈來襲,請計算最小需要多少套攔截系統。
輸入:導彈總個數,導彈依次飛來的高度。
輸出:最少要配備多少套這種導彈攔截系統。
輸入樣例:
8 389 207 155 300 299 170 158 65
輸出樣例:
2
直接用DP求解LIS(複雜度O(n2)):
定義狀態dp[ i ],表示以第 i 個數爲結尾的最長遞增子序列的長度,那麼:
dp[ i ] = max{0,dp[ j ]} +1,0<j<i , Aj<Ai
最後答案是max{dp( i )}。
#include<bits/stdc++.h>
using namespace std;
const int N=1001;
int n,dp[N],p[N],res;
int LIS(){//動態規劃
res=0;
for(int i=1;i<=n;++i){
for(int j=i;j>=1;--j){
if(p[i]>p[j]||i==j)//最差自己成列
dp[i]=max(dp[j]+1,dp[i]);
}
res=max(dp[i],res);
}
return res;
}
int main(){
while(cin>>n){
memset(p,0,sizeof(p));
for(int i=1;i<=n;++i){
cin>>p[i];
}
memset(dp,0,sizeof(dp));
cout<<LIS()<<endl;
}
return 0;
}
樹狀DP(複雜度O(n*logn)):
我們在遞推數組dp的時候,需要不斷地回頭遍歷數組本身,時間開銷比較大了,那有沒有辦法減小這部分的時間開銷呢?答案是肯定的,我們可以用樹的結構來優化數據的處理,具體思路如下:
首先把每個數看成一個節點,每個節點都含兩個屬性(val(值),cnt(序號)),接着把這些節點按val排序得到新的序列,然後枚舉所有節點的cnt,用樹狀DP處理(關鍵)。
#include<bits/stdc++.h>
using namespace std;
const int N=1001;
struct node{
int cnt,val;
inline friend bool operator<(const node &a,const node &b){
return a.val==b.val?a.cnt>b.cnt:a.val<b.val;
}
}p[N];
int n,dp[N],res;
int LIS(){//樹狀dp
res=0;
for(int i=1;i<=n;++i){
int x=p[i].cnt;
for(int j=x;j;j-=(j&(-j))){//以它爲尾的最長子序列
dp[x]=max(dp[j]+1,dp[x]);
}
for(int j=x;j<=n;j+=(j&(-j))){//刷新信息節點
dp[j]=max(dp[x],dp[j]);
}
res=max(dp[x],res);//更新結果
}
return res;
}
int main(){
while(cin>>n){
for(int i=1;i<=n;++i){
cin>>p[i].val;
p[i].cnt=i;
}
sort(p+1,p+n+1);//按值排序
memset(dp,0,sizeof(dp));
cout<<LIS()<<endl;
}
return 0;
}
看完代碼不要急,我們接着說樹狀DP是怎麼處理信息的,這主要依靠兩處循環代碼
其一:
for(int j=x;j;j-=(j&(-j))){//以它爲尾的最長子序列
dp[x]=max(dp[j]+1,dp[x]);
}
其二:
for(int j=x;j<=n;j+=(j&(-j))){//刷新信息節點
dp[j]=max(dp[x],dp[j]);
}
在這裏我們先引入一個概念——信息點(01不相混的二進制數)
如圖(被紅線標記的節點):
我們已經定義了信息點,但還要再定義兩個概念——大信息點(每行首個二進制數) 以及信息區(相鄰兩個信息點之間的區域),那這些信息點是幹嘛的呢,其實有兩個功能,功能一:接受其他節點傳來的信息,功能二:供其他節點查詢信息。
瞭解了這些以後,就可以進行解說了(重點)
在我們面前有兩個問題要處理:
問題一:處在不同信息區的兩個節點之間的信息傳遞問題。
問題二:處在同一個信息區的兩個節點之間的信息傳遞問題。
(信息,表示加入新數據後最長遞增子序長度,需要向後傳遞。)
前面說過了,把原序列通過val排序,然後我們按新序列來枚舉每一個節點的cnt,我們用x代表cnt,x首先要通過第一處循環代碼得到“以它結尾的最長上升子序列”,信息來源於它所在層並且二進制小於它的信息點,然後他會通過第二處循環代碼把信息傳給它所在層第一個二進制數大於它的信息點和所有二進制數大於它的大信息點,這就非常直觀地解釋了問題一。
而對於問題二又該怎麼解釋呢?其實已經解決了,我舉個例子你就懂了,例子一:(10000~11000)10001,10010,10011,10100,10101,10110,10111.
這是其中一組處於同一信息區的二進制序列,如果我把這些數的前綴1全部消去不就是一個更小規模樹狀,那問題二和問題一就是同一個問題,問題就迎刃而解了。