P8866 [NOIP2022] 喵了个喵

P8866 NOIP2022 喵了个喵 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

本题解中我们将图案为 \(x\) 的卡牌看做数字 \(x\),将本题对于卡牌的操作看做对数字的操作。

观察到数据范围,\(k \in \{2n - 2, 2n - 1\}\),那么做法肯定跟 \(k\) 强相关。很难不联想 2018 年的旅行。但是那个 \(m = n - 1\) 给了 60 分,这个呢?

\(k = 2n - 2\)

首先发现 \(op\) 的限制其实是假的:我们一定恰好进行了 \(m\) 次操作 \(1\),最多进行了 \(0.5m\) 次操作 \(2\),所以 \(op\) 一定天然满足 \(m \le op \le 1.5m\)

观察 \(k\)\(n\) 的关系,发现是近似二倍,而且 \(k = 2(n - 1)\),也就是我们如果把每两种数字考虑到一个栈,这样分配将恰好剩下一个空栈。

考虑将数字 \(2i - 1\)\(2i\) 分配到第 \(i\) 个栈中,第 \(n\) 个栈就可以作为空辅助栈了。稍作思考可以给出以下的解法:

遇到一个数字 \(x\),我们查看这个数字所分配到栈 \(s\) 的情况:

  • 如果 \(s\) 为空,直接入栈;
  • 如果 \(s\)\(1\) 个元素,和 \(x\) 相同,将 \(x\) 入栈 \(s\) 并将这两个 \(x\) 消除;
  • 如果 \(s\)\(1\) 个元素,和 \(x\) 不同,将 \(x\) 入栈 \(s\)
  • 如果 \(s\)\(2\) 个元素,栈顶和 \(x\) 相同,将 \(x\) 入栈 \(s\) 并将这两个 \(x\) 消除;
  • 如果 \(s\)\(2\) 个元素,栈顶和 \(x\) 不同,那么栈底一定和 \(x\) 相同。将 \(x\) 入辅助栈 \(n\),对栈 \(n\) 和栈 \(s\) 栈底相消。

容易看出,我们第偶数次遇到 \(x\) 的时候,总能立刻将它和上一个 \(x\) 抵消,所以一定能报告出解。

另外,可以注意到本题的操作只对栈顶和栈底有效,所以如果栈中某时元素很多就会造成大量的滞留,非常不优。这也和本做法中栈的大小始终不超过 \(2\) 有关系(达到 \(3\) 之后可以马上通过相消降为 \(2\))。

这样 15 分就到手了。是 60 分的四分之一。

\(k = 2n - 1\)

一种比较显然的想法是,对于数字 \(x \le 2n - 2\) 我们暂时仍然考虑上面的处理方式,然后考虑如何处理 \(x = 2n - 1\)。发现遇到 \(x = 2n-1\) 的时候局面的结构比较参差不定,不好想(后来发现好像也不是不可以,读者可以自己试试?)。

我选择了另一种想法,那就是抛开栈 \(i\) 和数字 \(2i\)\(2i - 1\) 的绑定关系,采用先来后到的思想,按照 \(k = 2n-2\) 的策略尽可能走下去。换句话说,当一个没出现在局面上的数即将来临时,临时给它绑定一个没有绑定满两个元素的栈(注意只有 \(n-1\) 个栈可用);当一个数在局面上消失时,立刻让它和栈失去绑定关系,然后接着使用 \(k = 2n-2\) 的策略。这时当走不下去的时候,一定是局面上的 \(n-1\) 个栈全部被填满各两个元素,每个元素恰出现一次,且接下来要填的数是另一个数 \(P\),此时局面结构就是固定的了。

提一嘴,我感觉在剩下的 85 分中,应该是有部分数据可以按照上述策略成功走完整个游戏的,毕竟随机数据下,出现走不下去的概率不是特别高(当然也不难构造)。所以也不失为一个骗分好办法。不过这题有多测,所以也有可能会卡。

直接把 \(P\) 放到辅助栈显然是错的:假如 \(P\) 后面跟了一个被压在栈底的数字,那么这两个数字可能很难再抵消,很不优。

