[TJOI2018] 数学计算

GitHub跳转原题关系图返回列表

不要把删除操作当成模除法,而要把每次乘法看成一个位置:插入时赋值为乘数,删除时改回 1,用线段树维护全局乘积。

OJ: luogu

题目 ID: P4588

难度:普及/提高-

标签:线段树乘积建模单点修改取模

日期: 2026-06-21 02:24

题意

初始有一个数 x = 1

接下来有 Q 次操作:

  • 1 m:把 x 变成 x * m
  • 2 pos:把 x 除以第 pos 次操作乘上的那个数

每次操作后都要输出当前:

x mod M

思路

先看一个可以直接验证想法的暴力:

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

// brute.cpp:直接维护每次乘法操作当前是否还“有效”,
// 每次输出时重新扫描所有有效乘数并求乘积。

const int MAXQ = 100000 + 5;

long long value_arr[MAXQ];
int alive[MAXQ];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;
    while (T--) {
        int q;
        long long mod_value;
        cin >> q >> mod_value;

        for (int i = 1; i <= q; i++) {
            value_arr[i] = 1;
            alive[i] = 0;
        }

        for (int i = 1; i <= q; i++) {
            int op;
            long long x;
            cin >> op >> x;

            if (op == 1) {
                value_arr[i] = x % mod_value;
                alive[i] = 1;
            } else {
                alive[(int)x] = 0;
            }

            long long ans = 1 % mod_value;
            for (int j = 1; j <= q; j++) {
                if (alive[j]) {
                    ans = ans * value_arr[j] % mod_value;
                }
            }
            cout << ans << '\n';
        }
    }

    return 0;
}

brute.cpp 记录每次乘法操作的值,以及它当前是否还有效。

每次输出前,把所有仍然有效的乘数重新乘一遍。

这个做法是对的,但每次都重扫所有位置,复杂度太高。

这题真正的关键是:不要把第二种操作理解成模意义下的除法。

因为模数 M 不保证是质数,也不保证被删掉的数和 M 互质,所以逆元路线并不可靠。

正确理解应该是:

  • 类型 1:往当前乘积集合里加入一个乘数
  • 类型 2:把某次以前加入的乘数从集合里删除

于是可以把第 i 次操作映射成一个位置:

  • 如果这次乘法当前有效,这个位置存它的乘数
  • 如果已经删除,或者本来不是乘法位置,这个位置存 1

由于 1 是乘法单位元,把一个位置改成 1 就等价于删掉这个乘数对总乘积的贡献。

这样题目就变成了:

  • 单点修改
  • 维护整个数组的乘积

用线段树维护区间乘积即可:

  • 1 m:把位置 i 改成 m
  • 2 pos:把位置 pos 改成 1

每次操作后,根节点存的就是当前所有有效乘数之积模 M

代码

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

const int MAXQ = 100000 + 5;

int q;
long long mod_value;
long long seg[MAXQ << 2];

void build(int u, int l, int r) {
    seg[u] = 1 % mod_value;
    if (l == r) {
        return;
    }

    int mid = (l + r) >> 1;
    build(u << 1, l, mid);
    build(u << 1 | 1, mid + 1, r);
}

void update(int u, int l, int r, int pos, long long val) {
    if (l == r) {
        seg[u] = val % mod_value;
        return;
    }

    int mid = (l + r) >> 1;
    if (pos <= mid) {
        update(u << 1, l, mid, pos, val);
    } else {
        update(u << 1 | 1, mid + 1, r, pos, val);
    }

    seg[u] = seg[u << 1] * seg[u << 1 | 1] % mod_value;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;
    while (T--) {
        cin >> q >> mod_value;
        build(1, 1, q);

        for (int i = 1; i <= q; i++) {
            int op;
            long long x;
            cin >> op >> x;
            if (op == 1) {
                update(1, 1, q, i, x);
            } else {
                // 删除第 x 次乘法操作,相当于把对应位置恢复成乘法单位元 1。
                update(1, 1, q, (int)x, 1);
            }
            cout << seg[1] % mod_value << '\n';
        }
    }

    return 0;
}

复杂度

对于每组数据:

  • 建树:O(Q)O(Q)
  • 每次操作:O(logQ)O(log Q)

总时间复杂度:

O(QlogQ)O(Q log Q)

空间复杂度:

O(Q)O(Q)

总结

这题的核心不是线段树本身,而是先把“删除某次乘法贡献”转成“把对应位置改回 1”。

一旦完成这个建模,后面就是一个非常干净的单点修改乘积线段树。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析