[THUPC 2023 决赛] 喵了个喵 III

· · 题解

此题解篇幅太长,请谨慎观看!

题目大意

解题思路

这题可用区间 dp 解决。

容易发现,第一种操作的后半句没什么用,因为可以改写成第二种操作来实现。

于是我们把相同的两个数放入不同的栈中,也避免了第一种操作的自动消除。

考虑牌堆中的一段区间 [l, r]。内部可以消去的肯定要消去,剩下的是不能消去的数,且按原牌堆顺序排列。

如图:(图中一种颜色的球代表一个数)

当然,也不一定像这样所有一起加入后再消除。

在 dp 时我们枚举 [l, r] 内最后消去的数(可能一个或两个),然后分段处理。

我们把 [1, l - 1] 中能和 [l, r] 中的数配对的数称为「有用的数」。

这里有一条非常重要的性质:

「有用的数」一定都(要)堆在同一个栈的最顶部。

我们把这个栈称为「有用的栈」。如果没有「有用的数」则认为两个栈都是「有用的栈」。

这条性质的证明将会在「区间 dp」部分的末尾给出。请读者时刻牢记这一性质。

Part 1:预处理

在 dp 之前,我们需要预处理几个数组:p, lx, ry, xl, ly, yr

这样我们就完成了所有的预处理。

Part 2:区间 dp

状态设计

f _ {l, r} 为当一个栈是「有用的栈」时,能否删完 [l, r] 内配对的部分并把剩下的堆在另一个栈。

同理记 g _ {l, r} 为当一个栈是「有用的栈」时,能否删完 [l, r] 内配对的部分并把剩下的堆在同一个栈。

考虑到可能没有剩余的,记 both _ {i, j} 为当一个栈是「有用的栈」时,能否删完 [l, r] 这部分。

容易发现 both 其实是 fg 共同的特殊情况。

注意「状态转移」部分中分界点的同 / 另一个栈是相对整个区间而言,而子区间的同 / 另一个栈是对子区间而言。

由于题目还要求具体方案,需要存三个数组 pf, pg, pboth,来记录转移的信息(具体见「状态转移」部分)。

根据「状态转移」部分可知,最终答案即 both _ {1, n} 的过程仅与 f, g, both 有关,故只设计设三种状态即可。

初始化

初始化 f _ {i + 1, i} = g _ {i + 1, i} = both _ {i + 1, i} = 00 \le i \le n)即可。

状态转移

容易发现只有这几种情况,分别转移即可。

对于第二、四、五种情况,其实如果预处理 ly _ {i, i - 1},那么前提两个条件中的第一个条件可以略去。

因为当 xl _ {l, i} = l 时,ly _ {l, i} 一定等于无穷大,也就是满足 ly _ {l, i} > r只是作者太懒了代码不想改 qwq。

目标状态

如果 both _ {1, n} = 1,则有解(输出 Cleared.);否则无解(输出 No solution)。

关于 both 数组的补充说明

很显然,当 [l, r] 内没有剩余即 lx _ {l, r} > r 时,both 数组取 fg 任意一个都行(相当于把空集堆在任意一个栈)。

因此我们转移时可以把 both 都换成 f,转移后再把 g \leftarrow f 就可以了。

记录上一个状态的数组 pboth 也可换成 pf,转移后再把 pg \leftarrow pf 即可。

关于性质的粗略证明

证明:

考虑用数学归纳法。命题对于 [1, n] 显然成立(都没有「有用的数」)。

设命题对于 [l, r] 成立,接下来证明命题对于由它分割的一个子区间 [l ^ \prime, r ^ \prime] 依然成立。

对于第一种情况(g = f + f),[l, r] 的「有用的数」都是 [l, i - 1] 的,命题对于 [l, i - 1] 显然成立;

而前提保证 [i + 1, r] 的「有用的数」都在 [l, i - 1] 中被堆在了另一个栈且只剩下「有用的数」,命题也成立。

对于第二种情况(f = g + f),根据我们在第二种情况时的讨论,命题对于 [l, i - 1] 显然成立;

[i + 1, r] 的「有用的数」要么就在之前「有用的栈」的最顶部,要么就在 [l, i - 1] 剩下的当中,命题也成立。

