K. Addition Robot

题意

给定 $n$ 个字母 $s_1, s_2 …, s_n$,每个字母要么为 $A$ 要么为 $B$。

现在有 $Q$ 个询问,每个询问有两种类型:

$1 ~ L ~ R$:将 $i \in [L,R]$ 的所有 $A$ 改成 $B$,$B$ 改成 $A$。

$2 ~ L ~ R ~ a ~ b$:$a,b$ 是两个非负整数,从左往右遍历 $L$ 到 $R$,遇到一个 A 就让 $a=a+b$,遇到一个 B 就让 $b=a+b$,返回 $a,b$ 在经历这些操作后的值。

其中,$n \leq 10^5, Q \leq 10^5$,答案对 $10^9+7$ 取模。

题解

线段树。

注意到这个 $a=a+b$,$b=a+b$,有没有想到 矩阵操作

设 $v = \begin{bmatrix} a \\
b \\
\end{bmatrix}$,那么设 $A = \begin{bmatrix} 1 & 1 \\
0 & 1 \\
\end{bmatrix}, B = \begin{bmatrix} 1 & 0 \\
1 & 1 \\
\end{bmatrix}$,就有

$$Av = \begin{bmatrix} a+b \\ b \\ \end{bmatrix}, Bv = \begin{bmatrix} a \\ a+b \\ \end{bmatrix}$$

所以线段树里面维护矩阵即可。


现在考虑一下第一种询问怎么处理?

第一种询问其实相当于保持矩阵不变,先把 $v = \begin{bmatrix} a \\
b \\
\end{bmatrix}$ 变成 $v‘ = \begin{bmatrix} b \\
a \\
\end{bmatrix}$,然后乘完矩阵以后,再上下对调变回来。

这个操作本质上也是矩阵乘法,即令 $C = \begin{bmatrix} 0 & 1 \\
1 & 0 \\
\end{bmatrix}$,假设原矩阵操作序列是 $M = (AABA)$,原操作序列得到的结果是 $Mv$,现在的结果是 $C(M(cv)) = CMCv = (CMC)v$。

由此可知第一种询问就是将 $[L,R]$ 维护的矩阵 $M$ 变成了 $CMC$。

进行一些手动计算以后可以发现这等效于将 $M$ 的两个对角线元素互换。


最后需要注意一点,矩阵乘法和序列的遍历顺序是反过来的,比如序列是 $AB$,那么矩阵乘法应该是 $(BA)v$,处理这种情况只要在维护线段树的时候先处理 right child即可。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+5;
const int maxn = 1e5+5;
const int maxm = 2e5+5;

struct Mat {
    ll a[2][2];
    friend Mat operator*(const Mat& lhs, const Mat& rhs) {
        Mat res;
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 2; j++) {
                // 第i行 乘 第j列
                res.a[i][j] = 0;
                for (int k = 0; k < 2; k++) {
                    res.a[i][j] += lhs.a[i][k] * rhs.a[k][j];
                    res.a[i][j] %= mod;
                }
            }
        }
        return res;
    }
    void rev() {
        swap(a[0][0], a[1][1]);
        swap(a[0][1], a[1][0]);
    }
};

int n, Q;
Mat A, B, arr[maxn];
struct Tree_Node {
    Mat a;
    int flag = 0;
} tr[maxn<<2];
void push_up(int cur) {
    tr[cur].a = tr[cur<<1|1].a * tr[cur<<1].a;
}
void push_down(int cur) {
    if (!tr[cur].flag) return;
    tr[cur].flag = 0;
    int lc = cur<<1, rc = lc|1;
    tr[lc].flag ^= 1;
    tr[rc].flag ^= 1;
    tr[lc].a.rev();
    tr[rc].a.rev();
}
void build(int cur, int l, int r) {
    if (l == r) {
        tr[cur].a = arr[l];
        return;
    }
    int mid = (l+r) >> 1;
    build(cur<<1|1, mid+1, r);
    build(cur<<1, l, mid);
    push_up(cur);
}
void update(int cur, int l, int r, int L, int R) {
    if (L <= l && R >= r) {
        tr[cur].flag ^= 1;
        tr[cur].a.rev();
        return;
    }
    push_down(cur);
    int mid = (l+r) >> 1;
    if (R > mid) update(cur<<1|1, mid+1, r, L, R);
    if (L <= mid) update(cur<<1, l, mid, L, R);
    push_up(cur);
}
Mat query(int cur, int l, int r, int L, int R) {
    if (L <= l && R >= r) {
        return tr[cur].a;
    }
    push_down(cur);
    int mid = (l+r) >> 1;
    Mat res {};
    res.a[0][0] = res.a[1][1] = 1;
    res.a[0][1] = res.a[1][0] = 0;
    if (R > mid) res = res * query(cur<<1|1, mid+1, r, L, R);
    if (L <= mid) res = res * query(cur<<1, l, mid, L, R);
    push_up(cur);
    return res;
}

int main() {
    fastio;
    cin >> n >> Q;

    A.a[0][0] = A.a[0][1] = A.a[1][1] = 1;
    B.a[0][0] = B.a[1][0] = B.a[1][1] = 1;

    for (int i = 1; i <= n; i++) {
        char c; cin >> c;
        if (c == 'A') arr[i] = A;
        else arr[i] = B;
    }
    build(1, 1, n);
    while (Q--) {
        int op;
        cin >> op;
        if (op == 1) {
            int L, R; cin >> L >> R;
            update(1, 1, n, L, R);
        } else {
            int L, R; ll a, b; cin >> L >> R >> a >> b;
            Mat res = query(1, 1, n, L, R);
            cout << (a * res.a[0][0] + b * res.a[0][1]) % mod << " " << (a * res.a[1][0] + b * res.a[1][1]) % mod << "\n";
        }
    }
}

