AcWing
  • 首页
  • 课程
  • 题库
  • 更多
    • 竞赛
    • 题解
    • 分享
    • 问答
    • 应用
    • 校园
  • 关闭
    历史记录
    清除记录
    猜你想搜
    AcWing热点
  • App
  • 登录/注册

AcWing 3115. 疯狂的馒头【并查集区间染色模型 / 线段树剪枝】    原题链接    中等

作者: 作者的头像   一只野生彩色铅笔 ,  2021-08-06 19:31:58 ,  所有人可见 ,  阅读 1719


28


17

这就是那道,y总说要加进提高课里,但是一直没有加的题目

题目描述

初始时,一共有 $n$ 个颜色为 $0$ 馒头,然后我们一共要操作 $m$ 次

每次使得第 $(i×p+q)modN+1 $ 个和第 $(i×q+p)modN+1$ 个之间的馒头染上颜色 $i$

最终输出每个馒头的颜色

分析

本题直接翻译成大白话的意思就是,给定一个区间,每次使该区间的某个子区间的所有元素值变成 $i$

问经过 $m$ 次操作后,该区间内每个元素的值为多少


一开始读完题,都会想到用一些 数据结构,例如 Splay 或者 Segment tree 来做模拟

我也都试了试,最后发现本题的 数据规模 令人绝望,$1 \le M \le 10^7$

而 splay 和 segment tree 的 区间修改 时间复杂度为 $O(\log n)$

因此毫无疑问会被卡掉(结尾我会把我T了的代码也贴上来,供大家参考)

针对于该 数据规模,我们需要想到一些线性的做法

已知 正着推,模拟的最优时间复杂度为 $O(m\log n)$,所有我们不妨想一想 倒着推 会不会好一点

观察到一个明显的性质:

  1. 如果正着枚举,那么任意阶段中每个馒头的颜色都不是唯一确定的(可能下个阶段就被染成新的了)
  2. 如果倒着枚举,那么如果当前馒头已被染上颜色,则他的颜色就是被唯一确定下来的了(最后一次被染色的操作)

那么本题就转化成了 区间覆盖模型 了,即每次把覆盖的区间删掉,最终把整个区间覆盖的模型

在本题里就是,每次把染色的区间删掉 并 记录该颜色,最后输出每个元素的颜色(没删的为 $0$)

这是一个 并查集 的 经典应用

用 $p_i$ 记录第 $i$ 个元素后面第一个 未被覆盖 的区间的 左端点

覆盖一个元素的方法就是,把 $p_{p_i}$ 更新成右边的 $p_{i+1}$ 即可

覆盖一个区间 $[l,r]$ 的方法类似,把该段里所有的 $p_{p_i}$ 都更新成$p_{r+1}$

这样直接搞,看上去是 $O(mn)$ 的,但是我们在覆盖区间的时候,是可以进行 指针跳跃 的

即删完一段后就跳到整段后面,再删后面一段 p[pa] = find(pa + 1);pa = find(pa); 具体参考下面代码

这样每进行一次 覆盖,保证了 连通块 就会 减少一个,因此最多不会超过 $n$ 次 覆盖操作

覆盖操作的具体图示如下:

4E3182602C889963BDA196C73B845FB6.png

这里贴出一些相似的题目供大家熟悉该模型:

  1. AcWing 1242. 修改数组
  2. AcWing 3820. 未出现过的最小正整数

当然倒着做的时候也可以用 Segment Tree 维护

但是需要在一个区间被完全覆盖之后,不在遍历该节点,达成 剪枝 的操作

那么 最坏时间复杂度 就是把线段树所有结点都更新的时间复杂度:$O(n\log n)$,而不是正推的 $O(m\log n)$

Code(并查集—区间覆盖模型)

时间复杂度: (理论)$O(m \log n)$

但是 y 总告诉我们,笔试的时候要说并查集时间复杂度为$O(\log n)$,上机的时候看做 $O(1)$ 就好啦~

时间复杂度: (玄学)$O(m)$

#include <iostream>

using namespace std;