因为前提保证了 [l, i - 1] 只会剩下「有用的数」,所以「有用的数」仍在同一个栈的最顶部。

对于第三种情况(both = f + f + both),同第一种情况,命题对于 [l, i - 1] 显然成立;

命题对于 [i + 1, p _ i - 1] 同样成立,理由同第一种情况;

前提保证了 [1, l - 1][l, i - 1] 部分都没有 [p _ i + 1, r] 的「有用的数」,所以「有用的数」只会在 [i + 1, p _ i - 1] 当中,命题也成立。

因为前提保证了 [p _ i + 1, r] 只会剩下「有用的数」,所以「有用的数」仍在同一个栈的最顶部。

对于第四种情况(both = g + f + both),同第二种情况,命题对于 [l, i - 1] 显然成立;

命题对于 [i + 1, p _ i - 1] 同样成立,理由同第二种情况;

对于 [p _ i + 1, r] 这部分同第三种情况,命题也成立。

对于第五种情况(both = g + both),同第二种情况,命题对于 [l, i - 1] 显然成立;

命题对于 [i + 1, r] 同样成立,理由同第二种情况。

证毕。

Part 3:求具体方案

首先容易发现 op = \frac 3 2 n,因为每个数都被进行了一次加入操作,两个数一组被进行了一次消除操作。

查找每个数放入的栈

gt _ i 为第 i 个数应该放入的栈。gt _ i = 0 表示放入栈 1gt _ i = 1 表示放入栈 2

pre _ {l, r, w} = \begin {cases} pf _ {l, r} & (w = 1) \\ pg _ {l, r} & (w = 0) \end {cases},可以有效简化步骤。

特别提醒:根据「关于 both 数组的补充说明」,此时 both 数组已并入 fg 中, pboth 数组已并入 pfpg 中。

考虑递归查找。需要传入四个参数:l, r, w, d,表示当前递归到区间 [l, r],是第 w 种转移方式,同一个栈为栈 d

这里 w = 1 表示转移方式 fw = 0 表示转移方式 gd = 0 表示栈 1d = 1 表示栈 2

查找的入口为区间 [1, n],我们钦定 w = d = 0

如果当前递归查找的区间为 [l, r],是第 w 种转移方式,同一个栈为栈 d + 1,分几种情况:

模拟具体操作

得出 gt 数组后,我们从前往后遍历每一个数。设当前遍历到位置 i,数为 A _ i

首先如果 gt _ i = 0,那么输出 1;否则输出 2。然后把 A _ i 压入对应的栈中。

在栈顶数字相同的情况下,一直输出 0 并弹出两边栈顶。

于是我们就可以愉快地搞定一道黑题啦。下面附上 AC 代码:

AC Code

#include <cstdio>
#include <cstring>
#define inf 0x3f3f3f3f
#define N 1505
#define min(x,y) ((x)<(y)?(x):(y))
using namespace std;

int n, a[N], l[N], p[N], gt[N], pf[N][N], pg[N][N];
int lx[N][N], xl[N], ly[N], tr[N];
int top[2], st[2][N]; // 数组模拟栈
bool tmp, f[N][N], g[N][N];

int &pre (int i, int j, bool k) // 合并两个数组为一个函数
{
    return k ? pf[i][j] : pg[i][j];
}

// 这里的树状数组写法有些不同,实现的是单点修改,区间(后缀)询问

void modify (int p, int c) // 树状数组加入元素
{
    for (; p; p &= p - 1) tr[p] = min (tr[p], c);
    return ;
}

int ask (int p) // 树状数组询问
{
    int res = inf;
    for (; p <= n; p += p & -p) res = min (res, tr[p]);
    return res;
}