我们考虑离线:也就是说,我们开始预知这个数后面都是什么数。依据后面数的情况,我们考虑 \(P\) 放在哪里。

考虑紧跟在 \(P\) 后方最长的一串数,满足这些数都在栈顶。如果不考虑当前 \(P\) 的去向,那么直接将这些数放到对应的栈,和栈顶相消即可,如果再来就仍然放置原来的栈顶。比如 \(5\) 是某个栈的栈顶,如果 \(P\) 后面来了个 \(5\) 就让它和 \(5\) 相消,如果再来一个 \(5\) 就放在原来的栈顶,如果再来就继续相消。因此我们发现,这些数还是相当自由的,我暂且称之为自由相消。

因此发现,如果 \(P\) 后面第一个不在栈顶上的数是 \(P\),那么我们就把当前的 \(P\) 放入辅助栈,然后接下来的数自由相消,最后在第二个 \(P\) 来时,将它也放入辅助栈,栈顶相消即可。这种情况比较简单。

现在讨论 \(P\) 后面第一个不在栈顶上的数是一个栈底 \(X\) 的情况。我们记其对应栈为 \(S\)。此时未来的数列是 \((P, \cdots, X)\),其中 \(\cdots\) 都是某个栈的栈顶。我们讨论 \(S\) 当前的栈顶 \(Y\)

  • 如果未来数列的 \(P\)\(X\) 之间,\(Y\) 出现了奇数次:把 \(P\) 放入辅助栈,接下来一直自由相消,直到将处理 \(X\) 为止。由于 \(Y\) 的数量是奇数,加上一开始的一个 \(Y\),此时 \(Y\) 一定被消除光,现在 \(S\) 的栈顶变成 \(X\),将 \(X\) 放入 \(S\) 栈顶相消,\(S\) 变成了空栈。
  • 如果 \(Y\) 出现了偶数次:把 \(P\) 放入 \(S\),未来所有不为 \(Y\) 的栈顶自由相消,对于 \(Y\),我们将其全部放入辅助栈相消,直到将处理 \(X\) 为止。由于 \(Y\) 的数量是偶数,这些 \(Y\) 一定都被消除光,辅助栈仍为空,我们将 \(X\) 放入辅助栈,和 \(S\) 栈底相消,辅助栈仍为空。

经过上述操作后,局面总会保持:

  • 存在一个空栈;
  • 除了某个数以外,其它数都在局面最多出现一次;
  • 所有栈最多只有两个元素。

直接让新的空栈作为辅助栈即可。

如何证明这种做法一定可以报告出解?其实很简单,只要不是在 \((P, \cdots, X)\) 的特殊处理过程期间,我们的操作总能保持让每个数最多只在局面出现一次;本题只能两两相消,每种数字数量均为偶数,因此最终每个数必定不会在局面中出现一次,也就是必定不会出现。

具体实现临时绑定和松绑时,用一个队列维护没有绑定满两个元素的栈的编号,当然,辅助栈不能入队。对于未绑满两个元素的栈,如果它绑定了一个元素,让它在队列中出现一次,否则如果没绑定元素,出现两次即可。当出现一个元素不在局面上,需要绑定栈,但队列为空时,证明我们要开始特殊操作了。

时间复杂度 \(\Theta(m)\)

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2022-12-04 01:03:11 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2022-12-04 13:35:14
 */
#include <bits/stdc++.h>
inline int read() {
    int x = 0;
    bool f = true;
    char ch = getchar();
    for (; !isdigit(ch); ch = getchar())
        if (ch == '-')
            f = false;
    for (; isdigit(ch); ch = getchar())
        x = (x << 1) + (x << 3) + ch - '0';
    return f ? x : (~(x - 1));
}

const int maxn = 305;
const int maxm = (int)2e6 + 5;

int a[maxm];
std :: deque <int> st[maxn];
int id[maxn * 2]; // 维护所在栈编号,局面未出现则为 0
bool top[maxn * 2]; // 维护是否在栈顶

typedef std :: pair <int, int> pii;
std :: vector <pii> ans;

inline void pu(int s) {
    ans.emplace_back(s, 0);
}