const int N = 1e6 + 10;

int n, m, _p, q;
int p[N], color[N];

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    scanf("%d%d%d%d", &n, &m, &_p, &q);
    for (int i = 1; i <= n + 1; ++ i) p[i] = i;
    for (int i = m; i; -- i)
    {
        int a = (i * q + _p) % n + 1;
        int b = (i * _p + q) % n + 1;
        int l = min(a, b), r = max(a, b);
        int pa = find(l);
        while (pa <= r)
        {
            color[pa] = i;
            p[pa] = find(pa + 1);
            pa = find(pa);
        }
    }
    for (int i = 1; i <= n; ++ i) printf("%d\n", color[i]);
    return 0;
}

Code(线段树—倒着推剪枝)

时间复杂度: $O(n \log n)$

#include <iostream>

using namespace std;

const int N = 1e6 + 10;

int n, m, p, q;
struct Node
{
    int l, r, v;
}tr[N << 2];
int color[N];

void pushup(int u)
{
    if (tr[u << 1].v && tr[u << 1 | 1].v)
    {
        tr[u].v = true;
    }
}
void build(int u, int l, int r)
{
    tr[u] = {l, r, 0};
    if (l == r) return;
    int mid = l + r >> 1;
    build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}
void modify(int u, int l, int r, int v)
{
    if (tr[u].v) return;//这个区间已经被染色了,就剪枝剪掉
    if (tr[u].l == tr[u].r) tr[u].v = v;
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u << 1, l, r, v);
        if (r > mid) modify(u << 1 | 1, l, r, v);
        pushup(u);
    }
}
void output(int u)
{
    if (tr[u].l == tr[u].r) printf("%d\n", tr[u].v);
    else output(u << 1), output(u << 1 | 1);
}
int main()
{
    scanf("%d%d%d%d", &n, &m, &p, &q);
    build(1, 1, n);
    for (int i = m; i; -- i)
    {
        int l = (i * p + q) % n + 1, r = (i * q + p) % n + 1;
        if (l > r) swap(l, r);
        modify(1, l, r, i);
    }
    output(1);
    return 0;
}

Code(Splay正着推)

过了 $60\%$

#include <iostream>

#define lson tr[u].s[0]
#define rson tr[u].s[1]

using namespace std;

const int N = 1e6 + 10;

int n, m, p, q;
struct Node
{
    int s[2], v, p, c, size, flag;
    void init(int _v, int _p) {v = _v, p = _p;}
}tr[N];
int root, idx;

void pushup(int u)
{
    tr[u].size = tr[lson].size + tr[rson].size + 1;
}
void pushdown(int u)
{
    if (tr[u].flag)
    {
        tr[u].c = tr[u].flag;
        tr[lson].flag = tr[u].flag;
        tr[rson].flag = tr[u].flag;
        tr[u].flag = 0;
    }
}
void rotate(int x)
{
    int y = tr[x].p, z = tr[y].p;
    int k = tr[y].s[1] == x;
    tr[z].s[tr[z].s[1] == y] = x, tr[x].p = z;
    tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
    tr[x].s[k ^ 1] = y, tr[y].p = x;
    pushup(y), pushup(x);
}
void splay(int x, int k)
{
    while (tr[x].p != k)
    {
        int y = tr[x].p, z = tr[y].p;
        if (z != k)
            if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
            else rotate(y);
        rotate(x);
    }
    if (!k) root = x;
}
void insert(int v)
{
    int u = root, p = 0;
    while (u) p = u, u = tr[u].s[v > tr[u].v];
    u = ++ idx;
    if (p) tr[p].s[v > tr[p].v] = u;
    tr[u].init(v, p);
    splay(u, 0);
}
int get_k(int k)
{
    int u = root;
    while (true)
    {
        pushdown(u);
        if (tr[lson].size >= k) u = lson;
        else if (tr[lson].size + 1 == k) return u;
        else k -= tr[lson].size + 1, u = rson;
    }
    return -1;
}
void output(int u)
{
    pushdown(u);
    if (tr[u].s[0]) output(tr[u].s[0]);
    if (1 <= tr[u].v && tr[u].v <= n) printf("%d\n", tr[u].c);
    if (tr[u].s[1]) output(tr[u].s[1]);
}
int main()
{
    scanf("%d%d%d%d", &n, &m, &p, &q);
    for (int i = 0; i <= n + 1; ++ i) insert(i);
    for (int i = 1; i <= m; ++ i)
    {
        int l = (i * p + q) % n + 1, r = (i * q + p) % n + 1;
        if (l > r) swap(l, r);
        l = get_k(l), r = get_k(r + 2);
        splay(l, 0), splay(r, l);
        tr[tr[r].s[0]].flag = i;
    }
    output(root);
    return 0;
}

