宝藏

用根号分块维护每块的未匹配删除和存活插入序列,以分块栈快速拼接双端队列并计算矩阵乘积。

OJ: shumeng

题目 ID: CSP202312D

难度:提高+/省选-

标签:根号分块数据结构矩阵乘法双端队列

日期: 2026-07-31 16:21

形式化题目

nn 条指令,每条是对矩阵双端队列的操作:在头部插入一个 2×22\times2 矩阵、在尾部插入,或删除队列中最晚插入的矩阵。支持单点修改指令,并查询:对空队列依次执行区间 [l,r][l,r] 的指令后,把队列中矩阵从头到尾相乘(元素对 998244353998244353 取模)得到的结果;队列为空时结果为单位矩阵。

思路

先看直接按区间模拟双端队列的暴力程序,它用链表记录每个插入元素的位置,删除时删掉最近插入的那个:

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:40
 */
// brute.cpp:小数据暴力解,直接用链表模拟区间内的双端队列。
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 998244353;

struct Matrix {
    long long a[2][2];
};

struct Operation {
    int type;
    Matrix matrix;
};

Matrix identity_matrix() {
    Matrix result = {{{1, 0}, {0, 1}}};
    return result;
}

Matrix multiply(const Matrix &left, const Matrix &right) {
    Matrix result = {};
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 2; j++) {
            for (int k = 0; k < 2; k++) {
                result.a[i][j] = (result.a[i][j] + left.a[i][k] * right.a[k][j]) % MOD;
            }
        }
    }
    return result;
}

Operation read_operation() {
    Operation result;
    cin >> result.type;
    if (result.type == 1 || result.type == 2) {
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 2; j++) cin >> result.matrix.a[i][j];
        }
    }
    return result;
}

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

    int n, event_count;
    cin >> n >> event_count;
    vector<Operation> operation(n);
    for (int i = 0; i < n; i++) operation[i] = read_operation();

    while (event_count--) {
        int event_type;
        cin >> event_type;
        if (event_type == 1) {
            int index;
            cin >> index;
            operation[index - 1] = read_operation();
            continue;
        }

        int left, right;
        cin >> left >> right;
        list<Matrix> queue;
        vector<list<Matrix>::iterator> inserted;
        for (int i = left - 1; i < right; i++) {
            if (operation[i].type == 1) {
                queue.push_front(operation[i].matrix);
                inserted.push_back(queue.begin());
            } else if (operation[i].type == 2) {
                queue.push_back(operation[i].matrix);
                list<Matrix>::iterator position = queue.end();
                --position;
                inserted.push_back(position);
            } else if (!inserted.empty()) {
                queue.erase(inserted.back());
                inserted.pop_back();
            }
        }

        Matrix answer = identity_matrix();
        for (list<Matrix>::iterator it = queue.begin(); it != queue.end(); ++it) {
            answer = multiply(answer, *it);
        }
        cout << answer.a[0][0] << ' ' << answer.a[0][1] << ' '
             << answer.a[1][0] << ' ' << answer.a[1][1] << '\n';
    }

    return 0;
}

一段指令的栈式摘要

删除操作总是删除最晚插入的矩阵,因此把每次插入看成压入“插入栈”,删除就是弹出栈顶。对一个固定分块从空栈开始扫描:

  • 删除时若块内栈不空,就抵消块内最后一次插入;
  • 否则记为一个未匹配删除,表示它将来会删除块外传入的栈顶;
  • 扫描结束后,栈中剩余的插入按时间顺序组成存活插入序列。

所以分块可以摘要为 pop_count 和一段存活插入序列。把分块作用到已有栈时,先弹出 pop_count 个元素,再把存活插入序列整体压入。

分块栈与矩阵聚合

设一段插入序列中,所有头插入矩阵按最终顺序的乘积为 front,所有尾插入矩阵按最终顺序的乘积为 back。先后相接的两段序列 A,BA,B 满足:

front(A+B)=front(B)×front(A),back(A+B)=back(A)×back(B)front(A+B)=front(B)\times front(A),\qquad back(A+B)=back(A)\times back(B)。

每个分块预处理存活插入序列每个前缀的 (front, back)。查询时,区间两端不足一个分块的部分逐条执行,中间完整分块作为整体压入分块栈;删除操作如果只截断当前分块,就用对应前缀聚合量重算累计值。最终答案就是 front × back

