方块掉落

方块掉落

题目描述

最近阿宁对一个名叫“方块掉落”的游戏感兴趣,沉迷于此。

每局游戏一开始,有一条无限长的水平线、一个箭头、一个操作序列 $t$,没有任何方块。

操作序列 $t$ 是一个字符串,仅包含"YBR"三种字符,分别代表颜色蓝红。依次按照操作序列 $s$ 掉落不同颜色的方块。

如果即将掉落的是黄色方块,那么箭头指向的位置掉落一个黄色方块。例如:

如果即将掉落的是蓝色方块,首先箭头移到下一个位置;然后箭头指向的位置掉落一个蓝色方块。例如:

如果即将掉落的是红色方块,在箭头指向的那一列,掉落这一列所有存在的方块;然后箭头指向的位置掉落一个红色方块。例如:

现在,给一个长度为 $n$ 字符串 $s$,有 $q$ 次操作。

操作有两种类型,修改操作和询问操作。

对于修改操作,会有两个参数 $p$ 和 $c$,代表将 $s_p$ 修改成字符 $c$。

对于询问操作,会有两个参数 $l, r$,代表游戏操作序列 $t$ 是子串$ s_{l \ldots r}$,问按照该操作序列进行游戏,最终会有多少个方块?答案对 $10^9+7$ 取模。

输入描述:

第一行输入两个整数 $n, q$。

第二行输入一个长度为 $n$,且仅包含 "YBR" 三种字符的字符串 $s$。

接下来 $q$ 行,每行首先输入一个整数 $op$,代表操作类型。

如果 $op=1$,那么该操作代表修改操作,接下来输入两个参数 $p, c$。

如果 $op=2$,那么该操作代表询问操作,接下来输入两个参数  $l, r$。

 

$1\leq n,q \leq 2 \times 10^5$

$1\leq p \leq n$

$1\leq l \leq r \leq n$

参数 $c$ 是"YBR" 三种字符的其中一个。

保证最少有一个询问操作。

输出描述:

对于每一个询问操作,输出一个整数,代表答案对 $10^9+7$ 取模的结果,占一行。

示例1

输入

8 10
YYBYRBRY
2 1 1
2 1 2
2 1 3
2 1 4
2 1 5
2 1 6
2 1 7
2 1 8
1 8 R
2 1 8

输出

1
2
3
4
7
8
10
11
14

 

解题思路

  考虑能不能用线段树来维护,假设线段树节点维护的区间是 $[l,r]$,首先需要维护由子串 $s_l \ldots s_r$ 生成的方块数量这一信息,记作 $s$。现在假设已经知道节点 $u$ 的两个儿子 $l$ 和 $r$ 的信息,考虑如何通过 $l$ 和 $r$ 更新 $u$,以及还需要维护什么额外的信息。

  以上图为例,当 $l$ 和 $r$ 合并后子串 $s_1$ 生成的方块数量还是 $1$,子串 $s_2 \ldots s_5$ 生成的方块数量 $rs$ 会因为 $s_8$ 和 $s_9$ 的红色字符被翻倍了 $2$ 次,因此合并后左边生成的方块数量变成了 $l \mathrm{.} s + rs \cdot (2^2-1)$,而右边由于不会受到左边的影响,因此生成的方块数量还是 $r \mathrm{.} s$。所以合并后的子串 $s_1 \ldots s_{10}$ 生成的方块数量就是 $u \mathrm{.} s = l \mathrm{.} s + rs \cdot (2^2-1) + r \mathrm{.} s$。

  可以发现我们除了关心每个儿子所维护子串生成的方块数量 $s$ 外,还关心左儿子最靠右的蓝色字符到末端所生成的方块数量 $rs$,和右儿子最靠左的蓝色字符之前红色方块的数量 $ls$。还要考虑如果没有蓝色字符的情况,那么 $rs$ 就是 $s$,$ls$ 就是整个子串 $s_l \ldots s_r$ 中红色字符的数量。因此还要维护子串内是否有蓝色字符,用 $f$ 标记,以及子串内红色字符的总数 $c$。所以线段树节点定义的信息就有:

