P4117 [Ynoi2018] 五彩斑斓的世界

P4117 [Ynoi2018] 五彩斑斓的世界

给你一个长为 n n n 的序列 a a a,有 m m m 次操作:

  1. 把区间 [ l , r ] [l,r] [l,r] 中大于 x x x 的数减去 x x x
  2. 查询区间 [ l , r ] [l,r] [l,r] x x x 的出现次数。

1 ≤ n ≤ 1 0 6 , 1 ≤ m ≤ 5 × 1 0 5 , 0 ≤ a i , x ≤ 1 0 5 + 1 1 \leq n \leq 10^6,1 \leq m \leq 5\times 10^5,0 \leq a_i,x \leq 10^5+1 1n106,1m5×105,0ai,x105+1,时限 7.50 s 7.50\text{s} 7.50s,空限 64 MB 64\text{MB} 64MB

sol

「突刺贯穿第二分块」。

难度评分: 6 6 6

下面先假定 V V V 为值域。

先是分块照常套路,对于散块直接暴力,下面只考虑整块。

看一下询问,发现,把大于 x x x 的数减去 x x x,也就是把大于 x x x,小于等于区间最大值 m a x x maxx maxx 的值 p p p 更改为 p − x p-x px

对于每一个整块建立一个并查集即可。

但是直接更改值复杂度是错的。

考虑 m a x x = 100001 maxx=100001 maxx=100001 x = 1 x=1 x=1 时,需进行 100000 100000 100000 次操作。

再想一想,我们发现,把大于 x x x 的值减去 x x x,等价于把所有小于等于 x x x 的数全部加上 x x x 并区间减 x x x

考虑综合上述两种方法。

分治。

  • 如果 m a x x ≤ 2 ⋅ x maxx \leq 2 \cdot x maxx2x,使用第一种方法把大于 x x x 小于等于 m a x x maxx maxx 的数 i i i 改为 i − x i-x ix,复杂度为 O ( x ) \mathcal O(x) O(x)

  • 如果 m a x x > 2 ⋅ x maxx> 2 \cdot x maxx>2x,使用第二种方法把小于等于 x x x 的数 i i i 改为 i + x i+x i+x 并打上区间减 x x x 的标记,复杂度为 O ( m a x x − x ) \mathcal O(maxx-x) O(maxxx)

我们发现 m a x x maxx maxx 是单调不增的,所以这部分是单块是 O ( V ) \mathcal O(V) O(V) ,所有块是 O ( n V ) \mathcal O(\sqrt{n}V) O(n V) 的。

再看回并查集部分,

对于并查集,我们修改的时候,只需要把修改前值对应的并查集的根,连到修改后的值的并查集的根(记录的是块中第一次出现这个值的下标)上即可。同时我们需要记录每个数的出现次数,修改的时候直接加过去就好了。

再设一个 v a l [ i ] val[i] val[i],表示位置 i i i 的值,我们是方便通过并查集的根还原数组。

同时我们发现一个值被修改后,原值就消失了,因此这里并查集并不会进行路径压缩,是 O ( 1 ) \mathcal O(1) O(1) 的。

假设我们跑到一个位置,值为 x x x,下标为 i i i,分类讨论:

  • 若无 x x x,跳过即可。
  • 若无 y y y,将 v a l [ i ] val[i] val[i] 改为 y y y,并设 y y y 的根为 i i i
  • 若有 y y y,就把这个位置的根设为 y y y 的根(也就是之前出现过的 y y y 的位置)。

这部分是 O ( 1 ) \mathcal O(1) O(1) 的。

再回忆一下处理步骤:

  1. 对序列分块预处理。
  2. 对于一个块的全局修改、全局查询,按上述方法做即可,总时间复杂度为 O ( V n ) \mathcal O(V \sqrt{n}) O(Vn )
  3. 对于一个块的部分修改,我们先暴力把每个位置的实际值还原,然后对块进行重构即可,单次为 O ( n ) O\mathcal (\sqrt n) O(n )
  4. 对于一个块的部分查询,我们直接用并查集找到每个位置的实际值,然后判断是否相等即可,单次不超过 O ( n ) O(\sqrt n) O(n )

故总时间复杂度不超过 O ( V n + m n ) \mathcal O(V \sqrt{n} + m\sqrt{n}) O(Vn +mn )

再分析空间复杂度,我们需要对每个块记录每个数的出现次数,那么至少需要一个 O ( V n ) O(V\sqrt n) O(Vn ) 的数组,这非常不可接受。

不过我们可以发现,我们在分块的时候,块是独立的,块与块之间不会相互影响。

所以我们可以将操作离线,对每个块都按顺序处理一遍所有的操作,然后把询问的答案累加即可。

思考一下,这个 trick 为什么其他很多题都用不了呢?

因为用它要满足下面几个性质:

  • 可以离线。
  • 块是独立的,块与块之间不会相互影响。
  • 答案可加性。

这样空间复杂度就优化成 O ( V ) \mathcal O(V) O(V) 了。

总结:

这题不是很卡常,但是用到了许多很有意思的 tricks

没了。

56.54s / 29.80MB / 4.87KB C++98 O2 \text{56.54s / 29.80MB / 4.87KB C++98 O2} 56.54s / 29.80MB / 4.87KB C++98 O2