G. Performance Review

题意

一家公司有 $N$ 个员工,总共有 $M$ 年,每个员工都有一个表现分 $A_i$。

第 $i$ 年,公司会淘汰 $R_i$ 个表现最差的员工,并且换上 $R_i$ 个新员工,他们的表现分为 $B_{i,1}, B_{i,2} … B_{i,R_i}$(这些新员工表现不一定比老员工好)。

现在考虑第一个员工 $A_1$,他有 $Q$ 个询问,每次询问他会更改一个未来替换的员工的表现分,回答在替换之后,他在 $M$ 年之后是否还能留在公司。

• 每次询问的更改结果会保留到后续询问。

其中,$N,M,Q \leq 10^5, \sum\limits_{i=1}^M R_i \leq 10^6$,所有员工(包括未来的)表现分各不相同。

题解

注意到员工 $A_1$ 能留到 $M$ 年后当且仅当对于所有的 $i$,在第 $i$ 年结束时,比他菜的人数 $\geq$ 要淘汰的人数。

形式化的:

$$cnt_{i-1} \geq \sum\limits_{j=1}^i R_i, \forall i \in [1, M]$$

其中,$cnt_{i-1}$ 代表第 $i-1$ 年结束后,(完成招聘以后)比他菜的人数。


现在,只要考虑每次询问过后有什么影响即可。

注意到要淘汰的人数是个定值,影响的只有比他菜的人数。

所以用线段树,储存每一年 $cnt_{i-1} - \sum\limits_{j=1}^i R_i$ 的值。

每次更改第 $j$ 年中,一个人的表现分以后,跟 $A[1]$ 对比,如果原本比他菜,现在比他强,意味着从 $(j+1)$ 年起,一直到 $M$,比他菜的人少了一个,反之则是多了一个。

所以就是线段树的区间修改,区间查询最小值是否 $\geq 0$。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+5;
int n, m, Q, a[maxn];
vector<int> per[maxn];
 
int cnt[maxn], sum_r[maxn];  // cnt[i]: 第i年之前比他小的 (0...i-1) 年,sum_r[i]: 第i年过后总共要裁的人数
int c[maxn];  // c[i] = cnt[i] - sum(R[1..i])
 
struct Tree_Node {
    int mn = 1e9, flag = 0;  // min_val, flag
} tr[maxn<<2];
void push_up(int cur) {
    tr[cur].mn = min(tr[cur<<1].mn, tr[cur<<1|1].mn);
}
void push_down(int cur) {
    if (!tr[cur].flag) return;
    int f = tr[cur].flag;
    tr[cur<<1].flag += f;
    tr[cur<<1|1].flag += f;
    tr[cur<<1].mn += f;
    tr[cur<<1|1].mn += f;
    tr[cur].flag = 0;
}
// += x
void update(int cur, int l, int r, int L, int R, int x) {
    if (L <= l && R >= r) {
        tr[cur].flag += x;
        tr[cur].mn += x;
        return;
    }
    push_down(cur);
    int mid = (l+r) >> 1;
    if (L <= mid) update(cur<<1, l, mid, L, R, x);
    if (R > mid) update(cur<<1|1, mid+1, r, L, R, x);
    push_up(cur);
}
 
// query 最小值
int query(int cur, int l, int r, int L, int R) {
    if (L <= l && R >= r) {
        return tr[cur].mn;
    }
    push_down(cur);
    int mid = (l+r) >> 1;
    int res = 1e9;
    if (L <= mid) res = min(res, query(cur<<1, l, mid, L, R));
    if (R > mid) res = min(res, query(cur<<1|1, mid+1, r, L, R));
    push_up(cur);
    return res;
}
 
void build(int cur, int l, int r) {
    if (l == r) {
        tr[cur].mn = c[l];
        return;
    }
    int mid = (l+r) >> 1;
    build(cur<<1, l, mid);
    build(cur<<1|1, mid+1, r);
    push_up(cur);
}
 
 
int main() {
    cin >> n >> m >> Q;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        if (i > 1 && a[i] < a[1]) cnt[1]++;
    }
    for (int i = 1; i <= m; i++) {
        int r; cin >> r;
        sum_r[i] = sum_r[i-1] + r;
        cnt[i+1] = cnt[i];
 
        while (r--) {
            int x; cin >> x;
            per[i].push_back(x);
            if (x < a[1]) cnt[i+1]++;
        }
    }
    for (int i = 1; i <= m; i++) c[i] = cnt[i] - sum_r[i];
    build(1, 1, m);
 
    while (Q--) {
        int x,y,z; cin >> x >> y >> z;
        if (x < m) {  // 注意这里特判
            if (per[x][y-1] < a[1] && z > a[1]) {  // 
                update(1, 1, m, x+1, m, -1);
            } else if (per[x][y-1] > a[1] && z < a[1]) {
                update(1, 1, m, x+1, m, 1);
            }
        }
 
        per[x][y-1] = z;
        if (query(1, 1, m, 1, m) < 0) {
            cout << 0 << "\n";
        } else cout << 1 << "\n";
    }
}