魔数

枚举五个模乘常数的 32 个闭包状态,用线段树维护状态置换后的区间和。

OJ: shumeng

题目 ID: CSP201912E

难度:提高+/省选-

标签:线段树懒标记群论区间修改

日期: 2026-07-31 16:21

形式化题目

初始序列 Ai=iA_i = i。每次给出区间 [l,r][l,r],先输出

s=i=lrf(Ai),f(x)=(xmodP)mod2019, s=\sum_{i=l}^{r} f(A_i),\qquad f(x)=(x\bmod P)\bmod 2019,

再令区间内全部元素乘给定的 Usmod5U_{s\bmod 5},乘法在模 P=2009731336725594113P = 2009731336725594113 下进行。需要在线处理所有查询,nn 最大 10610^6qq 最大 10510^5

思路

朴素做法

小数据暴力解逐项保存模 PP 后的数,按定义求和和相乘,用于对拍。

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 22:41
 */
// brute.cpp:小数据直接维护每个数,逐项求和并逐项乘法更新。
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 2009731336725594113LL; // 第一层模数
const int MOD_SMALL = 2019;                  // 第二层模数

// 五个魔数 U[0..4]
long long unit[5] = {
    314882150829468584LL,
    427197303358170108LL,
    1022292690726729920LL,
    1698479428772363217LL,
    2006101093849356424LL,
};

long long multiply_mod(long long left, long long right) {
    return (long long)((__int128)left * right % MOD);
}

// 第二层取模:f(x) = (x mod P) mod 2019。
int get_value(long long number) {
    return (int)(number % MOD_SMALL);
}

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

    int n, query_count;
    cin >> n >> query_count;
    vector<long long> value(n + 1);
    for (int i = 1; i <= n; i++) value[i] = i;
    while (query_count--) {
        int left, right;
        cin >> left >> right;
        int answer = 0;
        for (int i = left; i <= right; i++) answer += get_value(value[i]);
        cout << answer << '\n';
        int type = answer % 5;
        for (int i = left; i <= right; i++) value[i] = multiply_mod(value[i], unit[type]);
    }

    return 0;
}

五个乘数生成的 32 状态闭包

直接维护 AiA_i 没有意义,因为一次区间乘法会改动很多位置。关键在五个给定的乘数:从模 PP 下的单位元 11 出发,不断乘任意一个 U0U4U_0\dots U_4 并去重,闭包恰好只有 32 个元素。把它们编号为 G0G31G_0\dots G_{31},其中 G0=1G_0=1

因此任意时刻的值都能写成 Ai=iGpA_i = i \cdot G_p。在线段树的每个节点保存 32 个和:

sum[j]=i 在该节点区间f(AiGj). sum[j]=\sum_{i\text{ 在该节点区间}} f(A_i\cdot G_j).

真正要输出的值是 sum[0]sum[0]。若这个节点被乘上 UtU_t,新的第 jj 项为

f((AiUt)Gj)=f(Ai(UtGj)). f((A_i\cdot U_t)\cdot G_j)=f(A_i\cdot(U_t\cdot G_j)).

所以 sum[j]sum[j] 只是原数组中状态 UtGjU_t\cdot G_j 对应项的值,即一次长度为 32 的置换。把这个置换写入懒标记即可延后传给子节点。

样例推演

样例的每一步如下。最后一列只展示第二层取模后的值;它说明区间和先决定乘数类型,再改变后续查询结果。

查询 区间 输出和 ss 乘数类型 smod5s \bmod 5 更新后的 f(A1..A4)f(A_1..A_4)
1 [1,3] 6 1 1713, 1407, 1101, 4
2 [3,4] 1105 0 1713, 1407, 1735, 128
3 [3,3] 1735 0 1713, 1407, 1624, 128
4 [1,3] 4744 4 1550, 1081, 1065, 128

正确性

初始时 Ai=i=iG0A_i=i=i\cdot G_0。每次更新都只乘一个闭包中的元素,两个闭包元素相乘仍在闭包内,因此所有可能的 AiA_i 都被 32 个状态覆盖。叶子节点按定义保存 32 种附加乘数后的值,父节点逐项相加,故任意节点的 sum[j]sum[j] 都满足定义。乘 UtU_t 时的状态置换公式恰好给出更新后的 32 个定义值,懒标记只推迟、不改变置换。于是查询得到的 sum[0]sum[0] 正是更新前的区间和,之后按其模 5 进行区间置换也和题意完全一致。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 22:41
 */
#include <bits/stdc++.h>
using namespace std;

const int BASE = 1 << 20;        // 不小于 n 的二次幂
const int NODE_COUNT = BASE << 1; // 线段树节点数
const int MAX_STATE = 32;         // 五个乘数在模 P 下生成的乘法闭包大小
const long long MOD = 2009731336725594113LL; // 第一层模数
const int MOD_SMALL = 2019;       // 第二层模数

// 五个魔数 U[0..4]
long long unit[5] = {
    314882150829468584LL,
    427197303358170108LL,
    1022292690726729920LL,
    1698479428772363217LL,
    2006101093849356424LL,
};

int n, query_count, state_count;
int segment[NODE_COUNT][MAX_STATE]; // segment[node][j]:该节点区间内各元素乘上附加状态 G[j] 后 f 值之和
unsigned char lazy_tag[NODE_COUNT]; // 懒标记:节点待应用的乘数状态
int multiply_state[MAX_STATE][MAX_STATE]; // multiply_state[a][b] = G[a]*G[b] 对应的状态编号
int unit_state[5];                // 五个 U 对应的状态编号
vector<long long> state_multiplier; // G[0..31],G[0]=1 是所有可达乘数的闭包

