区块链

用父指针共享链前缀,并按时刻合并到达消息后进行事件模拟。

OJ: shumeng

题目 ID: CSP201912D

难度:提高+/省选-

标签:图论模拟事件模拟链式结构

日期: 2026-07-31 16:21

形式化题目

nn 个节点通过无向边相连,每个节点维护一条从创世块 0 开始的主链:收到更长的链时替换,收到等长链时选择末块编号更小的链;产生一个新块时,把它接在当前主链末尾。一条链经过每条边都恰好延迟 tt 个逻辑时刻。给出按时间非递减排列的产生块和查询操作,模拟每个节点在查询时的主链。一个时刻内必须先接收所有链,再产生块;查询排在该时刻所有产生块之后。

思路

朴素做法

小数据暴力解直接在消息中复制完整的 vector<int> 链,保留相同的事件顺序,用于对拍。

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 int MAXN = 20;

// 消息携带完整链:time 到达时刻,node 接收节点,chain 整条链的块编号序列。
struct Message {
    long long time;
    int node;
    vector<int> chain;
};

struct MessageOrder {
    bool operator()(const Message &left, const Message &right) const {
        return left.time > right.time;
    }
};

struct Operation {
    int node, block;
    long long time;
    bool is_query;
};

int n, m, delay_time;
vector<int> graph[MAXN], current_chain[MAXN], incoming_chain[MAXN];
bool received[MAXN];
priority_queue<Message, vector<Message>, MessageOrder> message_queue;

// 判断链 left 是否优于链 right;right 为空表示没有任何候选。
bool is_better(const vector<int> &left, const vector<int> &right) {
    if (right.empty()) return true;
    if (left.size() != right.size()) return left.size() > right.size();
    return left.back() < right.back();
}

void send_chain(int node, long long time) {
    for (int i = 0; i < (int)graph[node].size(); i++) {
        Message message = {time + delay_time, graph[node][i], current_chain[node]};
        message_queue.push(message);
    }
}

// 处理同一时刻到达的消息:先合并最优链,再统一更新并转发。
void process_messages(long long time) {
    vector<int> touched;
    while (!message_queue.empty() && message_queue.top().time == time) {
        Message message = message_queue.top();
        message_queue.pop();
        if (!received[message.node]) {
            received[message.node] = true;
            touched.push_back(message.node);
        }
        if (is_better(message.chain, incoming_chain[message.node])) {
            incoming_chain[message.node] = message.chain;
        }
    }
    for (int i = 0; i < (int)touched.size(); i++) {
        int node = touched[i];
        if (is_better(incoming_chain[node], current_chain[node])) {
            current_chain[node] = incoming_chain[node];
            send_chain(node, time);
        }
        incoming_chain[node].clear();
        received[node] = false;
    }
}

void print_chain(int node) {
    cout << current_chain[node].size();
    for (int i = 0; i < (int)current_chain[node].size(); i++) {
        cout << ' ' << current_chain[node][i];
    }
    cout << '\n';
}

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

    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        graph[u].push_back(v);
        graph[v].push_back(u);
    }
    int operation_count;
    cin >> delay_time >> operation_count;
    string line;
    getline(cin, line);

    vector<Operation> operations;
    for (int i = 0; i < operation_count; i++) {
        getline(cin, line);
        stringstream input(line);
        vector<long long> value;
        long long number;
        while (input >> number) value.push_back(number);
        Operation operation;
        operation.node = (int)value[0];
        operation.time = value[1];
        operation.is_query = value.size() == 2;
        operation.block = operation.is_query ? 0 : (int)value[2];
        operations.push_back(operation);
    }

    for (int i = 1; i <= n; i++) current_chain[i].push_back(0); // 创世块

    int operation_index = 0;
    while (operation_index < operation_count) {
        long long now = operations[operation_index].time;
        while (!message_queue.empty() && message_queue.top().time <= now) {
            process_messages(message_queue.top().time);
        }

        int end = operation_index;
        while (end < operation_count && operations[end].time == now) end++;
        vector<int> changed_nodes;
        for (int i = operation_index; i < end; i++) {
            if (operations[i].is_query) continue;
            int node = operations[i].node;
            current_chain[node].push_back(operations[i].block);
            changed_nodes.push_back(node);
        }
        sort(changed_nodes.begin(), changed_nodes.end());
        changed_nodes.erase(unique(changed_nodes.begin(), changed_nodes.end()), changed_nodes.end());
        for (int i = 0; i < (int)changed_nodes.size(); i++) {
            send_chain(changed_nodes[i], now);
        }
        for (int i = operation_index; i < end; i++) {
            if (operations[i].is_query) print_chain(operations[i].node);
        }
        operation_index = end;
    }

    return 0;
}

