最長上升子序列(及打印路徑) —— 導彈攔截題解 (LIS / Dilworth定理 / dp / 貪心+二分)

最長上升子序列( LIS )問題:即求一個序列中最長的上升子序列的長度。

應用: LIS經常用於確定一個代價最小的調整方案,使一個序列變爲升序。只需要固定LIS中的元素,調整其他元素即可。


在這裏插入圖片描述

題解

第一問很明顯可以看出是LIS的類似問題,只不過是變成求最長不上升子序列長度。

第二問根據Dilworth定理,我們將其反過來:“一個偏序集的最少鏈劃分數等於其最長反鏈的長度” 同樣成立,所以我們可以得出求最少需要多少套導彈攔截系統其實就是求最少鏈劃分數,也就相當於求最長反鏈的長度,所以第二問就轉化爲一個求最長上升子序列長度的問題了。
Dilworth定理:一個偏序集的最少反鏈劃分數等於其最長鏈的長度.

關於偏序集與dilworth定理

做法1:dp ( O(n^2) )

LIS的dp做法( O(n^2) ):
(1)定義:dp[ i ]表示以a[ i ]爲末尾的最長上升子序列的長度。
(2)轉移方程:dp[ i ] = max{ dp[ i ], dp[ j+1 ] | j < i 且 a[ j ] < a[ i ] }
(3)初始條件:dp[ i ] = 1

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int a[100002], n = 0, dp[100002]={}, ret1=0, ret2=0;
    while (~scanf("%d", &a[++n])); n--;//由於某種原因這裏需-1後n纔是輸入的個數
    /*求最長不上升子序列長度*/
    for (int i = 1; i <= n; i++){
        dp[i]=1;
        for (int j = 1; j < i; j++)
            if (a[i] <= a[j]) dp[i]=max(dp[i], dp[j]+1);
        ret1 = max(ret1, dp[i]);
    }
    printf("%d\n",ret1);
    /*求最長上升子序列長度*/
    for (int i = 1; i <= n; i++){
        dp[i]=1;
        for (int j = 1; j < i; j++)
            if (a[i] > a[j]) dp[i]=max(dp[i], dp[j]+1);
        ret2 = max(ret2, dp[i]);
    }
    printf("%d\n", ret2);
    return 0;
}

做法2:貪心思想+二分優化( O(nlogn) )

LIS的貪心+二分做法( O(nlogn) ):
從LIS的性質可知要想得到一個更長的上升子序列,該序列最後一個數必須儘量的小,才能越有利於後面接其他的數,變得更長。
比如1 4 9 3 5 7,當1 4 9遇到3時序列不能再變長了,這時我們可以將3和第一個大於3的數(即4)替換,變爲1 3 9,下面再遇到5時同理,替換5-9,變爲1 3 5,這時再遇到7便能繼續變長爲1 3 5 7。最後得到的1 3 5 7的個數就是1 4 9 3 5 7的最長的上升子序列長度,所以該序列的最長上升子序列長度爲4。

通過上述例子,我們可以發現在替換時要進行替換的數準備插入到的序列是個有序序列,所以這裏可以用二分去尋找插入位置進行優化,使得時間複雜度降低爲nlogn。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+2;
int main()
{
    int a[maxn], n = 0, s1[maxn]={}, s2[maxn]={}, len1 = 1, len2 = 1;
    while (~scanf("%d", &a[++n])); n--;
    s1[1] = a[1], s2[1] = a[1];
    for(int i = 2; i <= n; i++){
    	/*求最長不上升子序列*/
        if (a[i] <= s1[len1]) s1[++len1] = a[i];
        else *upper_bound(s1+1, s1+len1+1, a[i], greater<int>()) = a[i];
        /*求最長上升子序列*/
        if (a[i] > s2[len2]) s2[++len2] = a[i];
        else *lower_bound(s2+1, s2+len2+1, a[i]) = a[i];
    }
    printf("%d\n%d\n", len1, len2);
    return 0;
}

最長上升子序列dp做法打印路徑:

/*遞歸法打印路徑*/
#include<iostream>
#include<cstring>
using namespace std;
int vis[3100];
int n,a[3100],dp[3100],ret;
void dfs(int t)
{
    if(t==-1)return;
    dfs(vis[t]);
    cout<<a[t]<<' ';
}
int main()
{
    memset(vis,-1,sizeof(vis));
    int t;
    cin>>n;
    for(int i=0;i<n;i++)
        cin>>a[i],dp[i]=1;
    for(int i=0;i<n;i++){
        for(int j=0;j<i;j++){
            if(a[j]<a[i]){
                if(dp[i]<dp[j]+1)dp[i]=dp[j]+1,vis[i]=j;
                ///dp[i]=max(dp[i],dp[j]+1);
            }
        }
        ret=max(ret,dp[i]);
        if(ret==dp[i])
           t=i;
    }
    cout<<ret<<endl;
    /*遞歸打印路徑*/
    dfs(t);
    cout<<endl;
    return 0;
}
/*
9
3 5 2 8 6 4 7 9 11
*/
/*常規方法打印路徑:*/
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
int n,a[3100],dp[3100],ret;
int main()
{
    vector<int>ans; ///存儲路徑
    int t;
    cin>>n;
    for(int i=0;i<n;i++)
        cin>>a[i],dp[i]=1; ///dp數組初始化爲1
        
    for(int i=0;i<n;i++){
        for(int j=0;j<i;j++){
            if(a[j]<a[i]) dp[i]=max(dp[i],dp[j]+1);
        }
        ret=max(ret,dp[i]);
        if(ret==dp[i])t=i; ///求出最長的上升子序列的最後一個數的下標
    }
    cout<<ret<<endl; ///輸出長上升子序列的長度
    
    /*打印路徑*/
    ans.push_back(a[t]);
    int k=a[t],j=t;
    for(int i=t-1;i>=0;i--){
        if(a[i]<=k&&dp[i]==dp[j]-1){
            ans.push_back(a[i]);
            k=a[i];
            j=i;
        }
    }
    for(int i=ans.size()-1;i>=0;i--)
        cout<<ans[i]<<' ';
    cout<<endl;
    
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章