#include <cstdio>
#include <cmath>
#include <cstring>

namespace Fread
{
    const int SIZE = 1 << 21;
    char buf[SIZE], *S, *T;
    inline char getchar()
    {
        if (S == T)
        {
            T = (S = buf) + fread(buf, 1, SIZE, stdin);
            if (S == T)
                return '\n';
        }
        return *S++;
    }
}
namespace Fwrite
{
    const int SIZE = 1 << 21;
    char buf[SIZE], *S = buf, *T = buf + SIZE;
    inline void flush()
    {
        fwrite(buf, 1, S - buf, stdout);
        S = buf;
    }
    inline void putchar(char c)
    {
        *S++ = c;
        if (S == T)
            flush();
    }
    struct NTR
    {
        ~NTR()
        {
            flush();
        }
    } ztr;
}

#ifdef ONLINE_JUDGE
#define getchar Fread::getchar
#define putchar Fwrite::putchar
#endif

inline int read()
{
    int x = 0, f = 1;
    char c = getchar();
    while (c < '0' || c > '9')
    {
        if (c == '-')
            f = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9')
    {
        x = x * 10 + c - '0';
        c = getchar();
    }
    return x * f;
}

inline void write(int x)
{
    if (x < 0)
    {
        putchar('-');
        x = -x;
    }
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}

inline int min(int x, int y)
{
    return x < y ? x : y;
}

inline int max(int x, int y)
{
    return x > y ? x : y;
}

const int _ = 1e6 + 7, V = 1e5 + 7;

int n, m, blo, len;

int a[_], fa[_], val[_], name[V], siz[V], lazy, maxx, ans[_];

struct que
{
    int op, l, r, x;
} q[_];

inline int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

inline void build(int x)
{
    maxx = -2147483647 - 1, lazy = 0;
    int bl = (x - 1) * blo + 1, br = x * blo;
    if (x == len)
        br = n;
    for (int i = bl; i <= br; ++i)
    {
        maxx = max(maxx, a[i]);
        if (!name[a[i]])
        {
            val[i] = a[i];
            name[a[i]] = i;
            fa[i] = i;
        }
        else
            fa[i] = name[a[i]];
        siz[a[i]]++;
    }
}

inline void change(int x, int y)
{
    if (name[y])
        fa[name[x]] = name[y];
    else
    {
        name[y] = name[x];
        val[name[y]] = y;
    }
    siz[y] += siz[x];
    name[x] = siz[x] = 0;
}

inline void update1(int num, int l, int r, int x)
{
    int bl = (num - 1) * blo + 1, br = num * blo;
    if (num == len)
        br = n;
    l = max(l, bl), r = min(r, br);
    for (int i = bl; i <= br; ++i)
    {
        int w = val[find(i)];
        a[i] = w - lazy;
        name[w] = siz[w] = 0;
    }
    for (int i = bl; i <= br; ++i)
        val[i] = 0;
    for (int i = l; i <= r; ++i)
        if (a[i] > x)
            a[i] -= x;
    build(num);
}

inline void update2(int x)
{
    if (x > (maxx - lazy) / 2)
    {
        for (int i = x + lazy + 1; i <= maxx; ++i)
            if (name[i])
                change(i, i - x);
        maxx = min(maxx, x + lazy);
    }
    else
    {
        for (int i = lazy; i <= x + lazy; ++i)
            if (name[i])
                change(i, i + x);
        lazy += x;
    }
}

inline int query(int num, int l, int r, int x)
{
    int ans = 0, bl = (num - 1) * blo + 1, br = num * blo;
    if (num == len)
        br = n;
    l = max(l, bl), r = min(r, br);
    for (int i = l; i <= r; ++i)
        if (val[find(i)] - lazy == x)
            ans++;
    return ans;
}

signed main()
{
    n = read(), m = read();
    blo = sqrt(n), len = ceil(n * 1.0 / blo);
    for (int i = 1; i <= n; ++i)
        a[i] = read();
    for (int i = 1; i <= m; ++i)
    {
        q[i].op = read();
        q[i].l = read();
        q[i].r = read();
        q[i].x = read();
    }
    for (int i = 1; i <= len; ++i)
    {
        if (i != 1)
        {
            memset(name, 0, sizeof name);
            memset(siz, 0, sizeof siz);
        }
        lazy = 0, maxx = -2147483647 - 1;
        int bl = (i - 1) * blo + 1, br = i * blo;
        if (i == len)
            br = n;
        build(i);
        for (int j = 1; j <= m; ++j)
        {
            int op = q[j].op, l = q[j].l, r = q[j].r, x = q[j].x;
            if (bl > r || br < l)
                continue;
            if (op == 1)
            {
                if (!(l <= bl && br <= r))
                    update1(i, l, r, x);
                else
                    update2(x);
            }
            else
            {
                if (x + lazy > 1e5 + 1)
                    continue;
                if (!(bl >= l && br <= r))
                    ans[j] += query(i, l, r, x);
                else
                    ans[j] += siz[x + lazy];
            }
        }
    }
    for (int i = 1; i <= m; ++i)
        if (q[i].op == 2)
            write(ans[i]), putchar('\n');
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值