父指针共享链前缀

区块形成后,它和祖先的连接不会改变。把每个块保存为

text
Block { parent, id, length }

其中 parent 是前一个块的下标。一个节点的主链只需记录末块下标;比较两条链时直接比较 lengthid。输出时沿 parent 回溯并反转即可,不需要复制整条链。

事件模拟

消息写成事件 (到达时刻, 接收节点, 链末块),用按到达时刻排序的最小堆维护。推进到一个操作时刻时,先处理所有已经到达的消息。同一时刻可能有多个邻居同时向同一节点发送链,因此先在 incoming_chain 中保留最优收到链,全部消息取完后才更新该节点并向邻居传播。这样不会依赖堆中同一时刻消息的弹出顺序。

当前时刻的产生块也要合并处理:新块的父亲是该节点接收阶段结束后的主链末块。同一节点如果有多个产生块,最后只发送一次最终主链;更早产生的链是它的前缀,单独发送不会改变任何接收者的最优结果。

样例时间线

下面的时间线来自样例 1。时刻 2 时,五个长度为 2 的链同时到达,各节点都选择末块编号最小的 1;时刻 11 中,节点 2 先收到 0,1,10,再接上新块 9

时刻 产生块 查询
1 节点 15 分别产生块 15 查询五个节点
2 查询五个节点
10 节点 1 产生块 10
11 节点 2 产生块 9 查询五个节点
12 查询五个节点

正确性

父指针唯一地确定每个块及其所有祖先,因此它表示的链和题目中的主链完全一致,length 与末块编号也正好是比较规则需要的信息。对某个到达时刻,连续接收多条链后的最终状态一定是原主链和全部收到链中的最优者;先合并消息再更新恰好得到这个结果。每次主链变更都向所有邻居安排 tt 后的消息,故传播时刻与题意相同。按“接收、产生、查询”的次序处理每个时刻,输出的就是所询问时刻的主链。

代码

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 MAXN = 505;

// 一个区块:parent 是前一块下标,id 是题目给出的块编号,length 是所在链长度。
struct Block {
    int parent, id, length;
};

// 一条在途的链消息:time 是到达时刻,node 是接收节点,chain 是链的末块下标。
struct Message {
    long long time;
    int node, chain;
};

// 小根堆,按到达时刻从小到大取消息。
struct MessageOrder {
    bool operator()(const Message &left, const Message &right) const {
        return left.time > right.time;
    }
};

// 一条输入操作:产生块或查询。
struct Operation {
    int node, block;
    long long time;
    bool is_query;
};

int n, m, delay_time;
int current_chain[MAXN];  // 每个节点当前主链的末块下标
int incoming_chain[MAXN]; // 同一时刻收到的所有链中最好的一个,-1 表示尚未收到
vector<int> graph[MAXN];  // 无向图邻接表
vector<Block> blocks;     // 全部区块,下标即区块编号
priority_queue<Message, vector<Message>, MessageOrder> message_queue;

// 判断链 left 是否严格优于链 right;right == -1 表示没有任何候选。
bool is_better(int left, int right) {
    if (right == -1) return true;
    if (blocks[left].length != blocks[right].length) {
        return blocks[left].length > blocks[right].length;
    }
    return blocks[left].id < blocks[right].id;
}

// 节点 node 把末块为 chain 的链发给所有邻居,t 个时刻后到达。
void send_chain(int node, int chain, long long time) {
    for (int i = 0; i < (int)graph[node].size(); i++) {
        Message message = {time + delay_time, graph[node][i], chain};
        message_queue.push(message);
    }
}

// 处理所有在 time 时刻到达的消息:先合并出每个节点收到的最优链,再统一更新。
void process_messages(long long time) {
    vector<int> touched;
    while (!message_queue.empty() && message_queue.top().time == time) {
        Message message = message_queue.top();
        message_queue.pop();
        if (incoming_chain[message.node] == -1) touched.push_back(message.node);
        if (is_better(message.chain, incoming_chain[message.node])) {
            incoming_chain[message.node] = message.chain;
        }
    }
    for (int i = 0; i < (int)touched.size(); i++) {
        int node = touched[i];
        if (is_better(incoming_chain[node], current_chain[node])) {
            current_chain[node] = incoming_chain[node];
            send_chain(node, current_chain[node], time);
        }
        incoming_chain[node] = -1;
    }
}