struct Node {
    int l, r; // 维护的子串区间
    int s; // 整个子串生成的方块数量
    int ls; // 第一个蓝色字符前红色字符的数量,如果子串中没有蓝色字符则是子串中红色字符的数量c
    int rs; // 最后一个蓝色字符到s[r]这段字符生成的方块数量,如果子串中没有蓝色字符则是子串生成的方块数量s
    int f; // 子串中是否有蓝色字符
    int c; // 子串中红色字符的总数
}tr[N * 4];

  考虑如何更新节点 $u$ 的信息,即 pushup 操作。

  $u \mathrm{.} s$:合并后左边生成的方块数量会变成 $l \mathrm{.} s + l \mathrm{.} rs \cdot (2^{r \mathrm{.} ls} - 1)$,右边的方块数量不变 $r \mathrm{.} s$,因此 $u \mathrm{.} s = l \mathrm{.} s + l \mathrm{.} rs \cdot (2^{r \mathrm{.} ls} - 1) + r \mathrm{.} s$。

  $u \mathrm{.} ls$:如果左边存在蓝色字符,那么 $u \mathrm{.} ls = l \mathrm{.} ls$。否则如果右边存在蓝色字符,那么左边的红色字符全部加上,$u \mathrm{.} ls = l \mathrm{.} c + r \mathrm{.} ls$。否则都没有蓝色字符,那么就是整个子串中红色字符的数量 $u \mathrm{.} ls = l \mathrm{.} c + r \mathrm{.} c$。

  $u \mathrm{.} rs$:如果右边存在蓝色字符,那么 $u \mathrm{.} rs = r \mathrm{.} rs$。否则如果左边存在蓝色字符,那么就是合并后左边最后一个蓝色字符到左边末端所生成的方块数量加上右边的方块数量,即 $l \mathrm{.} rs \cdot 2^{r \mathrm{.} ls} + r \mathrm{.} s$。否则都没有蓝色字符,那么 $u \mathrm{.} rs = u \mathrm{.} s$。

  $u \mathrm{.} f$:直接与运算即可 $u \mathrm{.} f = l \mathrm{.} f \, \& \, r \mathrm{.} f$。

  $u \mathrm{.} c$:左右两边红色字符的总数 $u \mathrm{.} c = l \mathrm{.} c + r \mathrm{.} c$。

  因此 pushup 的代码就是:

void pushup(Node &u, Node &l, Node &r) {
    u.s = (1ll * l.rs * p[r.ls] + l.s + r.s - l.rs) % mod;
    if (l.f) u.ls = l.ls;
    else if (r.f) u.ls = l.c + r.ls;
    else u.ls = l.c + r.c;
    if (r.f) u.rs = r.rs;
    else if (l.f) u.rs = (1ll * l.rs * p[r.ls] + r.s) % mod;
    else u.rs = u.s;
    u.f = l.f | r.f;
    u.c = l.c + r.c;
}

  单点修改和区间查询都会用到 pushup,其中查询需要将返回的左右儿子的信息进行合并,比较简单看代码就可以理解。

  AC 代码如下,时间复杂度为 $O(q \log{n})$:

#include <bits/stdc++.h>
using namespace std;

const int N = 2e5 + 10, mod = 1e9 + 7;

char s[N];
int p[N];
struct Node {
    int l, r, s, ls, rs, f, c;
}tr[N * 4];

void pushup(Node &u, Node &l, Node &r) {
    u.s = (1ll * l.rs * p[r.ls] + l.s + r.s - l.rs) % mod;
    if (l.f) u.ls = l.ls;
    else if (r.f) u.ls = l.c + r.ls;
    else u.ls = l.c + r.c;
    if (r.f) u.rs = r.rs;
    else if (l.f) u.rs = (1ll * l.rs * p[r.ls] + r.s) % mod;
    else u.rs = u.s;
    u.f = l.f | r.f;
    u.c = l.c + r.c;
}

void build(int u, int l, int r) {
    tr[u] = {l, r, 0, 0, 0, 0};
    if (l == r) {
        tr[u].s = tr[u].rs = 1;
        if (s[l] == 'B') tr[u].f = 1;
        else if (s[l] == 'R') tr[u].ls = tr[u].c = 1;
    }
    else {
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
        pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
    }
}

void modify(int u, int x, char c) {
    if (tr[u].l == tr[u].r) {
        tr[u].ls = tr[u].f = tr[u].c = 0;
        if (c == 'B') tr[u].f = 1;
        else if (c == 'R') tr[u].ls = tr[u].c = 1;
    }
    else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u << 1, x, c);
        else modify(u << 1 | 1, x, c);
        pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
    }
}

Node query(int u, int l, int r) {
    if (tr[u].l >= l && tr[u].r <= r) return tr[u];
    int mid = tr[u].l + tr[u].r >> 1;
    if (r <= mid) return query(u << 1, l, r);
    if (l >= mid + 1) return query(u << 1 | 1, l, r);
    Node t1 = query(u << 1, l, r), t2 = query(u << 1 | 1, l, r), t;
    pushup(t, t1, t2);
    return t;
}

