CF1142B Lynyrd 和 Skynyrd

1 CF1142B Lynyrd 和 Skynyrd

2 题目描述

最近,\(Lynyrd\)\(Skynyrd\) 去了一家商店,\(Lynyrd\) 在那里买了一个长度为 \(n\)\(p\) 排列,\(Skynyrd\) 买了一个由 \(1\)\(n\) 的整数构成,长度 \(m\) 的数组 \(a\)

\(Lynyrd\)\(Skynyrd\) 很无聊,所以他们问了 \(q\) 个问题,问题都如以下形式:“从 \(l\)\(r\) 的区间内是否存在一个子段,是 \(p\) 的循环变换?” 请你回答他们的问题。

题目中长度为 \(n\)\(p\) 排列是一个 \(1\)\(n\) 的全排列,也就是说,在该排列中,\(1\)\(n\) 中的每个整数都以任意顺序出现且仅出现一次。

\(p\) 数组的循环变换是指形如 \(p_i\),\(p\)\(_i\)\(_+\)\(_1\)\(,...,\)\(p_n\),\(p_1\),\(p_2\),\(p_3\),...,\(p\)\(_i\)\(_-\)\(_1\) 的数列(\(i\in[1,n]\))。例如,数组有三个不同的循环变换:\((2,1,3)\),\((1,3,2)\),\((3,2,1)\)

数组 \(a\)\(l\)\(r\) 这一区间的子段是一个形如 \(a\)\(_i\)\(_1\),\(a\)\(_i\)\(_2\),...,\(a\)\(_i\)\(_k\) 的数列,其中的 \(i_1\),\(i_2\),...,\(i_k\) 满足 \(l≤\)\(i_1\)\(<\)\(i_2\)\(<...<\)\(i_k\)\(≤r\)

3 CF1142B LS 题解

这道题有许多难点,我们分别讨论。

首先,我们来解决关于循环变换的问题。我们发现:无论给出的 \(p\) 数组再怎么变换,每一个数的后继(也就是下一个数)是不变的。根据这个性质,我们想到:对于每一个数属于 \(a\) 数组的数,我们可以通过不断寻找其后继的方法将整个序列确定出来。举个例子,如果 \(p\) 数组为 \(2, 1, 3\)\(a\) 数组为 \(1, 3, 2, 1, 2, 3\)。那么我们首先找出后继:对于 \(2\) 来说,我们要找的值为 \(1\);对于 \(1\) 来说,我们要找的值为 \(3\);对于 \(3\) 来说,我们要找的值为 \(2\)。接下来,我们根据这个寻找后继的规律,将 \(a\) 数组所有数的最靠前的后继的坐标都找出来(没有找到后继的表示为 \(x\) ):\(2, 3, 4, x, x, x\)。根据这个,我们想到:如果记一个数的后继为 \(1\) 级后继,这个数后继的后继记为 \(2\) 级后继,那么如果我们找到了这个数的 \(n-1\) 级后继,我们就确定了一个 \(p\) 数组的循环变换。我们可以把这个 \(i\) 级后继存储到一个数组 \(nxt\) 中,以便计算。

接下来,我们来考虑如何计算 \(nxt\) 数组。根据上面的定义,我们可以初步定义为如下 \(nxt\) 数组:\(nxt_{i, j}\) 表示第 \(i\) 个数的 \(j\) 级后继的坐标。我们发现,一个数的 \(2\) 级后继,为这个数的 \(1\) 级后继的 \(1\) 级后继。据此,我们推出转移方程:\(nxt_{i, j} = nxt_{nxt_{i, j-1}, 1}\)。容易发现,这样的时间复杂度为 \(O(n^2)\),无法通过此题。我们仔细观察性质,发现:一个数的 \(2^x\) 级后继为其 \(2^{x-1}\) 级后继的 \(2^{x-1}\) 级后继。这个转移方程特别像我们在解决最近公共祖先 \(LCA\) 问题时的转移方程,因此,我们可以设计出同样的状态:\(nxt_{i, j}\) 表示第 \(i\) 个数的 \(2^j\) 级后继的坐标。转移方程根据上文便可得知:\(nxt_{i, j} = nxt_{nxt_{i, j-1}, j-1}\)。到了这里,我们就可以利用倍增求出 \(nxt\) 数组了。但此时仍旧有个问题:如何计算 \(nxt_{i, 0}\)?此时我们回到后继的定义上,我们要找的是最靠前的后继,意思就是该后继的坐标在没有小于第 \(i\) 个数的坐标的情况下最小。那么如何保证这一点呢?其实很简单,我们倒着处理整个 \(a\) 数组即可。更具体地,我们对于第 \(i\) 个数,先找到其应该寻找的数 \(x\),然后找到坐标最靠前的 \(x\)。但是如何找到最靠前的 \(x\) 呢?我们在处理时,用数组 \(s\) 存储对于每一个数 \(x\),其最靠前的坐标。即 \(s_i\) 表示所有值为 \(i\) 的数中,坐标最靠前的数的坐标。很容易想到,我们在处理第 \(i\) 个数时,可以将 \(s_{a_i}\) 的值改为 \(i\),此时的 \(i\) 肯定为满足条件的最小的坐标:如果有数的坐标比它更小,那么这个更小的坐标肯定比 \(i\) 小,从而不满足坐标比 \(i\) 大这一条件。综上所述,\(nxt\) 数组的计算可以在 \(O(n*log_2n)\) 的时间复杂度内计算出。

