P2101. 文翁点名

背景

题目描述

文翁班上有N个学生,每次上体育课他们会随机排列为一排,每个人有一个编号Ai(1~N的排列),现在文翁希望让所有人按编号升序排成一排,可是他不希望采取普通的排序方法来完成。他开创了一个有趣的排列方法,每一次他会点名一个学生,被点名的学生会确保他的顺序正确,当右边挨着他的人编号比他小,就和右边交换,然后当左边挨着他的人编号比他大他也会交换位置。这样,被点名的学生就完成了按他认为的按顺序排好(在他看来 左边的学生编号比他小,右边的学生编号比他大).

文翁需要选出一个学生编号子集,遍历这个子集,依次叫子集中每个人出列,重复下去直到所有人都排好序。如他选出子集{1,3,6},他就会先叫1,再叫3,再叫6.如果这n个人还没有排好序,他会再次叫1,3,6。很明显这样的子集很多,现在文翁希望得到的子集最小且能完成排序,这样他觉得还不够,他希望得到满足条件的字典序第K小子集。

输入格式

输入的第一行包含一个整数\(N\)

第二行包含一个整数\(K\)\(1≤K≤10^{18}\))。

第三行包含N个空格分隔的整数,表示从左到右学生的编号。

保证存在至少\(K\)个符合要求的子集。

输出格式

第一行输出最少需要选择多少学生数量

接下来若干行输出在所有可能子集中的字典序\(K\)小的集合编号(从小到大)

样例

输入数据 1

4 1
4 2 1 3

输出数据 1

2
1
4

开始的时候序列为4213 。在文翁叫编号为1的学生之后,序列变为1423 。在文翁叫编号为4的学生之后,序列变为1234 。在这个时候,序列已经完成了排序。

输入数据 2

6 1
6 2 4 3 1 5

输出数据 2

3
1 
3 
6

数据规模与约定

有20%数据\(N≤6\),并且\(K=1\)

另外有30%数据\(K=1\)

100%数据:\(1<=N<=10^5,1≤K≤10^{18},1<=N,10^5,1≤K≤10^{18}\)

看到这道题应该第一时间想到去寻找其对应的性质,又因为这个题目上只有一种神秘的交换,所以说应该思考交换的性质

而题目又说了这个交换和排序有关,那么可以先思考如何能一定通过这种交换方式完成排序

显而易见,当待排序集合\(A\)和集合\(S\)一样时,不停地进行交换操作,就可以使集合\(A\)有序

但是在交换的过程中和交换后,\(S\)的补集的相对位置是不会改变的,也就是说,如果\(S\)的补集不单调递增,最终序列一定不符合题意,即\(S\)一定不符合题意

与此同时,有这个结论可以合理推测:若\(S\)的补集是单调递增,则\(S\)一定符合题意,下面给出证明

考虑一个序列\(\{1,2,3,4,...,7,8,9,...,15,16,...,20\}\)

其中写出来的数是固定的,即\(S\)的补集

若进行第一次交换,则S中的最大值(\(19\))一定能跑回其规定位置,最小值(\(5\))也一定能跑回去。

由此也可以得到最终的交换序列是有序的,也就符合题意,故结论成立

若要使\(S\)元素最少,则要使\(S\)的补集元素最多,即为最长上升子序列的长度,但最长上升子序列的构成有很多种,现在的问题便是求出这些最长上升子序列的补集的字典序第\(K\)小的集合

我不知道写代码的人怎么想到的,但我知道他代码为什么要这么写

#include <bits/stdc++.h>
#define int unsigned long long  // 定义无符号长整型别名
#define lowbit(x) (x&(-x))      // 计算x的最低有效位
using namespace std;
const int N=4e5;                // 定义最大数组大小

// 定义结构体存储长度和计数
struct node{
    int len, cnt;  // len表示长度,cnt表示数量
} tr[N+100], dp[N];  // tr是树状数组,dp存储DP结果

int n, a[N];  // n是元素个数,a是原始数组

// 合并两个节点,取长度较大的,若长度相同则合并数量
node merge(node a, node b) {
    if(a.len > b.len) return a;
    if(a.len < b.len) return b;
    node ans = {a.len, a.cnt + b.cnt};
    ans.cnt = min(ans.cnt, (int)1e18);  // 防止溢出
    return ans;
}

// 更新树状数组
void update(int x, node t) {
    while(x) {  // 从x开始向前更新
        tr[x] = merge(tr[x], t);
        x -= lowbit(x);  // 移动到前一个位置
    }
}

// 查询树状数组
node query(int x) {
    node ans = {0, 1};  // 初始值
    while(x < N) {  // 从x开始向后查询
        ans = merge(ans, tr[x]);
        x += lowbit(x);  // 移动到后一个位置
    }
    return ans;
}

vector<int> v[N];  // 存储每个长度的所有位置
node ans;          // 存储最终结果
int vis[N], k, now;  // vis标记是否被选中,k是剩余需要选择的数量,now是当前位置

signed main() {
    ios::sync_with_stdio(0);
    cin >> n >> k;  // 读入n和k
    for(int i=1; i<=n; i++) cin >> a[i];  // 读入数组
    
    // 从后向前处理,计算以每个位置结尾的最长递增子序列
    for(int i=n; i>=1; i--) {
        dp[i] = query(a[i]+1);  // 查询比a[i]大的元素
        dp[i].len++;            // 长度加1
        update(a[i], dp[i]);    // 更新树状数组
        v[dp[i].len].push_back(i);  // 按长度分组存储位置
        ans = merge(ans, dp[i]);    // 更新全局最大值
    }
    
    cout << n - ans.len << endl;  // 输出需要删除的元素数量
    
    // 从最长长度开始,选择k个序列
    for(int i=ans.len; i>=1; i--) {
        // 从后向前处理每个长度的位置
        for(int j=v[i].size()-1; ~j; j--) {
            int pos = v[i][j];
            if(dp[pos].cnt < k)  // 如果当前数量不足k
                k -= dp[pos].cnt;  // 减去已处理的数量
            else {  // 否则选择当前元素
                vis[a[pos]] = 1;  // 标记为已选
                while(now <= pos) dp[now++].cnt = 0;  // 清空前面的计数
                break;
            }
        }
    }
    
    // 输出未被选中的元素
    for(int i=1; i<=n; i++) 
        if(!vis[i]) cout << i << endl;
    
    return 0;
}

\(43\)行的那个\(for\)循环里面,因为是从\(n\)\(1\),所以说\(v[i]\)\(j\)单调递减

又如果说\(a[j]\)不是单调递增,说明肯定$ \exists j,k \(, 满足\)a[j]<a[k],j<k\(,则对于\)dp[j]$而言,应该有更长的上升子序列

又因为希望求出在多种情况下补集的最小字典序,这样才能用k去减(\(54\)行),所以说是从大到小去扫(\(52,53\)行)

如果当前点为起始的\(LIS\)已经能覆盖\(k\)了,说明正确方案一定是以\(pos\)为起点的\(LIS\)的众多方案中的一种,故\(pos\)一定被保留,所以打上标记(\(57\)行),而后续选择保留的点一定在\(pos\)的右侧,所以\(pos\)左侧的点不应该能对答案产生影响,所以说清空\(dp[i]\)\(58\)行),因为选择了\(pos\),后续应该选择以\(pos\)为起点的\(LIS\)的一个子序列,即以\(pos\)右端点i为起始点的\(LIS\),所以这个\(LIS\)的长度应该是以\(dp[pos].len-1\),所以应该直接退出这一层的循环

posted @ 2025-05-31 11:38  shencheng4014  阅读(23)  评论(1)    收藏  举报