LIS演算法: 最長上升子序列
LIS定義
LIS(Longest Increasing Subsequence)最長上升子序列
一個數的序列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).
你的任務,就是對於給定的序列,求出最長上升子序列的長度。
兩種做法
O(N^2)做法:dp動態規劃
狀態設計:dp[i]代表以a[i]結尾的LIS的長度
狀態轉移:dp[i]=max(dp[i], dp[j]+1) (0<=j< i, a[j]< a[i])
邊界處理:dp[i]=1 (0<=j< n)
時間複雜度:O(N^2)
舉例: 對於序列(1, 7, 3, 5, 9, 4, 8),dp的變化過程如下
dp[i] | 初始值 | j=0 | j=1 | j=2 | j=3 | j=4 | j=5 |
---|---|---|---|---|---|---|---|
dp[0] | 1 | ||||||
dp[1] | 1 | 2 | |||||
dp[2] | 1 | 2 | 2 | ||||
dp[3] | 1 | 2 | 2 | 3 | |||
dp[4] | 1 | 2 | 3 | 3 | 4 | ||
dp[5] | 1 | 2 | 2 | 3 | 3 | 3 | |
dp[6] | 1 | 2 | 3 | 3 | 4 | 4 | 4 |
求完dp陣列後,取其中的最大值就是LIS的長度。【注意答案不是dp[n-1],這個樣例只是巧合】
#include <bits/stdc++.h>
using namespace std;
const int MAXX=100000+5;
const int INF=INT_MAX;
int a[MAXX],dp[MAXX]; // a陣列為資料,dp[i]表示以a[i]結尾的最長遞增子序列長度
int main()
{
int n;
while(cin>>n)
{
for(int i=0; i<n; i++)
{
cin>>a[i];
dp[i]=1; // 初始化為1,長度最短為自身
}
int ans=1;
for(int i=1; i<n; i++)
{
for(int j=0; j<i; j++)
{
if(a[i]>a[j])
{
dp[i]=max(dp[i],dp[j]+1); // 狀態轉移
}
}
ans=max(ans,dp[i]); // 比較每一個dp[i],最大值為答案
}
cout<<ans<<endl;
}
return 0;
}
O(NlogN)做法:貪心+二分
a[i]表示第i個數據。
dp[i]表示表示長度為i+1的LIS結尾元素的最小值。
利用貪心的思想,對於一個上升子序列,顯然當前最後一個元素越小,越有利於新增新的元素,這樣LIS長度自然更長。
因此,我們只需要維護dp陣列,其表示的就是長度為i+1的LIS結尾元素的最小值,保證每一位都是最小值,這樣子dp陣列的長度就是LIS的長度。
dp陣列具體維護過程同樣舉例講解更為清晰。
同樣對於序列 a(1, 7, 3, 5, 9, 4, 8),dp的變化過程如下:
- dp[0] = a[0] = 1,長度為1的LIS結尾元素的最小值自然沒得挑,就是第一個數。 (dp = {1})
- 對於a[1]=7,a[1]>dp[0],因此直接新增到dp尾,dp[1]=a[1]。(dp = {1, 7})
- 對於a[2]=3,dp[0]< a[2]< dp[1],因此a[2]替換dp[1],令dp[1]=a[2],因為長度為2的LIS,結尾元素自然是3好過於7,因為越小這樣有利於後續新增新元素。 (dp = {1, 3})
- 對於a[3]=5,a[3]>dp[1],因此直接新增到dp尾,dp[2]=a[3]。 (dp = {1, 3, 5})
- 對於a[4]=9,a[4]>dp[2],因此同樣直接新增到dp尾,dp[3]=a[9]。 (dp = {1, 3, 5, 9})
- 對於a[5]=4,dp[1]< a[5]< dp[2],因此a[5]替換值為5的dp[2],因此長度為3的LIS,結尾元素為4會比5好,越小越好嘛。(dp = {1, 3, 4, 9})
- 對於a[6]=8,dp[2]< a[6]< dp[3],同理a[6]替換值為9的dp[3],道理你懂。 (dp = {1, 3, 5, 8})
ok,這樣子dp陣列就維護完畢,所求LIS長度就是dp陣列長度4。
通過上述求解,可以發現dp陣列是單調遞增的,因此對於每一個a[i],先判斷是否可以直接插入到dp陣列尾部,即比較其與dp陣列的最大值即最後一位;如果不可以,則找出dp中第一個大於等於a[i]的位置,用a[i]替換之。
這個過程可以利用二分查詢,因此查詢時間複雜度為O(logN),所以總的時間複雜度為O(NlogN)
#include <bits/stdc++.h>
using namespace std;
const int MAXX=100000+5;
const int INF=INT_MAX;
int a[MAXX],dp[MAXX]; // a陣列為資料,dp[i]表示長度為i+1的LIS結尾元素的最小值
int main()
{
int n;
while(cin>>n)
{
for(int i=0; i<n; i++)
{
cin>>a[i];
dp[i]=INF; // 初始化為無限大
}
int pos=0; // 記錄dp當前最後一位的下標
dp[0]=a[0]; // dp[0]值顯然為a[0]
for(int i=1; i<n; i++)
{
if(a[i]>dp[pos]) // 若a[i]大於dp陣列最大值,則直接新增
dp[++pos] = a[i];
else // 否則找到dp中第一個大於等於a[i]的位置,用a[i]替換之。
dp[lower_bound(dp,dp+pos+1,a[i])-dp]=a[i]; // 二分查詢
}
cout<<pos+1<<endl;
}
return 0;
}
一道例題
題目描述:
給出1-n的兩個排列P1和P2,求它們的最長公共子序列。
輸入格式:
第一行是一個數n,
接下來兩行,每行為n個數,為自然數1-n的一個排列。
輸出格式:
一個數,即最長公共子序列的長度
輸入樣例:
5
3 2 1 4 5
1 2 3 4 5
輸出樣例:
3
【資料規模】
對於50%的資料,n≤1000
對於100%的資料,n≤100000
求解:
顯然這是一道LCS最長公共子序列的問題,但是N特別大,對於LCS的O(N^2)做法,時間肯定得爆吧,由於題目中的兩個序列都是1~n的一個排列,噢很巧哦,由於這個條件LCS的問題可以用LIS來解決。
若其中一個排列是1,2,3…n,那麼他們的LCS(最長公共子序列)就是就是另一個序列的LIS(最長上升子序列)。如果兩個序列的排列都不是1,2,3…n,那麼我們可以認為其中一個序列是1,2,3..n,然後把第一個序列的a[1]對映到1,a[2]對映到2,a[n]對映到n,對b序列也按照a序列的對映規則處理,這樣再求b序列的LIS即可。
#include <bits/stdc++.h>
using namespace std;
const int MAXX=100000+5;
const int INF=INT_MAX;
int dp[MAXX];
int a[MAXX],b[MAXX];
int main()
{
int n;
while(cin>>n)
{
int v;
for(int i=0; i<n; i++)
{
cin>>v;
a[v]=i;//把a[i]對映到i
}
for(int i=0; i<n; i++)
{
cin>>v;
b[i]=a[v];//把b陣列按照a陣列的對映規則進行對映
}
for(int i=0; i<n; i++)
dp[i]=INF; //初始化
int pos=0; // 記錄dp當前最後一位的下標
dp[0]=b[0];
for(int i=1; i<n; i++)
{
if(b[i]>=dp[pos])
dp[++pos]=b[i];
else
dp[lower_bound(dp,dp+pos+1,b[i])-dp]=b[i];
}
cout<<pos+1<<endl;
}
return 0;
}