[USACO18DEC]Sort It Out

题目

传送门 to luogu

思路

虽然原题的表述是找到最小的奶牛集合,实则在求 最长的上升子序列。根据题意,不在集合中的奶牛的相对位置不会发生改变。而最后的序列是有序的,所以这些没有被选中的奶牛是天生有序的——也就是上升的。未被选中的越多,被选中的就越少,所以是在求最长上升子序列 L I S LIS LIS

至于为什么一定能完成?因为逆序对是在减少的。

那么字典序第 k k k 大该怎么处理?可以考虑将 未选中集合(也就是最长上升子序列)一位一位的确定。

假设有两个最长上升子序列,分别以 a a a b b b 开头,不妨设 a > b a>b a>b

  1. 选择序列 a a a ,对应的是集合 [ 1 , a ) ∪ ( a , . . . ] [1,a)∪(a,...] [1,a)(a,...]
  2. 选择序列 b b b ,对应的是集合 [ 1 , b ) ∪ ( b , . . . ] [1,b)∪(b,...] [1,b)(b,...]

将前 b b b 位排列出来,也就是

  1. 序列 a a a 对应 1 , 2 , 3 , . . . , b − 1 , b 1,2,3,...,b-1,b 1,2,3,...,b1,b
  2. 序列 b b b 对应 1 , 2 , 3 , . . . , b − 1 , b + 1 1,2,3,...,b-1,b+1 1,2,3,...,b1,b+1

注意到二者从第 b b b 位开始出现差异,且第 b b b 位上 a a a 的字典序小于 b b b 的字典序,故 a a a 的字典序小于 b b b 的字典序。由此,我们得到:

结论一:开头大的字典序小

那么我们只需要把最长上升子序列按照第一位从大到小进行排序,然后寻找第 k k k 大在哪个子序列开头中。开头确定后,在 开头后面的位置中 寻找长度小 1 1 1 的上升子序列,用同样的判断确定第二位。直到整个未选中集合确认为止。

既然是按照第一位排序,就用 d p i dp_i dpi 表示以第 i i i 个数开头的最长上升子序列吧(下面的最长上升子序列定义同)!

可是这样还要排序,不是很麻烦吗?

考虑两个同样长度的最大上升子序列,其开头的 下标 分别为 a a a b b b (原数组为 S S S )。不妨设 a < b a<b a<b

S a < S b S_a<S_b Sa<Sb ,则将 S a S_a Sa 加入以 S b S_b Sb 开头的上升子序列,将会得到一个新的上升子序列,且长度大于以 S a S_a Sa 开头的原序列,与原序列为最长上升子序列矛盾。故 S a < S b S_a<S_b Sa<Sb 一定不成立。

既然 S a ≥ S b S_a\ge S_b SaSb ,根据结论一,以 S a S_a Sa 开头的最长上升子序列就比 S b S_b Sb 开头的序列小。所以有:

结论二:开头靠前的字典序小

那么我们从前往后枚举开头,放进数组便 自然有序

有个小细节,就是个数可能过大(最大为 ( n n 2 ) {n\choose{\frac{n}{2}}} (2nn) n n n 100000 100000 100000 ,作者能力有限,无法计算)。但超过 k k k 个,就不再重要了,我们只需字典序第 k k k 大的。故个数始终与 k k k 取较小值。

代码

以前的 丑陋的 代码。

#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;

struct Node{
    int len; long long cnt;
};

const int MAXN=100000;
int n, a[MAXN+2]; long long k;
Node dp[MAXN+2];

Node BIT[MAXN+2];
inline int lowbit(const int &x){ return x&-x; }
Node Query(int id){
    Node ans; ans.len = 0, ans.cnt = 1;
    for(; id; id-=lowbit(id))
        if(ans.len < BIT[id].len)
            ans = BIT[id];
        else if(ans.len != BIT[id].len)
            continue;
        else
            ans.cnt = min(ans.cnt+BIT[id].cnt,k);
    return ans;
}
void Add(int id,Node &v){
    for(; id<=n; id+=lowbit(id))
        if(BIT[id].len < v.len)
            BIT[id] = v;
        else if(BIT[id].len != v.len)
            continue;
        else
            BIT[id].cnt = min(BIT[id].cnt+v.cnt,k);
}

bool chose[MAXN+2];
vector<int> head[MAXN+2];
int main(){
    cin >> n >> k;
    for(int i=1; i<=n; ++i)
        scanf("%d",&a[i]);
    for(int i=n; i; --i){
        dp[i] = Query(n-a[i]);
        ++ dp[i].len;
        Add(n+1-a[i],dp[i]);
    }
    for(int i=1; i<=n; ++i)
        head[dp[i].len].push_back(i);
    int MaxLen = Query(n).len;// of LIS
    for(int len=MaxLen,last=1; len; --len) // 本质是枚举第几位
        for(auto i:head[len])
            if(dp[i].cnt < k)
                k -= dp[i].cnt;
            else{
                chose[a[i]] = 1; // 将当前这一位设定为没有被选中的
                for(; last<=i; ++last)
                    dp[last].cnt = 0; // 确保是在当前位的后面寻找下一位
                break;
            }
    printf("%d\n",n-MaxLen);
    for(int i=1; i<=n; ++i)
        if(not chose[i])
            printf("%d\n",i);
    return 0;
}

部分参考了洛谷题解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值