题目

有一个\(n\)的排列。
给你每个位置结尾的最长上升子序列的长度\(a_i\),让你构造这个排列使得每个位置开头的最长下降子序列的长度之和最大。


思考历程

贪心一直都不是我的强项……
我比赛的时候是想着如何用差分约束之类的东西搞的:
对于每个\(a_i\),找到前面的\(a_j=a_i-1\),它们至少有一个满足\(x_j\)小于\(x_i\)
对于前面的每个\(a_j\geq a_i\)\(x_j\)都大于\(x_i\)
然后我发现我不会连边……因为“至少”这个东西太恶心了……


正解

贪心的精髓在于钦定。
有个不怎么显然的策略就是:将大的尽量往前放,小的尽量往后放。
按照这个策略,就可以钦定一些东西:
对于所有的\(a_j=a_i-1\),取最后一个\(j\),钦定\(a_j<a_i\)。因为\(a_i\)限制了前面满足条件的\(j\)的其中一个,所以将这个放在最后是最优的策略。
对于前面的\(a_j=a_i\),取最后一个\(j\),钦定\(a_j>a_i\)。这是为了保证构造出来的序列的正确性。
容易发现,这样处理之后前面的每个\(a_j\geq a_i\)\(j\)都会直接或间接地满足条件。
将这些限制关系连边,跑拓扑。
跑的时候用个堆,将小的尽量往后放。


代码

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 100010
#define ll long long
int n;
int a[N];
int buc[N];
int q[N];
bool cmpq(int son,int fa){return son>fa;}
int to[N],to2[N],d[N];
int v[N];
int t[N];
void add(int x,int c){
    for (;x<=n;x+=x&-x)
        t[x]=max(t[x],c);
}
int query(int x){
    int res=0;
    for (;x;x-=x&-x)
        res=max(res,t[x]);
    return res;
}
int main(){
    freopen("alice.in","r",stdin);
    freopen("alice.out","w",stdout);
    scanf("%d",&n);
    for (int i=1;i<=n;++i){
        scanf("%d",&a[i]);
        to[i]=buc[a[i]-1];
        d[to[i]]++;
        if (buc[a[i]]){
            to2[buc[a[i]]]=i;
            d[i]++;
        }
        buc[a[i]]=i;
    }
    int nh=0;
    for (int i=1;i<=n;++i)
        if (d[i]==0)
            q[nh++]=i;
    int num=n;
    while (nh){
        int x=q[0];
        pop_heap(q,q+nh--,cmpq);
        v[x]=num--;
        if (to[x] && !--d[to[x]]){
            q[nh++]=to[x];
            push_heap(q,q+nh,cmpq);
        }
        if (to2[x] && !--d[to2[x]]){
            q[nh++]=to2[x];
            push_heap(q,q+nh,cmpq);
        }
    }
    ll ans=0;
    for (int i=n;i>=1;--i){
        int f=query(v[i]-1)+1;
        ans+=f;
        add(v[i],f);
    }
    printf("%lld\n",ans);
    return 0;
}

总结

在贪心问题中,钦定是关键。

02-09 21:35