动态规划系列学习之 最长上升子序列(LIS)问题全解

lis - Longest Increasing Subsequence - 最长上升子序列

1.LIS问题

给定一个序列,找出其中最长的上升子序列,输出长度

O(nlogn)做法

维护一个集合,第i个元素表示长度为i的上升子序列末尾数字.
每次出现新的数字时,查找并替换它的lower_bound,如果没有则直接插入.
集合长度即为lis长度.

例题: HDU 1025

排序后求lis,注意roads.

pair<int,int> save[M];
int main(void)
{
    int n,kase=0;
    while(scanf("%d",&n)!=EOF)
    {
        for(int i=0;iset<int> st;
        for(int i=0;iint val = save[i].second;
            auto it = st.lower_bound(val);
            if(it != st.end()) st.erase(it);
            st.insert(val);
        }   
        printf("Case %d:\nMy king, at most %u road%s can be built.\n\n",
            ++kase,st.size(),st.size()==1?"":"s" );
    }
    return 0;
}   

2.LIS问题的变体

1.最初版本:最长上升子序列

set<int> st;
for(int i=0;iint val;
    auto it = st.lower_bound(val);
    if(it != st.end()) st.erase(it);
    st.insert(val);
}   

2.最长下降子序列
将原程序中的set 改为 set>
3.最长不下降子序列
将原程序中的set改为multiset,lower_bound改为upper_bound
4.最长不上升子序列
综合2与3.

例题: HDU 5532

直接求lis(3)与lis(4),判断长度是否都小于n-1

int save[M];
int main(void)
{
    int t=read();
    while(t--)
    {
        int n=read();
        for(int i=0;imultiset<int> st;
        for(int i=0;iint val = save[i];
            auto it = st.upper_bound(val);
            if(it!=st.end()) st.erase(it);
            st.insert(val);
        }
        multiset<int,greater<int>> st2;
        for(int i=0;iint val = save[i];
            auto it2 = st2.upper_bound(val);
            if(it2!=st2.end()) st2.erase(it2);
            st2.insert(val);
        }
        printf("%s\n",(int)st.size()>=n-1||(int)st2.size()>=n-1?"YES":"NO" );
    }

    return 0;
}

3.Dilworth定理

Dilworth定理:对于一个有穷偏序集,最少链划分等于最长反链长度。
Dilworth对偶定理:对于一个有穷偏序集,其最少反链划分数等于其最长链的长度。

用人话说,在一个序列中下降子序列的最少个数就等于其最长不下降子序列长度,反过来同理,上升同理,即
lis(1)的长度=lis(4)的个数,
lis(4)的长度=lis(1)的个数,
lis(2)的长度=lis(3)的个数,
lis(3)的长度=lis(2)的个数,
可以简记为,”不”字可以将长度与个数转换.

例题: luogu P1020 导弹拦截

求lis(4)的长度与个数,即求lis(4)和lis(1).

int save[M];
int main(void)
{
    int n=0;
    while(scanf("%d",&save[n])!=EOF)
        n++;
    multiset<int,greater<int>> st4;
    for(int i=0;iint val = save[i];
        auto it = st4.upper_bound(val);
        if(it!=st4.end()) st4.erase(it);
        st4.insert(val);
    }
    set<int> st1;
    for(int i=0;iint val = save[i];
        auto it = st1.lower_bound(val);
        if(it!=st1.end()) st1.erase(it);
        st1.insert(val);
    }
    printf("%u\n%u\n",st4.size(),st1.size() );

    return 0;
}

4.输出答案

给定一个序列,输出LIS长度及LIS.

O(nlogn)做法

集合中不再维护值,而是维护对应下标,比较规则仍按值比较.
对下标记录一个前驱数组,表示这个下标如果在lis中,它的上一个数是哪个下标.
集合中每插入一个新下标时,这个下标的前驱就是它在集合中上一位的下标.(或者-1)
输出时从集合最后一个元素开始按前驱倒序输出.

例题: HDU 1160

按速度从大到小排序,求体重的lis.

struct Item{
    int w,s,id;
}save[M];
int cmp_sort(Item a,Item b){
    return a.s==b.s?a.wb.s;
}
struct cmp{
    bool operator()(int a,int b){
        return save[a].wint last[M];
void print(int a)
{
    if(a==-1) return;
    print(last[a]);
    printf("%d\n",save[a].id );
}
int main(void)
{
    int n=0;
    while(scanf("%d%d",&save[n].w,&save[n].s)!=EOF)
    {
        save[n].id = n+1;
        n++;
    }
    sort(save,save+n,cmp_sort);
    set<int,cmp> st;
    for(int i=0;iauto it = st.lower_bound(i);
        if(it != st.end()) st.erase(it);
        it = st.insert(i).first;
        last[i] = (it==st.begin()?-1:*(--it)); 
    }
    printf("%u\n",st.size() );
    print(*st.rbegin());

    return 0;
}

这是一个AC代码,但是可以被hack.
1 4 2 3 3 2 4 1 5 1

5.记录信息

通过输出答案可以看出,跑一遍lis可以记录很多信息,比如以每个位置为结尾的lis长度.
本质上输出答案也是记录了信息:序列中上一个数的位置.

Luogu P1091 合唱队形

给一个数字序列,求最少删去几个数能使得序列成为一个先升再降的序列.上升下降部分均可以没有,即lis(1)或lis(2).

跑一遍lis(1),记录以每个位置为结尾的lis长度,记作in.
再倒序跑一遍lis(1),记录以每个位置为结尾的倒序lis长度,记作out.
答案就是 maxn1i=0(nin[i]out[i]+1) m a x i = 0 n − 1 ( n − i n [ i ] − o u t [ i ] + 1 )
out中实际记录的是以每个位置为开头的lis(2)长度.

int save[M];
int in[M],out[M];
struct cmp{
    bool operator() (int a,int b){
        return save[a]int main(void)
{
    int n=read();
    for(int i=0;iset<int,cmp> st;
    for(int i=0;iauto it = st.lower_bound(i);
        if(it!=st.end()) st.erase(it);
        it = st.insert(i).first;
        in[i] = it==st.begin()?1:in[*(--it)]+1;
    }
    st.clear();
    for(int i=n-1;i>=0;i--)
    {
        auto it = st.lower_bound(i);
        if(it!=st.end()) st.erase(it);
        it = st.insert(i).first;
        out[i] = it==st.begin()?1:out[*(--it)]+1;
    }

    int ans=n;
    for(int i=0;i1);
    printf("%d\n",ans );
    return 0;
}

6. 习题

Luogu P1439

求两个1到n排列的LCS.

映射后求LIS.

int mp[M];
int main(void)
{
    int n=read();
    for(int i=1;i<=n;i++) mp[read()]=i;
    set<int> st;
    for(int i=1;i<=n;i++)
    {
        int val = mp[read()];
        auto it = st.lower_bound(val);
        if(it!=st.end()) st.erase(it);
        st.insert(val);
    }
    printf("%u\n",st.size() );
    return 0;
}

HDU 3564 Another LIS

一个初始为空的序列,将1到n依次插入在给定的位置,每次插入后输出当前的lis长度.

离线操作,用某种数据结构得到序列后,跑一遍lis得到以每个位置为终点的lis长度.
从1到n遍历,与前一个输出的答案求max,输出.

这个题的难点实际上全在想到离线操作,得到序列部分是数据结构的经典题.

但是不会做,题解都看不懂,日后再补吧!

你可能感兴趣的:(学习笔记)