Code(线段树正着推)

过了 $80\%$

是不是说明 $Splay$ 常数比 $Segment Tree$ 大?

#include <iostream>

using namespace std;

const int N = 1e6 + 10;

int n, m, p, q;
struct Node
{
    int l, r, v, flag;
}tr[N << 2];

void pushup(int u)
{
    if (tr[u << 1].v == tr[u << 1 | 1].v) tr[u].v = tr[u << 1].v;
    else tr[u].v = -1;
}
void pushdown(int u)
{
    if (tr[u].flag)
    {
        tr[u << 1].v = tr[u].flag;
        tr[u << 1].flag = tr[u].flag;
        tr[u << 1 | 1].v = tr[u].flag;
        tr[u << 1 | 1].flag = tr[u].flag;
        tr[u].flag = 0;
    }
}
void build(int u, int l, int r)
{
    tr[u] = {l, r, 0};
    if (l == r) return;
    int mid = l + r >> 1;
    build(u << 1, l, mid);
    build(u << 1 | 1, mid + 1, r);
    pushup(u);
}
void modify(int u, int l, int r, int v)
{
    if (l <= tr[u].l && tr[u].r <= r)
    {
        tr[u].v = v;
        tr[u].flag = v;
    }
    else
    {
        pushdown(u);
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u << 1, l, r, v);
        if (r > mid) modify(u << 1 | 1, l, r, v);
        pushup(u);
    }
}
int query(int u, int k)
{
    if (tr[u].l <= k && k <= tr[u].r && ~tr[u].v) return tr[u].v;
    pushdown(u);
    int mid = tr[u].l + tr[u].r >> 1;
    if (k <= mid) return query(u << 1, k);
    return query(u << 1 | 1, k);
}
int main()
{
    scanf("%d%d%d%d", &n, &m, &p, &q);
    build(1, 1, n);
    for (int i = 1; i <= m; ++ i)
    {
        int l = (i * p + q) % n + 1, r = (i * q + p) % n + 1;
        if (l > r) swap(l, r);
        modify(1, l, r, i);
    }
    for (int i = 1; i <= n; ++ i) printf("%d\n", query(1, i));
    return 0;
}

7 评论


用户头像
柳晨荫   2021-08-08 11:57      1    踩      回复

学累了,点开AcWing,给彩铅点赞

用户头像
一只野生彩色铅笔   2021-08-08 12:01         踩      回复

o(╥﹏╥)o


用户头像
CZL   2021-08-07 09:20      1    踩      回复

我要是女生就追铅笔男神

用户头像
一只野生彩色铅笔   2021-08-07 10:16      1    踩      回复

😂 被富家子弟鱼佬包养不香吗hh


用户头像
Peter_5   2021-08-06 23:36      1    踩      回复

巨巨好强啊o(╥﹏╥)o我哭了

用户头像
一只野生彩色铅笔   2021-08-06 23:43         踩      回复

我好菜的o(╥﹏╥)o


用户头像
一个迷路的野指针   2021-10-12 10:31         踩      回复

看大佬的题解真是一种享受,太精彩了


App 内打开
你确定删除吗?
1024
x

© 2018-2025 AcWing 版权所有  |  京ICP备2021015969号-2
用户协议  |  隐私政策  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标 qq图标
请输入绑定的邮箱地址
请输入注册信息