线段树入门:[JSOI2008] 最大数

线段树入门:[JSOI2008] 最大数

题目描述

现在请求你维护一个数列,要求提供以下两种操作:

1. 查询操作

  • 语法Q L
  • 功能:查询当前数列中末尾 L 个数中的最大的数,并输出这个数的值。
  • 限制:L 不超过当前数列的长度(L > 0)

2. 插入操作

  • 语法A n
  • 功能:将 n 加上 t,其中 t 是最近一次查询操作的答案(如果还未执行过查询操作,则 t = 0),并将所得结果对一个固定的常数 D 取模,将所得答案插入到数列的末尾。
  • 限制:n 是整数(可能为负数)并且在长整范围内。

注意事项

  • 初始时数列是空的,没有一个数。

基本概念:什么是线段树?

线段树简介

线段树(Segment Tree) 是一种二叉树结构,每个节点代表一个区间,用于高效地维护和查询区间信息。它是一种区间查询与修改的经典数据结构。

核心特点

  1. 完全二叉树:线段树是一棵完全二叉树
  2. 区间表示:每个节点存储一个区间的信息
  3. 高效操作
  • 单点修改:O(log n)
  • 区间查询:O(log n)
  • 区间修改:O(log n)
  1. 空间复杂度:O(n)

线段树的构建

区间 [1, 8]
├── [1, 4]
│   ├── [1, 2]
│   │   ├── [1, 1]
│   │   └── [2, 2]
│   └── [3, 4]
│       ├── [3, 3]
│       └── [4, 4]
└── [5, 8]
    ├── [5, 6]
    │   ├── [5, 5]
    │   └── [6, 6]
    └── [7, 8]
        ├── [7, 7]
        └── [8, 8]

解题思路

题目分析

要求维护一个初始为空的数列,支持两种操作:

  1. 查询操作 Q L:查询当前数列中最后 L 个数的最大值
  2. 插入操作 A n:将 (n + t) % d 插入到数列末尾,其中 t 是上一次查询的结果(初始为 0),d 是给定的模数

关键问题

由于操作次数 M 最多为 200,000,每次查询如果遍历最后 L 个数会超时(O(M))。

解决方案

使用线段树实现 O(log M) 时间的区间最大值查询和单点修改。


核心实现

1. 数据结构设计

struct Node {
    int l, r;  // 区间的左右边界
    int v;     // 该区间的最大值
} tr[N * 4];  // 线段树需要开 4 倍空间

2. 初始化

void build(int u, int l, int r) {
    tr[u].l = l, tr[u].r = r;
    if (l == r) {
        tr[u].v = INT_MIN;  // 初始化为一个非常小的数
        return;
    }

    int mid = (l + r) >> 1;
    build(u << 1, l, mid);      // 递归构建左子树
    build(u << 1 | 1, mid + 1, r); // 递归构建右子树
}

重要细节:将所有节点初始化为 INT_MIN,因为查询的区间可能包含尚未插入数的位置,如果不初始化为极小值,这些位置的默认值 0 可能会影响负数情况下的查询结果。

3. 单点修改

void update(int u, int x, int v) {
    if (tr[u].l == x && tr[u].r == x) {  // 如果当前节点是叶子节点
        tr[u].v = v;  // 直接修改
        return;
    }

    int mid = (tr[u].l + tr[u].r) >> 1;
    if (x <= mid)
        update(u << 1, x, v);      // 目标在左子树
    else
        update(u << 1 | 1, x, v);  // 目标在右子树

    pushup(u);  // 子节点更新父节点的最大值
}

4. 区间查询

int query(int u, int l, int r) {
    // 如果当前区间完全包含在查询区间内,直接返回该节点的最大值
    if (l <= tr[u].l && tr[u].r <= r)
        return tr[u].v;

    int mid = (tr[u].l + tr[u].r) >> 1;
    int res = INT_MIN;  // 有可能是负数
    if (l <= mid)
        res = max(res, query(u << 1, l, r));  // 左子树与查询区间有重叠
    if (r > mid)
        res = max(res, query(u << 1 | 1, l, r));  // 右子树与查询区间有重叠

    return res;
}

5. 向上更新

void pushup(int u) {
    tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);  // 左右子节点的最大值更新父节点
}

完整代码实现

结构体版

#include <iostream>
#include <cstdio>
#include <climits>
using namespace std;
typedef long long LL;  // x + a 可能是 LL
const int N = 2e5 + 10;  // 最多操作次数也是线段树的最大长度

struct Node {  // 线段树节点结构体
    int l, r;  // 节点所代表的区间 [l, r]
    int v;     // 该区间的最大值
} tr[N * 4];  // 线段树需要开 4 倍空间

int M;  // 操作次数 M
int D;  // 模数 D

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) {  // 预先构建覆盖[1, M]的线段树
    tr[u].l = l, tr[u].r = r;
    if (l == r) {
        tr[u].v = INT_MIN;  // 初始化为一个非常小的数
        return;
    }

    int mid = (l + r) >> 1;
    build(u << 1, l, mid);      // 递归构建左子树
    build(u << 1 | 1, mid + 1, r);  // 递归构建右子树
}

