【学习笔记】最长不下降子序列

版权声明:Fashion Education https://blog.csdn.net/ModestCoder_/article/details/81515294

题目
给定一个长为n(1≤n≤100000)的正整数(最大为2^31-1)序列,求最长不下降子序列的长度.

Sample inupt

7
1 3 3 6 8 3 6

Sample output

5

博主自己随便弄的一道题目
这里我讲四种方法

1、普通DP

时间复杂度O(n^2),用在此题会Tle,普通DP在本博文视为暴力~~
dp[i]表示以第i个数结尾的最长长度
我今天改进了码风,码这个代码就当练码风了

Code:

uses math;
var
    dp,a:array[0..100000] of longint;
    n,i,j,ans:longint;

begin
    readln(n);
    for i := 1 to n do
    begin
        read(a[i]);
        for j := 1 to i-1 do
            if a[j] <= a[i] then dp[i] := max(dp[i], dp[j]);
        inc(dp[i]);
        ans := max(ans, dp[i]);
    end;
    writeln(ans);
end.

2、单调队列+二分

我们令d[i]表示长度为i的最长不下降子序列的最后一个数的最小值
发现:d数组具有单调性
具体如何操作呢?
令len为当前最长长度
每读入一个数x,x有两种情况:

  • d[len]<=x,说明可以在当前最好情况更进一步,即d[++len]=x;
  • d[len]>x,那么就用x把d数组其中一个给替换掉。那么被替换的那个要满足什么条件呢?对于一个数y(1≤y≤len),要让d[y]=x必须满足:(1)d[y]>x (2)d[y-1]≤x
    为什么?因为我们保持d数组的单调性。那么找到这个y就要用二分

外面一个1~n循环,里面一个logn的二分,时间复杂度O(nlogn)

Code:

var
    d:array[0..1000000] of longint;
    n,x,y,len,i:longint;

function find:longint;
var
    l,r,mid:longint;

begin
    l := 0; r := len; find := maxlongint;
    while l <= r do
    begin
        mid := (l + r) >> 1;
        if d[mid] > x then
        begin
            r := mid - 1; find := mid;
        end else l := mid + 1;
    end;
end;

begin
    readln(n);
    read(x);
    len := 1;
    d[1] := x;
    for i := 2 to n do
    begin
        read(x);
        if d[len] <= x then
        begin
            inc(len);
            d[len] := x;
        end else
        begin
            y := find;
            if y <> maxlongint then d[y] := x;
        end;
    end;
    writeln(len);
end.

3、树状数组

树状数组的解法是优化O(n^2)暴力的,目标是把一个n变成logn。
一般树状数组优化dp的话dp数组的定义是不变的,但这边要稍微变一下,令tree[i]表示末尾为i(并不是第i个数)的最长长度
所以时间复杂度也变了,令maxv为序列中元素最大值
则O(nlog maxv)

Code:

uses math;
const
    maxn = 10000000;
var
    tree:array[0..maxn] of longint;
    n,i,x,sum,ans:longint; 

function lowbit(x:longint):longint;

begin
    exit(x and -x);
end;

procedure change(x,y:longint);

begin
    while x <= maxn do
    begin
        tree[x] := max(tree[x], y);
        inc(x, lowbit(x));
    end;
end;

function getsum(x:longint):longint;

begin
    getsum := 0;
    while x > 0 do
    begin
        getsum := max(getsum, tree[x]);
        dec(x, lowbit(x));
    end;
end;

begin
    readln(n);
    for i := 1 to n do
    begin
        read(x);
        sum := getsum(x)+1;
        change(x,sum);
        ans := max(ans, sum);
    end;
    writeln(ans);
end.

4、线段树

跟树状数组一样的操作,复杂度同上

Code:

uses math;
const
    maxn= 10000000;
var
    tree:array[0..maxn] of record
        num,tag:longint;
    end;
    n,i,x,sum,ans:longint; 

procedure pushup(root:longint);

begin
    tree[root].num := max(tree[root << 1].num,tree[root << 1 + 1].num);
end;

procedure pushdown(root:longint);

begin
    if (tree[root].tag > 0) then
    begin
        tree[root << 1].num := max(tree[root << 1].num, tree[root].tag);
        tree[root << 1 + 1].num := max(tree[root << 1 + 1].num, tree[root].tag);
        tree[root << 1].tag := max(tree[root << 1].tag, tree[root].tag);
        tree[root << 1 + 1].tag := max(tree[root << 1 + 1].tag,tree[root].tag);
        tree[root].tag := 0;
    end;
end;

procedure update(root,tl,tr,l,r,k:longint);
var
    mid:longint;

begin
    if (tl > r) or (tr < l) then exit;
    if (tl >= l) and (tr <= r) then
    begin
        tree[root].num := max(tree[root].num, k);
        tree[root].tag := max(tree[root].tag, k);
        exit;
    end;
    pushdown(root);
    mid := (tl + tr) >> 1;
    update(root << 1, tl, mid, l, r, k);
    update(root << 1 + 1, mid + 1, tr, l, r, k);
    pushup(root);
end;

function query(root,tl,tr,l,r:longint):longint;
var
    mid:longint;

begin
    if (tl > r) or (tr < l) then exit(0);
    if (tl >= l) and (tr <= r) then exit(tree[root].num);
    pushdown(root);
    mid := (tl + tr) >> 1;
    query := max(query(root << 1, tl, mid, l, r), query(root << 1 + 1, mid + 1, tr, l, r));
end;

begin
    readln(n);
    for i := 1 to n do
    begin
        read(x);
        sum := query(1, 1, n, 1, x) + 1;
        update(1, 1, n, x, maxn, sum);
        ans := max(ans, sum);
    end;
    writeln(ans);
end.

总结:

单调队列,树状数组,线段树这三种方法都≈O(nlogn)
但仔细观察一下3、4两种方法和题目的数据范围可发现,树状数组和线段树在本题不能用!因为序列中元素最大可以达到2^31-1
在本题四种方法提交后的结果:

  • 暴力DP:Tle
  • 单调队列:AC
  • 树状数组:RE
  • 线段树:RE

令maxn为n的最大值(与代码中的maxn无关)
我们发现:

  • maxn < maxv ,单调队列为正解
  • maxn > maxv ,树状数组与线段树比单调队列跑得更快一点;树状数组码量又比线段树小,所以树状数组最优!不过一般这种情况很少,也满足O(nlogn)可以过

经过分析,我们得到一个结论:这种题型用单调队列!

我码字码到这里突然又发现:O(nlogn)算法无法打印具体的方案(即输出最长的不下降子序列)!!!
若题目要求打印方案之类的东西,只能用O(n^2)

所以,现在真正的结论出来了:

  • 不打印方案—->单调队列
  • 打印方案—->暴力DP

猜你喜欢

转载自blog.csdn.net/ModestCoder_/article/details/81515294