【清华集训 2014】玄学 (UOJ46)

题意

给定一个数列,一个取模用的数字 $m$,和一些形如 “ 将 $[l,r]$内的数 $x$变为 $(ax+b)\bmod m$” 这样的形式的操作。每次可能新增一个操作,或者询问:如果将已经出现的编号为 $[l,r]$的这些操作依次进行后,原始数列中第 $p$位的数字会变为多少。询问不会影响到原始数列。强制在线。数字个数 $n\leq 10^5$,新增操作 $m\leq 10^5$,询问个数 $q\leq 5\times 10^5$,下文将默认这些数字同阶。

初步分析

由于这些诡异的操作并不一定有逆,因此我们不能利用操作的前缀和得到答案。而且操作没有交换律,只有结合律。

这时,我们可能需要一些可以维护二维信息的数据结构。比如树套树什么的。

但是,如果在这道题上使用树套树,可能有点大材小用了。题目中的操作只会依次添加,不会修改之前的操作,并不是完整的二维信息问题。因此使用树套树意义不大。

所以,我们还可以用一些特殊的技巧解决这个问题。

在接下来的叙述中,将用函数 $f(x)$表示一个操作。

解法 1:分块的妙用

在数据结构题中,一个常见的技巧就是将修改按时间分块。比如每隔 $\sqrt n$的时间就将数据结构重建一遍。

这个技巧在这道题里也适用。我们可以每隔 $\sqrt n$的时间将新来的操作重建为一个块 (可以使用线段树),块内维护 $\forall {i\in[1,n]},f_i(x)$,也就是每个位置在这 $\sqrt n$个操作的作用下的变化。

对于每次询问,如果其包含某些整块,我们可以 $O(1)$直接取得一整块的 $f$,对于零散的部分,暴力判断每隔 $f$是否会对答案造成影响即可。

由于每个操作只会被所在的块重建一次,每次重建复杂度又是 $O(n)​$的,因此,时间复杂度为 $O(n\log n+n\sqrt n+q\sqrt n)​$,空间复杂度为 $O(n\sqrt n)​$,都略微有点大,在 UOJ 上只能得到 $80​$分。

想想哪里对时间复杂度有浪费,我们发现,有两点:

  • 1. 由于每个块内只有 $\sqrt n​$个操作,本质不同的 $f_i(x)​$实际上只有 $O(\sqrt n)​$个,因此对于 $\forall {i\in [1,n]}​$都计算 $f_i(x)​$有点浪费。
  • 2. 如果我们将块分的很大,对于整块的询问将很快,但是零散部分的询问很慢。块很小,两者也会一大一小。这样很难均衡。

对于第二个问题有一个比较好的优化。我们可以同时维护两个分块,一大一小。整块用大的询问,零散部分用小的询问,岂不是很爽。这样,由于时间复杂度的上限没有变 (因为空间复杂度为 $O(n^2/s)$导致小块大小 $s$不能太小),我们还是认为时间复杂度为 $O(n\sqrt n)$。

然而,经过尝试,小块大小为 $180$,大块大小为 $4680$时,算法在随机数据下的效率最高,已经可以通过 UOJ 的全部数据。

再考虑能否通过优化第一个问题降低复杂度。显然是可以的,并且这样一来,空间复杂度也降了下来,我们甚至可以结合第二种方法达到更低的复杂度。

考虑到每一块内本质不同的 $i$不多,只有 $O(s)$个,而且都是连续的,我们就可以直接维护这些连续段。虽然说这样会导致复杂度套上一个 $\log$,但是接下来我们可以做的 (理论上可行) 事情将使复杂度远远降低,弥补这一个 $\log$带来的影响。

我们可以效仿第一种方法, 将小块大小设为 $S$,大块大小设为 $T\times S$,在小块中,由于块数较多,需要维护本质相同的连续段,而大块则不需要。因此单次询问的时间复杂度为:
$$
O(S+T\log N+\frac{N}{ST})
$$
最小化该函数,为华丽的 $O(N^{\frac{1}{3}}\log^{\frac{1}{3}}N)​$,比普通分块的 $O(N^\frac{1}{2})​$看上去好看多了。

当然,这还不是这种方法最牛逼的地方。如果多套几层分块,(比如 $k$层),算法的复杂度将会是 (大约是,具体是什么取决于多少层分块维护了本质相同的连续段)$O(kN^\frac{1}{k+1}\log_2^\frac{k}{2(k+1)}N)$,当 $k$取 $8$或 $7$的时候取到最小值。添加操作的单次复杂度为 $O(k\log N)$,并不是瓶颈。

解法 2:时间线段树

上一种分块方法虽然很妙,但是有点难以实现。其中两层分块的做法我用了 165 行实现。如果分块层数更多,可能 165 行之内难以解决。

我们可以继续挖掘 “本质不同的连续段很少” 这个性质,将分块算法优化成一个全新的算法。如果我们每次不重新建立一个新块,而是将相邻两个块合并成一个新块,用类似线段树的结构维护合并的过程,将会更方便。

我们将每个新的操作添加到一棵时间线段树的叶子节点处。如果一个节点满了 (即保存的操作个数等于区间长度),则将它的操作上传到父亲节点。每次询问则像普通线段树那样,在每个节点处的 “本质不同的连续段” 内二分查找对应位置的答案即可。