然后,我们来看如何找到 \(n-1\) 级后继。由于我们 \(nxt\) 数组存储的后继都是 \(2\) 的整数次幂的后继的坐标,那么有极大的概率我们没法直接统计出 \(n-1\) 级后继。如何解决这个问题呢?在有关 \(2\) 的处理中,起到重要作用的一种操作就是二进制拆分。对于这一题也是如此,我们可以将 \(n-1\) 这个数进行二进制拆分。跟快速幂一样,如果在二进制中从左往右数第 \(k\) 位是 \(1\),那么我们将坐标变为当前坐标的 \(2^{k-1}\) 级后继。如此跳转 \(log_2n\) 次后,我们就可以求出第 \(i\) 个数的 \(n-1\) 级后继:坐标从 \(i\) 开始,将 \(n-1\) 进行二进制拆分后计算出第 \(i\) 个数的 \(2^{k_1 - 1}\) 级后继的 \(2^{k_2-1}\) 几后继 \(...\)\(2^{k_t - 1}\) 级后继。这个值就是第 \(i\) 个数的 \(n-1\) 级后继。

最终,我们来回答题目问的问题。看到了 \(l, r\) 之后,相信很多人脑中都会蹦出数据结构解法。我们结合上文的所有铺垫,来看看能否解决问题。首先,我们用 \(End\) 数组存储所有数的 \(n-1\) 级后缀,如果不存在该后缀则赋为极大值。\(End\) 数组的含义便是:对于第 \(i\) 个数,一个完整的循环变换的终点坐标在哪里。我们思考:如果保证 \(i \in[l, r]\),那么只要存在一个 \(End_i\) 满足 \(End_i \leq r\),这个区间内就一定存在一个循环变换。我们思考一下:最有可能满足条件的 \(End\) 值一定是 \(i \in [l,r]\)\(End_i\) 的最小值。因此,我们得到了本题的解法:利用线段树或者 \(ST\) 表维护区间最小值,元素就是所有 \(End_i\)。每次查询可以看作询问 \([l,r]\) 区间的最小值是否小于等于 \(r\)。这里我使用的是线段树。

注意:在计算 \(nxt\)时一定要先循环遍历 \(j\),再循环遍历 \(i\),这是因为如果先循环遍历 \(i\) 的话,坐标大于 \(i\) 的数的 \(nxt\) 数组还没有计算完毕,会导致在 \(j\) 逐渐变大的时候 \(nxt_{i,j-1}\) 这一值因为没有被计算过而被视为极大值,从而导致总体计算出现严重错误。

4 代码(空格警告):

#include <iostream>
#include <cstring>
using namespace std;
const int N = 2e5+10;
int n, m, q;
int a[N], b[N], nxt[N][20], s[N], nxt2[N], li, ri, fans[N];
long long End[N];
struct node
{
    int l, r, minn;
}t[N*4];
void build(int p, int l, int r)
{
    t[p].l = l;
    t[p].r = r;
    if (l == r)
    {
        t[p].minn = End[l];
        return ;
    }
    int mid = (l+r)/2;
    build(p*2, l, mid);
    build(p*2+1, mid+1, r);
    t[p].minn = min(t[p*2].minn, t[p*2+1].minn);
}
int query(int p, int l, int r)
{
    if (t[p].l >= l && t[p].r <= r) return t[p].minn;
    int mid = (t[p].l + t[p].r) / 2;
    int ans = 0x3f3f3f3f;
    if (l <= mid) ans = min(ans, query(p*2, l, r));
    if (r > mid) ans = min(ans, query(p*2+1, l, r));
    return ans;
}
int get(int x, int t)
{
    int ans = x, cnt = 0;
    if (t == 9 && x == 1)
    {
        while (t)
        {
            if (t & 1)
            {
                ans = nxt[ans][cnt];
            }
            if (ans == 0x3f3f3f3f) break;
            t >>= 1;
            cnt++;
        }
        return ans;
    }
    while (t)
    {
        if (t & 1) ans = nxt[ans][cnt];
        if (ans == 0x3f3f3f3f) break;
        t >>= 1;
        cnt++;
    }
    return ans;
}
int main()
{
    memset(nxt, 0x3f, sizeof(nxt));
    memset(s, 0x3f, sizeof(s));
    memset(End, 0x3f, sizeof(End));
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        nxt2[a[i]] = i+1;
    }
    nxt2[a[n]] = 1;
    for (int i = 1; i <= m; i++) cin >> b[i];
    for (int i = m; i >= 1; i--)
    {
        int x = a[nxt2[b[i]]];
        nxt[i][0] = s[x];
        s[b[i]] = i;
    }
    for (int j = 1; j <= 18; j++)
    {
        for (int i = 1; i <= m; i++)
        {
            if (nxt[i][j-1] == 0x3f3f3f3f) continue;
            nxt[i][j] = nxt[nxt[i][j-1]][j-1];
        }
    }
    for (int i = 1; i <= m; i++) End[i] = get(i, n-1);
    build(1, 1, m);
    for (int i = 1; i <= q; i++)
    {
        cin >> li >> ri;
        if (query(1, li, ri) <= ri) fans[i] = 1;
    }
    for (int i = 1; i <= q; i++) cout << fans[i];
    return 0;
}

欢迎关注我的公众号:智子笔记

posted @ 2021-02-05 15:51  David24  阅读(66)  评论(0编辑  收藏  举报