动态修改与查询

单点修改只影响所在分块,重新扫描该分块即可。一个区间至多包含两个边界分块和 O(n/B)O(n/B) 个完整分块,其中 B=320B=320,因此查询和修改复杂度均为 O(B+n/B)O(B+n/B)

代码

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:40
 */
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 998244353;
const int BLOCK_SIZE = 320;

// 2x2 矩阵
struct Matrix {
    long long a[2][2];
};

// 一条指令:type 1 头插,2 尾插,3 删除最晚插入的矩阵
struct Operation {
    int type;
    Matrix matrix;
};

// 一段插入序列聚合:front 为所有头插入按最终顺序的乘积,back 为所有尾插入的乘积
struct Aggregate {
    Matrix front;
    Matrix back;
};

// 一个分块的摘要:未匹配的删除个数 + 存活插入序列每个前缀的聚合
struct BlockSummary {
    int pop_count;
    vector<Aggregate> prefix;
};

// 分块栈中的一个元素:source 为分块编号(>=0)或单条指令(-index-1),length 为存活插入个数
struct ChunkState {
    int source;
    int length;
    Aggregate all; // 从栈底到当前元素的累计聚合
};

int n, event_count, block_count;
vector<Operation> operation;
vector<BlockSummary> block;
vector<ChunkState> chunk_stack;

Matrix identity_matrix() {
    Matrix result = {{{1, 0}, {0, 1}}};
    return result;
}

// 矩阵乘法,模 MOD
Matrix multiply(const Matrix &left, const Matrix &right) {
    Matrix result;
    result.a[0][0] = (left.a[0][0] * right.a[0][0]
        + left.a[0][1] * right.a[1][0]) % MOD;
    result.a[0][1] = (left.a[0][0] * right.a[0][1]
        + left.a[0][1] * right.a[1][1]) % MOD;
    result.a[1][0] = (left.a[1][0] * right.a[0][0]
        + left.a[1][1] * right.a[1][0]) % MOD;
    result.a[1][1] = (left.a[1][0] * right.a[0][1]
        + left.a[1][1] * right.a[1][1]) % MOD;
    return result;
}

Aggregate identity_aggregate() {
    Matrix identity = identity_matrix();
    return {identity, identity};
}

// 拼接两段插入序列:先出现 left 再出现 right。
// 头插入的最终顺序与出现顺序相反,尾插入的顺序相同。
Aggregate concatenate(const Aggregate &left, const Aggregate &right) {
    return {multiply(right.front, left.front), multiply(left.back, right.back)};
}

// 单条插入指令自身作为一个聚合
Aggregate operation_aggregate(int index) {
    Aggregate result = identity_aggregate();
    if (operation[index].type == 1) result.front = operation[index].matrix;
    if (operation[index].type == 2) result.back = operation[index].matrix;
    return result;
}

// 取一段长度为 length 的插入序列的聚合。
// source >= 0 表示分块编号,否则表示单条指令 -source-1。
Aggregate chunk_aggregate(int source, int length) {
    if (source >= 0) return block[source].prefix[length];
    if (length == 0) return identity_aggregate();
    return operation_aggregate(-source - 1);
}

// 重新扫描一个分块,生成其摘要:先抵消块内的删除,再对存活插入做前缀聚合
void rebuild_block(int id) {
    int left = id * BLOCK_SIZE;
    int right = min(n, left + BLOCK_SIZE);
    vector<int> push_operation; // 尚未被块内删除抵消的插入指令
    block[id].pop_count = 0;
    for (int i = left; i < right; i++) {
        if (operation[i].type == 3) {
            if (push_operation.empty()) block[id].pop_count++; // 未匹配删除
            else push_operation.pop_back();                    // 抵消最近一次插入
        } else {
            push_operation.push_back(i);
        }
    }

    // 对存活插入序列做前缀聚合
    block[id].prefix.clear();
    block[id].prefix.resize(push_operation.size() + 1, identity_aggregate());
    for (int i = 0; i < (int)push_operation.size(); i++) {
        block[id].prefix[i + 1] = block[id].prefix[i];
        int index = push_operation[i];
        if (operation[index].type == 1) {
            block[id].prefix[i + 1].front = multiply(
                operation[index].matrix, block[id].prefix[i].front);
        } else {
            block[id].prefix[i + 1].back = multiply(
                block[id].prefix[i].back, operation[index].matrix);
        }
    }
}