这样实现的时间复杂度为 $O(n\log n+q\log^2n)$

代码

下面给出分块做法 (仅对上文提到的第二个问题进行了优化)

#include <algorithm>
#include <iostream>
#include <assert.h>
#include <cstring>
#include <cstdio>
#include <cmath>
#define MX 100005
#define BS 180
#define MUL 26

using namespace std;

template <typename T> void read(T& x)
{
    x = 0; char c = getchar(); bool f = 0;
    while(!isdigit(c) && c!='-') c = getchar();
    if(c == '-') f = 1, c = getchar();
    while(isdigit(c)) x = x*10+c-'0', c = getchar();
    if(f) x = -x;
}

int type, n, m, q;

struct NODE
{
    int a, b;

    NODE (const int &a0 = 1, const int &b0 = 0) : a(a0), b(b0) {}
    NODE f(const NODE &t) const {return NODE(1ll*a*t.a%m, (1ll*b*t.a+t.b)%m);}
    inline void f_(const NODE &t) {a = 1ll*a*t.a%m, b = (1ll*b*t.a+t.b)%m;}
};

struct SEGT
{
    #define ls (a<<1)
    #define rs (a<<1|1)
    #define mid ((l+r)>>1)

    NODE tre[MX*4];

    void psd(int a)
    {
        tre[ls].f_(tre[a]);
        tre[rs].f_(tre[a]);
        tre[a] = NODE();
    }

    void add(int a, int l, int r, int ql, int qr, const NODE &x)
    {
        if(ql<=l && r<=qr) tre[a].f_(x);
        else if(ql>r || qr<l) return;
        else psd(a), add(ls, l, mid, ql, qr, x), add(rs, mid+1, r, ql, qr, x);
    }

    void get(int a, int l, int r, NODE *seq)
    {
        if(l == r) seq[l] = tre[a];
        else psd(a), get(ls, l, mid, seq), get(rs, mid+1, r, seq);
    }

    void build(int a, int l, int r)
    {
        tre[a] = NODE();
        if(l != r) build(ls, l, mid), build(rs, mid+1, r);
    }

    #undef ls
    #undef rs
    #undef mid
} T;

struct BLOCK
{
    NODE opr[MX], stk[BS*MUL+1];
    int sl[BS*MUL+1], sr[BS*MUL+1];
    int top;

    NODE qur(int l, int r, int p)
    {
        NODE ret;
        for(int i=l; i<=r; i++)
            if(sl[i]<=p && p<=sr[i])
                ret.f_(stk[i]);
        return ret;
    }

    bool push(const NODE &x, int l, int r, int lim)
    {
        stk[++top] = x;
        sl[top] = l;
        sr[top] = r;
        if(top == lim)
        {
            T.build(1, 1, n);
            for(int i=1; i<=top; i++) T.add(1, 1, n, sl[i], sr[i], stk[i]);
            T.get(1, 1, n, opr);
            return true;
        }
        return false;
    }
} B[MX/BS+2], B2[MX/BS/MUL+2];

int val[MX];

void input()
{
    read(type); read(n); read(m);
    for(int i=1; i<=n; i++) read(val[i]);
    read(q);
}

inline int block(int x, int s) {return (x-1)/s+1;}
inline int bl(int x, int s) {return s*x-s+1;}
inline int br(int x, int s) {return s*x;}

void work()
{
    int cmd, a, b, c, d, lans = 0;
    int nowb = 1, nowb2 = 1;
    for(int i=1; i<=q; i++)
    {
        read(cmd);
        if(cmd == 1)
        {
            read(a); read(b); read(c); read(d);
            if(type&1) a ^= lans, b ^= lans;
            nowb += B[nowb].push(NODE(c, d), a, b, BS);
            nowb2 += B2[nowb2].push(NODE(c, d), a, b, BS*MUL);
        }
        else
        {
            read(a); read(b); read(c);
            if(type&1) a ^= lans, b ^= lans, c ^= lans;
            NODE x(0, val[c]);
            int ba = block(a, BS), bb = block(b, BS);
            int ba2 = block(a, BS*MUL), bb2 = block(b, BS*MUL);
            if(ba == bb) x.f_(B[ba].qur(a-bl(ba, BS)+1, b-bl(ba, BS)+1, c));
            else
            {
                x.f_(B[ba].qur(a-bl(ba, BS)+1, BS, c));
                if(ba2 < bb2-1)
                {
                    for(int i=ba+1; i<=block(br(ba2, BS*MUL), BS); i++) x.f_(B[i].opr[c]);
                    for(int i=ba2+1; i<=bb2-1; i++) x.f_(B2[i].opr[c]);
                    for(int i=block(bl(bb2, BS*MUL), BS); i<=bb-1; i++) x.f_(B[i].opr[c]);
                }
                else
                {
                    for(int i=ba+1; i<=bb-1; i++) x.f_(B[i].opr[c]);
                }
                x.f_(B[bb].qur(1, b-bl(bb, BS)+1, c));
            }
            lans = x.b;
            printf("%d\n", lans);
        }
    }
}

int main()
{
    input();
    work();
    return 0;
}
分类: 文章

2 条评论

foreverpiano · 2019年4月1日 2:56 下午

二进制分组? 只不过建成线段树结构

    boshi · 2019年4月1日 8:22 下午

    对,就是这样

发表评论

电子邮件地址不会被公开。 必填项已用*标注