// 单点修改:将位置 x 的值修改为 v
void update(int u, int x, int v) {
    if (tr[u].l == x && tr[u].r == x) {  // 如果当前节点是叶子节点
        tr[u].v = v;  // 直接修改
        return;
    }
    int mid = (tr[u].l + tr[u].r) >> 1;
    if (x <= mid)
        update(u << 1, x, v);  // 目标在左子树
    else
        update(u << 1 | 1, x, v);  // 目标在右子树

    pushup(u);  // 子节点更新父节点的最大值
}

// 区间查询:查询区间 [l, r] 的最大值
int query(int u, int l, int r) {
    // 如果当前区间完全包含在查询区间内,直接返回该节点的最大值
    if (l <= tr[u].l && tr[u].r <= r)
        return tr[u].v;

    int mid = (tr[u].l + tr[u].r) >> 1;
    int res = INT_MIN;  // 有可能是负数
    if (l <= mid)
        res = max(res, query(u << 1, l, r));  // 左子树与查询区间有重叠
    if (r > mid)
        res = max(res, query(u << 1 | 1, l, r));  // 右子树与查询区间有重叠

    return res;
}

int main() {
    scanf("%d%d", &M, &D);
    int n = 0;  // 当前数列的实际长度
    int a = 0;  // 上一次查询的结果 初始 0

    build(1, 1, M);  // 构建一棵大小为 M 的线段树(最多有 M 个数)

    while (M--) {  // M 次处理
        char op[2];  // 操作类型
        scanf("%s", op);
        if (op[0] == 'A') {  // 插入操作
            LL x;  // x 是整数(可能为负数)并且在长整范围内
            scanf("%lld", &x);
            ++n;  // 更新数列长度
            int v = (x + a) % D;  // 计算要更新的值
            update(1, n, v);  // 在位置 n 更新为 v
        } else {  // 查询操作
            int L;  // 末尾 L 个数
            scanf("%d", &L);
            a = query(1, n - L + 1, n);  // 查询末尾 L 个数中的最大的数:[n - L + 1, n]
            printf("%d\n", a);
        }
    }

    return 0;
}

非结构体版

#include <iostream>
#include <cstdio>
#include <climits>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;

int tr[N * 4];  // 线段树数组

// 单点更新:将 p 位置的值更新为 v
void update(int u, int l, int r, int p, int v) {
    if (l == r) {
        tr[u] = v;  // 直接修改
        return;
    }
    int mid = (l + r) >> 1;
    if (p <= mid)
        update(u << 1, l, mid, p, v);  // 目标在左子树
    else
        update(u << 1 | 1, mid + 1, r, p, v);  // 目标在右子树
    tr[u] = max(tr[u << 1], tr[u << 1 | 1]);  // 子节点更新父节点的最大值
}

// 区间查询 [L, R] 的最大值
int query(int u, int l, int r, int L, int R) {
    if (L <= l && r <= R)
        return tr[u];

    int mid = (l + r) >> 1;
    int v = INT_MIN;  // 有可能是负数
    if (L <= mid)
        v = max(v, query(u << 1, l, mid, L, R));  // 左子树与查询区间有重叠
    if (R > mid)
        v = max(v, query(u << 1 | 1, mid + 1, r, L, R));  // 右子树与查询区间有重叠
    return v;
}

int main() {
    int m, d;
    scanf("%d%d", &m, &d);
    fill(tr, tr + N * 4, INT_MIN);  // 初始化线段树所有节点为极小值

    int a = 0;  // 上一次查询的结果
    int n = 0;  // 当前数列长度

    for (int i = 0; i < m; i++) {  // 不能用 while(m--) m 不能变化
        char op[2];
        scanf("%s", op);
        if (op[0] == 'A') {  // 添加操作
            LL x;
            scanf("%lld", &x);
            n++;
            int v = (x + a) % d;  // 需要取模
            update(1, 1, m, n, v);  // 最多有 m 个位置
        } else {  // 查询操作
            int L;  // 查询末尾 L 个数的最大值
            scanf("%d", &L);
            a = query(1, 1, m, n - L + 1, n);
            printf("%d\n", a);
        }
    }

    return 0;
}

复杂度分析

时间复杂度

  • 建树:O(M)
  • 单点修改:O(log M)
  • 区间查询:O(log M)
  • 总复杂度:O(M log M)

空间复杂度

  • 线段树空间:O(M)
  • 总空间:O(M)

关键点总结

  1. 数组存储:使用数组存储树结构(非结构体)
  2. 动态插入:每次插入时,n 增加 1,调用单点更新
  3. 区间查询:查询区间 [n – L + 1, n] 的最大值
  4. 取模运算:插入的值是 (x + a) % d,其中 x 是输入的数
  5. 初始化:线段树一开始全部设成极小值,这样没插入的位置不会影响查询

参考链接

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