// 模 P 下的乘法(用 __int128 避免中间溢出)。
long long multiply_mod(long long left, long long right) {
    return (long long)((__int128)left * right % MOD);
}

// 查找某个乘数值对应的状态编号。
int find_state(long long value) {
    for (int i = 0; i < (int)state_multiplier.size(); i++) {
        if (state_multiplier[i] == value) return i;
    }
    return -1;
}

// 从 1 出发不断乘任意一个 U,BFS 生成全部可达乘数的闭包 G。
void build_states() {
    state_multiplier.push_back(1);
    for (int position = 0; position < (int)state_multiplier.size(); position++) {
        for (int i = 0; i < 5; i++) {
            long long next = multiply_mod(state_multiplier[position], unit[i]);
            if (find_state(next) == -1) state_multiplier.push_back(next);
        }
    }
    state_count = (int)state_multiplier.size();
    for (int i = 0; i < 5; i++) unit_state[i] = find_state(unit[i]);
    for (int i = 0; i < state_count; i++) {
        for (int j = 0; j < state_count; j++) {
            multiply_state[i][j] = find_state(multiply_mod(state_multiplier[i], state_multiplier[j]));
        }
    }
}

// 把节点整体乘上状态 state:32 项按乘法表重排,并合并进懒标记。
void apply_state(int node, int state) {
    if (state == 0) return;
    int old_sum[MAX_STATE];
    for (int i = 0; i < state_count; i++) old_sum[i] = segment[node][i];
    for (int i = 0; i < state_count; i++) {
        segment[node][i] = old_sum[multiply_state[state][i]];
    }
    lazy_tag[node] = (unsigned char)multiply_state[lazy_tag[node]][state];
}

// 下传懒标记到两个子节点。
void push_down(int node) {
    if (lazy_tag[node] == 0) return;
    apply_state(node << 1, lazy_tag[node]);
    apply_state(node << 1 | 1, lazy_tag[node]);
    lazy_tag[node] = 0;
}

// 用两个子节点的 32 项和更新父节点。
void pull_up(int node) {
    for (int i = 0; i < state_count; i++) {
        segment[node][i] = segment[node << 1][i] + segment[node << 1 | 1][i];
    }
}

void update(int node, int left, int right, int query_left, int query_right, int state) {
    if (query_left <= left && right <= query_right) {
        apply_state(node, state);
        return;
    }
    push_down(node);
    int middle = (left + right) >> 1;
    if (query_left <= middle) update(node << 1, left, middle, query_left, query_right, state);
    if (query_right > middle) update(node << 1 | 1, middle + 1, right, query_left, query_right, state);
    pull_up(node);
}

int query(int node, int left, int right, int query_left, int query_right) {
    if (query_left <= left && right <= query_right) return segment[node][0];
    push_down(node);
    int middle = (left + right) >> 1;
    int answer = 0;
    if (query_left <= middle) answer += query(node << 1, left, middle, query_left, query_right);
    if (query_right > middle) answer += query(node << 1 | 1, middle + 1, right, query_left, query_right);
    return answer;
}

// 建树:叶子 i 存 i 乘上每个附加状态 G[j] 后 f 值的前缀和。
void build_segment_tree() {
    long long remainder[MAX_STATE] = {};
    for (int i = 1; i <= n; i++) {
        int node = BASE + i - 1;
        for (int j = 0; j < state_count; j++) {
            remainder[j] += state_multiplier[j];
            if (remainder[j] >= MOD) remainder[j] -= MOD;
            segment[node][j] = (int)(remainder[j] % MOD_SMALL);
        }
    }
    for (int node = BASE - 1; node >= 1; node--) pull_up(node);
}

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

    cin >> n >> query_count;
    build_states();
    build_segment_tree();
    while (query_count--) {
        int left, right;
        cin >> left >> right;
        // 单位元状态 sum[0] 就是当前区间和,再按 s mod 5 选择乘数做区间更新。
        int answer = query(1, 1, BASE, left, right);
        cout << answer << '\n';
        update(1, 1, BASE, left, right, unit_state[answer % 5]);
    }

    return 0;
}

复杂度

状态数固定为 32。令 BASE=220BASE=2^{20},它是不小于 10610^6 的最小二次幂。建树时间为 O(32BASE)O(32 \cdot BASE),每次区间查询和区间更新均为 O(32logn)O(32\log n)。树数组有 2BASE2 \cdot BASE 个节点,每项使用 int,约 256 MiB;加上懒标记后总空间为 O(32BASE)O(32 \cdot BASE)

总结

看似任意的大整数区间乘法,被五个特殊常数限制在一个 32 元的有限乘法状态集合中。把“元素当前乘数”转成“节点在所有附加状态下的答案”,区间乘法就从逐点修改变成了固定置换。

图示解析

这张图展示从魔数闭包到线段树操作的主线:

text
五个 U 的模 P 乘法闭包(32 个状态)
`- 每个 A[i] = i * G[p]
   `- 节点保存每个附加 G[j] 的 f 值和
      |- 查询:读取单位元状态 sum[0]
      `- 更新:按 U[t] * G[j] 重排 32 项并记录懒标记

32 个状态不是额外的近似,而是给定五个常数在模 PP 下所有可达乘数的完整集合。线段树始终同时维护这些状态下的答案,因此一次更新只需替换状态下标,不需要逐项重新计算 ff