【线性动态规划】最长不下降子序列(LIS)

最长不下降子序列(LIS)

   我们知道,递推就是按照递推公式不断往前求解的一个过程,对于当前,只有一个状态描述当前的值。若某些问题并非由一个状态而是由多个状态不断推导,那么这种方法就是动态规划,简称DP。动态规划是运筹学的一个分支,是将问题分解成各个阶段,由相邻的两个阶段根据状态转移方程推到求解的一种方法。
   所谓的线性动态规划,就是该问题模型是线性的,数据结构表现为线性表的形式。

题目

题干:
   最长不下降子序列。给定一个长度为n的序列a,求出这个序列中最长不下降子序列,所谓最长不下降子序列,就是对于i<j,有a[ i ]<a[ j ]
输入格式:
第一行,一个整数n(n<=5000)
下面n行,没一个整数,其中第i+1行的数为序列a[i]的值。(a[i]<=10^9)
输出格式:
第一行,最长不下降子序列长度
第二行,最长不下降子序列(若有多个,输出一个即可)
输入样例:

8
38 27 55 30 29 70 58 65

输出样例:

3
38 55 70

分析

   本题给出的是一个序列,若将该序列的每一个数看成一个点,则数据模型就是一个线性队列。
   设f[ i ]表示到当前第i个元素为止最长不下降子序列长度,对于当前的第i个元素,考虑到跟之前哪一个子序列能继续构成不下降序列,从而选取决策。有如下状态转移方程
       f[ i ] = max{f[ i ] , f[ i ]+1} 满足 j<i & a[ j ]<=a[ i ]
   因为要输出方案,所以在转台转移的时候,需要记录一下这个状态是由哪个状态转移过来的,即记录最优决策。pre[ i ] = j表示当前子序列i的前一个为j

代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <set>
#include <string>
#include <queue>
#include <map>
#include <stack>
#include <map>
#include <unordered_map>
#include <vector>
#include <cmath>
#include<string.h>
using namespace std;
#define LL long long
#define Ios ios::sync_with_stdio(false) , cin.tie(0) , cout.tie(0);
#define mem(a,b) memset(a,b,sizeof(a))
const int N=5005;
const int inf=0x3f3f3f3f;
int n,len,a[N],f[N],pre[N],ans[N]; //f为状态描述,pre记录决策值
int main() {
    Ios;
    cin>>n;
    for(int i=1;i<n;i++) cin>>a[i];
    a[0] = -inf; //尽管a[0]不存在,因为f[1]要与a[0]比较,因此设为最小
    for(int i=1;i<=n;i++){
        pre[i] = 0; //初始化决策
        for(int j=1;j<i;j++) if(a[j]<=a[i]&&f[j]>f[pre[i]]) pre[i]=j; //记录决策
            f[i] = f[pre[i]] + 1; //更新最优值
    }
    int tail = 0;
    for(int i=1;i<=n;i++) if(f[tail]<f[i]) tail = i; //找到最长子序列的位置
    for(;tail;tail=pre[tail]) ans[++len]=tail; //将最优子序列逆序放入ans中
    cout<<len<<endl; //输出长度
    while(len) cout<<a[ans[len--]]<<" "; //输出子序列
    cout<<endl; 
    return 0;
}

优化算法

   上述算法时间复杂度为O(N * N) ,对于该题,若数据再大一点,则会超时。我们这里可以利用有序队列优化到时间复杂度为 O(N * logN)
   我们再来观察一下这个状态转移方程:
       f(i) = max{f(i)+1} 其中 j<i amd a[ j ]<=a[ i ]
   上述式子的含义为: 找到i之前的某个j,a[ j ] <= a[ j ], 对于所有的j取 f[ j ] 的最大值。
   我们来看下表

i12345678
a[ i ]3827553029705865
f[ i ]11222334

   这里的j,是找到1~i之间的所有j,其实这里面有很多是无效的查找,那么我们能否快速查找到我们所要更改的j呢?
   更新需要满足两个条件:

  • j<i && a [ j ] <= a[ i ]
  • f( j )尽可能大

   以上两个条件提示我们,后面的值一定要大于等于前面的值。因此我们试着按f[ i ]的增序构建一个a[ i ]不下降的队列。构建过程如下表所示:

i12345678说明
a[ i ]3827553029705865
i=138f = 1
i=227f =1, 27<38,替换38
i=32755f=2
i=42730f=2,30<55,替换55
i=52729f=2,29<30,替换30
i=6272970f=3
i=7272958f=2,58<70,替换70
i=827295865f=4

   可以看出,每对一次扫描,我们都是再有序队列查找相应的添加或者更新的位置。因此有序队列的二分查找的时间复杂度为 O(logN) ,所以该算法的时间复杂度为 O(N * logN)

优化代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <set>
#include <string>
#include <queue>
#include <map>
#include <stack>
#include <map>
#include <unordered_map>
#include <vector>
#include <cmath>
#include<string.h>
using namespace std;
#define LL long long
#define Ios ios::sync_with_stdio(false) , cin.tie(0) , cout.tie(0);
#define mem(a,b) memset(a,b,sizeof(a))
const int N=100001;
const int inf=0x7f;
int solve(int a[], int n){
    int s[N],f[N]; //s[i]存储有序队列的下标,f[s[i]] = i,且a[s[i]]最小
    int best,i,low,high,mid;
    mem(s,inf);
    f[1] = s[1] = best = 1;
    for(i=2;i<=n;i++){
        //二分查找以a[i]结尾的lIS长度,找到更新位置
        for(low=1,high=best;low<=high;(a[a[mid]]<=a[i])?(low=mid+1):(high=mid-1))
            mid=(low+high)/2;
        //如果f[i]>best则更新best,并设置相应的s[f[i]],否则更新有序队列s[f[i]]
        if((f[i]=high+1)>best){
            s[best=f[i]]=i;
        }else{
            s[f[i]] = (a[s[f[i]]] <=a[i])?s[f[i]]:i;
        }    
    }
    return best;
}
int main() {
    Ios;
    int a[N];
    int n,i;
    scanf("%d",&n);
    for(i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    printf("%d\n%d\n",solve(a,n));
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值