【ACWing】1275. 最大数

题目地址:

https://www.acwing.com/problem/content/1277/

给定一个正整数数列 a 1 , a 2 , … , a n a_1,a_2,…,a_n a1,a2,,an,每一个数都在 0 ∼ p − 1 0∼p−1 0p1之间。可以对这列数进行两种操作:添加操作:向序列后添加一个数,序列长度变成 n + 1 n+1 n+1;询问操作:询问这个序列中最后 L L L个数中最大的数是多少。程序运行的最开始,整数序列为空。写一个程序,读入操作的序列,并输出询问操作的答案。

输入格式:
第一行有两个正整数 m , p m,p m,p,意义如题目描述;接下来 m m m行,每一行表示一个操作。如果该行的内容是 Q   L Q\ L Q L,则表示这个操作是询问序列中最后 L L L个数的最大数是多少;如果是 A   t A\ t A t,则表示向序列后面加一个数,加入的数是 ( t + a ) m o d    p (t+a) \mod p (t+a)modp。其中, t t t是输入的参数, a a a是在这个添加操作之前最后一个询问操作的答案(如果之前没有询问操作,则 a = 0 a=0 a=0)。第一个操作一定是添加操作。对于询问操作, L > 0 L>0 L>0且不超过当前序列的长度。

输出格式:
对于每一个询问操作,输出一行。该行只有一个数,即序列中最后 L L L个数的最大数。

数据范围:
1 ≤ m ≤ 2 × 1 0 5 1≤m≤2×10^5 1m2×105
1 ≤ p ≤ 2 × 1 0 9 1≤p≤2×10^9 1p2×109
0 ≤ t < p 0≤t<p 0t<p

涉及到单点修改和区间查询的操作,可以用线段树来做。线段树是一个近似完全二叉树的结构,其每个节点存储的是某个区间的信息,并且对每个节点而言,其左半子区间的信息存在其左儿子节点里,右半子区间的信息存在其右儿子节点里。由于其近似完全二叉树,所以一般用数组来存储这棵树。树的最后一层不能保证是满的,但是倒数第二层可以保证是满的,其倒数第二层的节点个数不超过区间长度(设区间长度为 n n n),从而最后一层的节点个数不超过 2 n 2n 2n,总共节点个数不超过 4 n 4n 4n,所以一般开 4 n 4n 4n个节点来存线段树。经典的线段树有如下几个基本操作:
1、初始化树:作用是求一下每个节点需要维护的区间是哪个区间,即维护的区间的左右端点是什么;并且,初始化各个区间的信息(初始化是自底向上的。初始化好子区间之后再处理当前区间)。能用线段树的前提是,某个区间的信息可以由其左右两个子区间的信息汇总得到(例如,大区间的和可以由两个子区间的和加起来得到,大区间的最大值可以由两个子区间的最大值加起来得到,等等);
2、询问:即询问某个区间的信息。如果询问的区间是 [ l , r ] [l,r] [l,r],而当前节点维护的区间是 [ t l , t r ] [t_l,t_r] [tl,tr]的话,那么有几种情况:如果 [ t l , t r ] ⊂ [ l , r ] [t_l,t_r]\subset [l,r] [tl,tr][l,r],那么直接返回当前节点的信息即可;否则的话,设 [ l , r ] [l,r] [l,r]的中点是 m m m,如果 l ≤ m l\le m lm,则需要询问左半边,如果 r > m r>m r>m则需要询问右半边,然后将左右两个半边询问的结果汇总即可;
3、单点修改:即修改某一点 x x x的值,使之成为 v v v。可以递归地做,如果当前节点的区间就是单点 x x x,那么直接赋值即可;否则的话,设当前节点维护的区间是 [ t l , t r ] [t_l,t_r] [tl,tr],设维护的区间的中点是 m m m,那么如果 x ≤ m x\le m xm,则去修改左半子区间,如果 x > m x>m x>m则去修改右半子区间;修改完子区间之后再修改当前节点维护的区间(这一步叫做pushup)。

#include <iostream>
using namespace std;

const int N = 200010;
int m, p;
// 这里开了4N个节点,每个节点x的左右孩子分别是2x和2x + 1,即树根的下标是1
struct Node {
    
    
	// l和r存当前树节点维护的区间的左右端点;v存被维护的区间的最大值
    int l, r;
    int v;
} tr[4 * N];

void pushup(int u) {
    
    
    tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
}

void build(int u, int l, int r) {
    
    
    tr[u] = {
    
    l, r};
    if (l == r) return;
    int m = l + (r - l >> 1);
    // 递归建立左右子树
    build(u << 1, l, m), build(u << 1 | 1, m + 1, r);
}

// u是当前节点在线段树数组里的下标,l和r是要查询的区间的左右端点
int query(int u, int l, int r) {
    
    
	// 如果当前维护的区间完全包含在[l, r]中了,则直接返回存的信息v
    if (l <= tr[u].l && tr[u].r <= r) return tr[u].v;

	// 求一下当前节点维护的区间的中点
    int m = tr[u].l + (tr[u].r - tr[u].l >> 1);
    // 分别存左右子区间的查询结果
    int x = 0, y = 0;
    if (l <= m) x = query(u << 1, l, r);
    if (r > m) y = query(u << 1 | 1, l, r);

    return max(x, y);
}

// u含义同上,要把原区间的x这个位置的数修改为v
void modify(int u, int x, int v) {
    
    
    if (tr[u].l == x && tr[u].r == x) tr[u].v = v;
    else {
    
    
        int m = tr[u].l + (tr[u].r - tr[u].l >> 1);
        if (x <= m) modify(u << 1, x, v);
        else modify(u << 1 | 1, x, v);

        pushup(u);
    }
}

int main() {
    
    
    int n = 0, last = 0;
    cin >> m >> p;
    build(1, 1, m);

    int x;
    char op[2];
    while (m--) {
    
    
        scanf("%s%d", op, &x);
        if (op[0] == 'Q') {
    
    
            last = query(1, n - x + 1, n);
            printf("%d\n", last);
        } else {
    
    
            modify(1, n + 1, (last + x) % p);
            n++;
        }
    }
    
    return 0;
}

每次操作时间复杂度都是 O ( log ⁡ n ) O(\log n) O(logn) n n n是线段树维护的区间的长度。空间 O ( n ) O(n) O(n)

猜你喜欢

转载自blog.csdn.net/qq_46105170/article/details/115157373