Codeforces gym102423 - J One of Each(贪心 + 栈)

传送门


题目大意

给出 n n n个数,仅包含 [ 1 , k ] [1,k] [1,k]中的数,且至少含有 k k k个不同的数。找到一个长度为 k k k的子序列是 k k k的一个排列且字典序最小。

解题思路

这个题算是比较难想的贪心了,而且要用栈维护。一开始用尺取发现维护不了,贪心的话自己又陷入了如下几个误区:

  1. 如果从后向前贪心,显然是可以的,但是对于已经选过的数,无法得知前缀中待选的数是否能全部出现,即使 b i t s e t bitset bitset维护也要爆内存。
  2. 如果用双端队列从前向后维护,没出现的就入队,如果出现了重复的数要遍历队列,找到他前面的逆序对(且在后面还会出现的)出队。但是这样维护的难度很大,而且时间复杂度无法保证,即无法保证一个数最多遍历、入队、出队一次。

正解:

从前向后维护序列,对于未入栈的数,在它入栈之前检查栈顶的元素是否会在后面出现而且大于它,不断检查直到栈顶元素不符合条件。其中后缀的元素是否出现只需要记录数目然后扫描的过程中不断减少即可。

解释:(参考了队友visors)

我们的解一定是在一个子区间中选出来的,那么当我们的区间向右扩大时,若该数字出现过,不用讨论,因为该数字即使在该处取,也没有在之前取到的地方取好(后面能接的数是之前的子集);若该数字没出现过,那么我们要考虑该数字能否在追求字典序最小的情况下优化当前解。根据字典序,当然是越小的数越出现在前面越好,所以我们可以对当前解从后往前检查,如果该数字比后端数字小,那么将它放在该位置显然会使字典序最小,但放的时候也要考虑后面会不会有机会再把这个后端元素取回来,如果取不回来,解最终就凑不齐 k k k 个了。当我们上浮至无法上浮的位置时,就接着去考虑下一个数的加入就行了。因为我们每次上浮都保证了去掉的后端元素存在于后面未取数字中,所以一次线性扫描一定能恰巧取出字典序最小的 k k k 个元素形成的序列。

#include <bits/stdc++.h>

using namespace std;
const int maxn = 2e5 + 10;

int a[maxn], cnt[maxn];
bitset<maxn> vis;
vector<int> q;

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        cnt[a[i]]++;
    }
    for (int i = 1; i <= n; i++) {
        cnt[a[i]]--;
        if (!vis[a[i]]) {
            while (!q.empty() && cnt[q.back()] && q.back() > a[i]) {
                vis[q.back()] = 0, q.pop_back();
            }
            q.push_back(a[i]);
            vis[a[i]] = 1;
        }
    }
    bool flag = 1;
    for (auto i : q) {
        if (flag)
            flag = 0, cout << i;
        else
            cout << " " << i;
    }
    cout << endl;
    return 0;
}

你可能感兴趣的:(#)