// 从分块栈顶弹出 count 个插入元素(模拟连续删除)
void pop_elements(int count) {
    while (count > 0 && !chunk_stack.empty()) {
        ChunkState &last = chunk_stack.back();
        if (last.length <= count) {
            count -= last.length;
            chunk_stack.pop_back();
        } else {
            // 只截断栈顶分块:用截断后的前缀聚合重算累计值
            last.length -= count;
            Aggregate before = chunk_stack.size() == 1
                ? identity_aggregate() : chunk_stack[chunk_stack.size() - 2].all;
            last.all = concatenate(before, chunk_aggregate(last.source, last.length));
            count = 0;
        }
    }
}

// 把一个完整分块压入栈,length 为它存活插入序列的长度
void append_chunk(int source, int length) {
    if (length == 0) return;
    Aggregate before = chunk_stack.empty() ? identity_aggregate() : chunk_stack.back().all;
    Aggregate current = chunk_aggregate(source, length);
    chunk_stack.push_back({source, length, concatenate(before, current)});
}

// 把单条指令作用到分块栈上
void apply_operation(int index) {
    if (operation[index].type == 3) {
        pop_elements(1);
    } else {
        Aggregate before = chunk_stack.empty() ? identity_aggregate() : chunk_stack.back().all;
        Aggregate current = before;
        if (operation[index].type == 1) {
            current.front = multiply(operation[index].matrix, before.front);
        } else {
            current.back = multiply(before.back, operation[index].matrix);
        }
        chunk_stack.push_back({-index - 1, 1, current});
    }
}

// 把整个分块作为摘要作用到栈上:先处理未匹配删除,再压入存活插入序列
void apply_block(int id) {
    pop_elements(block[id].pop_count);
    append_chunk(id, (int)block[id].prefix.size() - 1);
}

// 对区间 [left, right] 执行所有指令,返回最终队列矩阵乘积
Matrix query(int left, int right) {
    chunk_stack.clear();
    int index = left;
    while (index <= right) {
        int id = index / BLOCK_SIZE;
        int end = min(right, (id + 1) * BLOCK_SIZE - 1);
        // 完整块用摘要整体处理,边界块逐条执行
        if (index == id * BLOCK_SIZE && end - index + 1 == BLOCK_SIZE) {
            apply_block(id);
        } else {
            for (int i = index; i <= end; i++) apply_operation(i);
        }
        index = end + 1;
    }
    if (chunk_stack.empty()) return identity_matrix();
    return multiply(chunk_stack.back().all.front, chunk_stack.back().all.back);
}

// 读入一条指令
Operation read_operation() {
    Operation result;
    cin >> result.type;
    if (result.type == 1 || result.type == 2) {
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 2; j++) cin >> result.matrix.a[i][j];
        }
    }
    return result;
}

void print_matrix(const Matrix &matrix) {
    cout << matrix.a[0][0] << ' ' << matrix.a[0][1] << ' '
         << matrix.a[1][0] << ' ' << matrix.a[1][1] << '\n';
}

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

    cin >> n >> event_count;
    operation.resize(n);
    for (int i = 0; i < n; i++) operation[i] = read_operation();

    // 预处理每个分块的摘要
    block_count = (n + BLOCK_SIZE - 1) / BLOCK_SIZE;
    block.resize(block_count);
    for (int i = 0; i < block_count; i++) rebuild_block(i);

    while (event_count--) {
        int type;
        cin >> type;
        if (type == 1) {
            // 单点修改:更新指令并重建所在分块
            int index;
            cin >> index;
            operation[index - 1] = read_operation();
            rebuild_block((index - 1) / BLOCK_SIZE);
        } else {
            int left, right;
            cin >> left >> right;
            print_matrix(query(left - 1, right - 1));
        }
    }

    return 0;
}

复杂度

设分块长 B=320B=320。预处理所有分块为 O(n)O(n);单点修改为 O(B)O(B),一次区间查询为 O(B+n/B)O(B+n/B);空间复杂度为 O(n)O(n)。矩阵乘法只涉及固定的 2×22\times2 矩阵,单次乘法是常数时间。

总结

删除“最晚插入”使指令具有栈结构,根号分块可以把每块压缩成有限的未匹配删除和存活插入序列。分别维护头插入和尾插入的矩阵乘积,就能在动态修改下快速回答区间密码。