void dfs (int l, int r, bool w, bool d) // dfs 求具体方案
{
    if (l > r) return ;
    if (lx[l][r] <= r)
    {
        gt[lx[l][r]] = w ^ d;
        dfs (l, lx[l][r] - 1, !w, d);
        dfs (lx[l][r] + 1, r, 1, !w ^ d);
    }
    else
    {
        if (pre (l, r, w) > 0)
        {
            gt[pre (l, r, w)] = d;
            gt[p[pre (l, r, w)]] = !d;
            if (pre (l, r, w) < p[pre (l, r, w)])
            {
                dfs (l, pre (l, r, w) - 1, 1, d);
                dfs (pre (l, r, w) + 1, p[pre (l, r, w)] - 1, 1, !d);
                dfs (p[pre (l, r, w)] + 1, r, 1, d);
            }
            else
            {
                dfs (l, p[pre (l, r, w)] - 1, 0, d);
                dfs (p[pre (l, r, w)] + 1, pre (l, r, w) - 1, 1, d);
                dfs (pre (l, r, w) + 1, r, 1, !d);
            }
        }
        else
        {
            gt[-pre (l, r, w)] = !d;
            dfs (l, -pre (l, r, w) - 1, 0, d);
            dfs (-pre (l, r, w) + 1, r, 1, d);
        }
    }
    return ;
}

int main ()
{
    scanf ("%d", &n), f[1][0] = g[1][0] = 1; // 初始化
    for (int i = 1; i <= n; i ++)
    {
        scanf ("%d", &a[i]);
        if (l[a[i]]) p[l[a[i]]] = i, p[i] = l[a[i]]; // 预处理
        l[a[i]] = i, f[i + 1][i] = g[i + 1][i] = 1; // 初始化
    }
    for (int r = 1; r <= n; r ++)
    {
        lx[r + 1][r] = r + 1; // 初始化
        for (int l = r; l; l --) // 预处理
        {
            lx[l][r] = lx[l + 1][r];
            if (p[l] > r) lx[l][r] = l;
        }
    }
    for (int l = n, ry = 0; l; l --, ry = 0)
    {
        xl[l - 1] = l, memset (tr, 0x3f, sizeof tr); // 初始化
        for (int r = l; r <= n; r ++) // 预处理
        {
            xl[r] = min (xl[r - 1], p[r]);
        }
        for (int r = n; r >= l; r --) // 预处理
        {
            ly[r] = ask (xl[r]);
            if (p[r] < l) modify (p[r], r);
        }
        for (int r = l, now; r <= n; r ++) // 区间 dp
        {
            if (p[r] < l) ry = r; // 顺便预处理
            if (lx[l][r] <= r)
            {
                now = lx[l][r];
                if (ry < now) g[l][r] = f[l][now - 1] & f[now + 1][r]; // 第一种情况
                if (xl[now] == l || r < ly[now]) // 第二种情况
                {
                    f[l][r] = g[l][now - 1] & f[now + 1][r];
                }
            }
            else
            {
                for (int i = l, yr = l; i <= r; i ++)
                {
                    if (p[i] < yr) continue;
                    if (ry < i) // 第三种情况
                    {
                        tmp = f[l][i - 1] & f[i + 1][p[i] - 1] & f[p[i] + 1][r];
                        if (f[l][r] = tmp) {pf[l][r] = i; goto skip;}
                    }
                    if ((xl[i] == l || r < ly[i]) && ry < p[i]) // 第四种情况
                    {
                        tmp = g[l][i - 1] & f[i + 1][p[i] - 1] & f[p[i] + 1][r];
                        if (f[l][r] = tmp) {pf[l][r] = p[i]; goto skip;}
                    }
                    yr = p[i]; // 顺便预处理
                }
                if (xl[r] < l)
                {
                    now = p[xl[r]];
                    if (xl[now - 1] == l || r < ly[now - 1]) // 第五种情况
                    {
                        tmp = g[l][now - 1] & f[now + 1][r];
                        if (f[l][r] = tmp) pf[l][r] = -now;
                    }
                }
                skip:; g[l][r] = f[l][r], pg[l][r] = pf[l][r]; // 关于 Both 的处理
            }
        }
    }
    puts (f[1][n] ? "Cleared." : "No solution.");
    if (!f[1][n]) return 0;
    printf ("%d\n", n / 2 * 3), dfs (1, n, 0, 0);
    for (int i = 1; i <= n; i ++) // 模拟
    {
        putchar ('1' + gt[i]), st[gt[i]][++ top[gt[i]]] = a[i];
        while (top[0] && top[1] && st[0][top[0]] == st[1][top[1]])
        {
            putchar ('0'), top[0] --, top[1] --;
        }
    }
    return 0;
}

题解若有问题欢迎在评论区反馈。