inline void de(int s, int t) {
    ans.emplace_back(s, t);
}

int spt; // 辅助栈编号
std :: queue <int> stq; // 维护空栈

inline bool simple(int x) { // 尝试简单相消
    int s = id[x];
    if (!s) {
        if (stq.empty())
            return false;
        id[x] = s = stq.front();
        stq.pop();
    }
    int z = st[s].empty() ? 0 : st[s].front();
    if (!st[s].empty() && x == st[s].back()) {
        pu(s);
        id[x] = 0;
        stq.push(s);
        top[z] = true;
        top[x] = false;
        // 注意上面两个语句顺序不能颠倒,因为 z 和 x 相等时我们期望其 top 为 false
        st[s].pop_back();
    }
    else if (st[s].size() < 2) {
        pu(s);
        st[s].push_back(x);
        top[z] = false;    
        top[x] = true;
        // 同样不能颠倒
    } else {
        pu(spt), de(spt, s);
        id[z] = 0;
        top[z] = false;
        st[s].pop_front();
        stq.push(s);
    }
    return true;
}

int main() {
    for (int T = read(); T; --T) {
        int n = read(), m = read(); read();
        for (int i = 1; i <= m; ++i)
            a[i] = read();
        std :: memset(id, 0, sizeof(id));
        ans.clear();
        spt = n;

        while (!stq.empty())
            stq.pop();
        for (int i = 1; i < n; ++i) {
            stq.push(i);
            stq.push(i);
        }
        
        for (int i = 1; i <= m; ++i) if (!simple(a[i])) {
            int p = a[i];
            int r = i + 1;
            for (; r <= m && top[a[r]]; ++r);
            // 此时 r 是 i 后第一个不在栈顶上的数
            int x = a[r];
            if (x == p) {
                pu(spt);
                for (int j = i + 1; j < r; ++j)
                    simple(a[j]);
                pu(spt);
            } else {
                int s = id[x], y = st[s].back();
                bool evn = true; // 中间栈顶中,y 的数量是否为偶数
                for (int j = i + 1; j < r; ++j)
                    if (a[j] == y)
                        evn = !evn;
                if (evn) {
                    pu(s);
                    st[s].push_back(p);
                    for (int j = i + 1; j < r; ++j) {
                        if (a[j] == y)
                            pu(spt);
                        else
                            simple(a[j]);
                    }
                    pu(spt);
                    de(spt, s);
                    st[s].pop_front();
                    // st[s] 从栈底到栈顶,原先为 x, y,现在为 y, p
                    // 依此更新 id 和 top
                    id[x] = 0;
                    id[p] = s;
                    top[y] = false;
                    top[p] = true;
                } else {
                    pu(spt);
                    st[spt].push_back(p);
                    for (int j = i + 1; j < r; ++j) {
                        if (a[j] == y)
                            pu(s); // 注意这里不要直接 simple(a[j])
                        // 原因是 s 即将变成 spt,所以暂时不能让 s 弹入 stq
                        // 特判让 simple 函数此时不把 s 不弹入 stq 也不行
                        // 假如 3 1 2 1 1 1 ....
                        // 如果直接不弹入 stq,会造成后面的一串 1 1 1 无法正确弹入 s
                        // 最后暴力扫队列把 s 删除复杂度也不对
                        // 所以正确的做法就是不用 simple 函数
                        else
                            simple(a[j]);
                    }
                    pu(s);
                    st[s].clear();
                    // 原先辅助栈 spt 此时存在一个元素 p
                    // s 原先栈底到栈顶为 x,y, 现在为空
                    // s 作为新的 spt
                    // 依此更新 id,top 和 stq
                    id[x] = id[y] = 0;
                    id[p] = spt;
                    top[y] = false;
                    top[p] = true;
                    stq.push(spt);
                    spt = s;
                }
            }

            i = r; // 注意循环会自带 ++i,下一次循环 i = r + 1
        }

        printf("%d\n", (int)ans.size());
        for (pii p : ans) {
            if (p.second)
                printf("2 %d %d\n", p.first, p.second);
            else
                printf("1 %d\n", p.first);
        }
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值