int main() {
    int n, m;
    scanf("%d %d %s", &n, &m, s + 1);
    p[0] = 1;
    for (int i = 1; i <= n; i++) {
        p[i] = p[i - 1] * 2 % mod;
    }
    build(1, 1, n);
    while (m--) {
        int op;
        scanf("%d", &op);
        if (op == 1) {
            int x;
            char c[5];
            scanf("%d %s", &x, c);
            modify(1, x, c[0]);
        }
        else {
            int l, r;
            scanf("%d %d", &l, &r);
            printf("%d\n", query(1, l, r).s);
        }
    }
    
    return 0;
}

  再补充个用线段树维护矩阵乘法的做法,实际上会比上面数学推导的方法要简单很多。

  用 $c$ 来表示当前列方块的数量,$s$ 来表示总的方块数量。当遇到黄色方块就会有 $(c, s) \to (c+1, s+1)$,遇到蓝色方块就会有 $(c, s) \to (1, s+1)$,遇到红色方块就会有 $(c, s) \to (2 \cdot c + 1, s + c + 1)$。$c$ 和 $s$ 存在关联,因此可以用矩阵 $F = \begin{bmatrix} c & s & 1 \end{bmatrix}$ 来维护。

  再考虑黄色方块对应的转移矩阵 $Y$,使得 $F \times Y = \begin{bmatrix} c+1 & s+1 & 1 \end{bmatrix}$,容易知道有 $Y = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 1 & 1 & 1 \end{bmatrix}$。

  同理蓝色方块对应的转移矩阵为 $B = \begin{bmatrix} 0 & 0 & 0 \\ 0 & 1 & 0 \\ 1 & 1 & 1 \end{bmatrix}$,有 $F \times B = \begin{bmatrix} 1 & s+1 & 1 \end{bmatrix}$。

  红色方块对应的转移矩阵为 $R = \begin{bmatrix} 2 & 1 & 0 \\ 0 & 1 & 0 \\ 1 & 1 & 1 \end{bmatrix}$,有 $F \times R = \begin{bmatrix} 2 \cdot c + 1 & s + c + 1 & 1 \end{bmatrix}$。

  因此我们只需用线段树来维护区间的转移矩阵的乘积。

  AC 代码如下,时间复杂度为 $O(q \log{n})$:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int N = 2e5 + 10, mod = 1e9 + 7;

char s[N];
struct Matrix {
    array<array<int, 3>, 3> a;
    
    Matrix(array<array<int, 3>, 3> b = {0}) {
        a = b;
    }
    auto& operator[](int x) {
        return a[x];
    }
    Matrix operator*(Matrix b) {
        Matrix c;
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                for (int k = 0; k < 3; k++) {
                    c[i][j] = (c[i][j] + (LL)a[i][k] * b[k][j]) % mod;
                }
            }
        }
        return c;
    }
};
struct Node {
    int l, r;
    Matrix f;
}tr[N * 4];
Matrix mp[1 << 8];

void build(int u, int l, int r) {
    tr[u] = {l, r};
    if (l == r) {
        tr[u].f = mp[s[l]];
    }
    else {
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
        tr[u].f = tr[u << 1].f * tr[u << 1 | 1].f;
    }
}

void modify(int u, int x, char c) {
    if (tr[u].l == tr[u].r) {
        tr[u].f = mp[c];
    }
    else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u << 1, x, c);
        else modify(u << 1 | 1, x, c);
        tr[u].f = tr[u << 1].f * tr[u << 1 | 1].f;
    }
}

Matrix query(int u, int l, int r) {
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].f;
    int mid = tr[u].l + tr[u].r >> 1;
    Matrix ret({1, 0, 0, 0, 1, 0, 0, 0, 1});
    if (l <= mid) ret = query(u << 1, l, r);
    if (r >= mid + 1) ret = ret * query(u << 1 | 1, l, r);
    return ret;
}

int main() {
    int n, m;
    scanf("%d %d %s", &n, &m, s + 1);
    mp['Y'] = Matrix({1, 0, 0, 0, 1, 0, 1, 1, 1});
    mp['B'] = Matrix({0, 0, 0, 0, 1, 0, 1, 1, 1});
    mp['R'] = Matrix({2, 1, 0, 0, 1, 0, 1, 1, 1});
    build(1, 1, n);
    while (m--) {
        int op;
        scanf("%d", &op);
        if (op == 1) {
            int x;
            char c[5];
            scanf("%d %s", &x, c);
            modify(1, x, c[0]);
        }
        else {
            int l, r;
            scanf("%d %d", &l, &r);
            printf("%d\n", (Matrix({0, 0, 1}) * query(1, l, r))[0][1]);
        }
    }
    
    return 0;
}

 

参考资料

  【题目讲解】2024牛客寒假算法基础集训营4:https://www.bilibili.com/video/BV1By42187Kj/

posted @ 2024-02-21 23:10  onlyblues  阅读(8)  评论(0编辑  收藏  举报
Web Analytics