// 沿父指针回溯整条链并正序输出。
void print_chain(int chain) {
    vector<int> answer;
    while (chain != -1) {
        answer.push_back(blocks[chain].id);
        chain = blocks[chain].parent;
    }
    reverse(answer.begin(), answer.end());
    cout << answer.size();
    for (int i = 0; i < (int)answer.size(); i++) cout << ' ' << answer[i];
    cout << '\n';
}

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

    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        graph[u].push_back(v);
        graph[v].push_back(u);
    }
    int operation_count;
    cin >> delay_time >> operation_count;
    string line;
    getline(cin, line);

    // 读入全部操作:两个数表示查询,三个数表示产生块。
    vector<Operation> operations;
    for (int i = 0; i < operation_count; i++) {
        getline(cin, line);
        stringstream input(line);
        vector<long long> value;
        long long number;
        while (input >> number) value.push_back(number);
        Operation operation;
        operation.node = (int)value[0];
        operation.time = value[1];
        operation.is_query = value.size() == 2;
        operation.block = operation.is_query ? 0 : (int)value[2];
        operations.push_back(operation);
    }

    Block genesis = {-1, 0, 1}; // 创世块 0,链长 1
    blocks.push_back(genesis);
    for (int i = 1; i <= n; i++) current_chain[i] = 0;
    fill(incoming_chain, incoming_chain + MAXN, -1);

    int operation_index = 0;
    while (operation_index < operation_count) {
        long long now = operations[operation_index].time;
        // 一个时刻内先处理所有已经到达的消息(接收阶段)。
        while (!message_queue.empty() && message_queue.top().time <= now) {
            process_messages(message_queue.top().time);
        }

        // 当前时刻的所有产生块:接在接收阶段确定的主链末尾。
        int end = operation_index;
        while (end < operation_count && operations[end].time == now) end++;
        vector<int> changed_nodes;
        for (int i = operation_index; i < end; i++) {
            if (operations[i].is_query) continue;
            int node = operations[i].node;
            Block block = {current_chain[node], operations[i].block,
                           blocks[current_chain[node]].length + 1};
            blocks.push_back(block);
            current_chain[node] = (int)blocks.size() - 1;
            changed_nodes.push_back(node);
        }
        // 同一节点产生多个块时只发送最终主链一次。
        sort(changed_nodes.begin(), changed_nodes.end());
        changed_nodes.erase(unique(changed_nodes.begin(), changed_nodes.end()), changed_nodes.end());
        for (int i = 0; i < (int)changed_nodes.size(); i++) {
            int node = changed_nodes[i];
            send_chain(node, current_chain[node], now);
        }
        // 查询排在当前时刻所有产生块之后。
        for (int i = operation_index; i < end; i++) {
            if (operations[i].is_query) print_chain(current_chain[operations[i].node]);
        }
        operation_index = end;
    }

    return 0;
}

复杂度

设实际发送的链消息数为 SS,所有查询实际输出的块编号数为 PP。消息堆操作的时间复杂度为 O(SlogS)O(S\log S),创建区块和读取操作为 O(k)O(k),回溯输出为 O(P)O(P),总时间复杂度 O(SlogS+k+P)O(S\log S + k + P)。区块数不超过产生块操作数,空间复杂度为 O(S+k)O(S+k)

总结

事件模拟中,最容易错的是同一时刻的先后关系。先把同一时刻到达消息合并成最优候选,再统一传播,可以严格对应“先接收、后产生”的规则。父指针既保留了每条历史链,又避免了每次消息复制整个链。

图示解析

这张图串起本题从输入操作到主链输出的主线:

text
按时间分组的操作 + 延迟消息
`- 先合并同一时刻到达的最优链
   `- 节点更新后向邻居安排 time + t 的消息
      `- 产生块接到当前链尾,查询时沿 parent 回溯输出

先区分“消息到达”和“输入操作”两类事件。每个时刻只在接收阶段选一次最优链,之后的产生块必然接在该阶段确定的主链上。父指针让链状态可共享,传播时只传递末块编号。