用按原始编号分割合并的 Treap 表示木板,并只扫描切下部分与剩余部分中较小的一块来维护颜色频次。
OJ: shumeng
题目 ID: CSP202409E
难度:提高+/省选-
标签:Treap分裂合并启发式合并模拟
日期: 2026-07-31 16:21
形式化题目
初始木板由编号
每次操作输出新木板的两个值:
- 不同颜色数;
- 颜色段数(相邻相同颜色算一段)。
思路
直接保存每块木板的段编号序列并整块扫描是
朴素做法:整块扫描
先看直接实现:每块木板保存一个段编号序列,每次操作扫描整块,按编号范围拆成选中段与剩余段,再统计新木板的颜色信息。
/**
* 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:39
*/
// brute.cpp:小数据暴力解,直接保存每块木板上的原始段编号,每步扫描整块木板。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
cin >> n >> m >> k;
vector<int> color(n + 1);
for (int i = 1; i <= n; i++) cin >> color[i];
vector<vector<int> > board(k + 2); // board[i] 保存第 i 块木板上的段编号序列
for (int i = 1; i <= n; i++) board[1].push_back(i);
for (int operation = 1; operation <= k; operation++) {
int source, left_key, right_key;
cin >> source >> left_key >> right_key;
// 扫描整块木板,按编号范围拆成选中段与剩余段
vector<int> selected;
vector<int> remaining;
for (int i = 0; i < (int)board[source].size(); i++) {
int position = board[source][i];
if (left_key <= position && position <= right_key) selected.push_back(position);
else remaining.push_back(position);
}
board[source] = remaining;
board[operation + 1] = selected;
// 统计新木板的不同颜色数与颜色段数(连续同色计为一段)
vector<char> appeared(m + 1, 0);
int distinct = 0;
int runs = 0;
int previous_color = -1;
for (int i = 0; i < (int)selected.size(); i++) {
int current_color = color[selected[i]];
if (!appeared[current_color]) {
appeared[current_color] = 1;
distinct++;
}
if (i == 0 || current_color != previous_color) runs++;
previous_color = current_color;
}
cout << distinct << ' ' << runs << '\n';
}
return 0;
}做法完全符合题意,但每步
主解:用 Treap 表示木板
所有段的相对顺序始终与原始编号一致,因此每块木板可以表示成“按键为原始编号”的 Treap,只需要保存一个根节点。一次切割就是两次按键分裂:
板 x = [编号 < l] + [l..r] + [编号 > r]中间根就是新木板,首尾两部分合并后仍是原木板。Treap 节点维护子树大小、首尾颜色和颜色段数,于是新木板的颜色段数可以在根节点
小块扫描维护不同颜色数
不同颜色数不能只用边界信息维护,需要为每块木板保存一张 <颜色, 出现次数> 频率表。一次切割后,切下部分和剩余部分构成原木板的划分:只扫描两者中较小的一块,建立它的频率表;原木板原有的频率表转移给较大的一块并扣除小块频次,另一块使用扫描得到的新表。
摊还关键:一个段只要被扫描一次,它所在的新木板大小就至多变成原来的一半,因此每个段总共被扫描
代码
/**
* 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:39
*/
#include <bits/stdc++.h>
using namespace std;
struct SequenceNode {
int left, right;
int priority;
int key, color;
int size;
int first_color, last_color;
int run_count;
};
vector<SequenceNode> sequence_tree;
int sequence_size(int root) {
return root == 0 ? 0 : sequence_tree[root].size;
}
void pull_sequence(int root) {
if (root == 0) return;
SequenceNode &node = sequence_tree[root];
node.size = sequence_size(node.left) + sequence_size(node.right) + 1;
node.first_color = node.left == 0 ? node.color : sequence_tree[node.left].first_color;
node.last_color = node.right == 0 ? node.color : sequence_tree[node.right].last_color;
node.run_count = sequence_tree[node.left].run_count + 1 + sequence_tree[node.right].run_count;
if (node.left != 0 && sequence_tree[node.left].last_color == node.color) node.run_count--;
if (node.right != 0 && node.color == sequence_tree[node.right].first_color) node.run_count--;
}
int merge_sequence(int left_root, int right_root) {
if (left_root == 0) return right_root;
if (right_root == 0) return left_root;
if (sequence_tree[left_root].priority > sequence_tree[right_root].priority) {
sequence_tree[left_root].right = merge_sequence(sequence_tree[left_root].right, right_root);
pull_sequence(left_root);
return left_root;
}
sequence_tree[right_root].left = merge_sequence(left_root, sequence_tree[right_root].left);
pull_sequence(right_root);
return right_root;
}
void split_sequence(int root, int key, int &left_root, int &right_root) {
if (root == 0) {
left_root = 0;
right_root = 0;
return;
}
if (sequence_tree[root].key <= key) {
left_root = root;
split_sequence(sequence_tree[root].right, key, sequence_tree[root].right, right_root);
pull_sequence(left_root);
} else {
right_root = root;
split_sequence(sequence_tree[root].left, key, left_root, sequence_tree[root].left);
pull_sequence(right_root);
}
}
void collect_colors(int root, unordered_map<int, int> &count) {
if (root == 0) return;
collect_colors(sequence_tree[root].left, count);
count[sequence_tree[root].color]++;
collect_colors(sequence_tree[root].right, count);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
cin >> n >> m >> k;
vector<int> color(n + 1);
for (int i = 1; i <= n; i++) cin >> color[i];
sequence_tree.assign(n + 1, SequenceNode());
mt19937 random_engine(712367821);
int initial_root = 0;
for (int i = 1; i <= n; i++) {
sequence_tree[i].left = 0;
sequence_tree[i].right = 0;
sequence_tree[i].priority = (int)random_engine();
sequence_tree[i].key = i;
sequence_tree[i].color = color[i];
sequence_tree[i].size = 1;
sequence_tree[i].first_color = color[i];
sequence_tree[i].last_color = color[i];
sequence_tree[i].run_count = 1;
initial_root = merge_sequence(initial_root, i);
}
vector<int> board_root(k + 2, 0);
vector<unique_ptr<unordered_map<int, int> > > board_colors(k + 2);
board_root[1] = initial_root;
board_colors[1].reset(new unordered_map<int, int>());
board_colors[1]->reserve(n * 2 + 1);
for (int i = 1; i <= n; i++) (*board_colors[1])[color[i]]++;
for (int operation = 1; operation <= k; operation++) {
int source_board, left_key, right_key;
cin >> source_board >> left_key >> right_key;
int left_root, middle_root, right_root;
int temporary_root;
split_sequence(board_root[source_board], left_key - 1, left_root, temporary_root);
split_sequence(temporary_root, right_key, middle_root, right_root);
int remaining_root = merge_sequence(left_root, right_root);
int middle_size = sequence_size(middle_root);
int remaining_size = sequence_size(remaining_root);
int small_size = min(middle_size, remaining_size);
unique_ptr<unordered_map<int, int> > small_colors(new unordered_map<int, int>());
small_colors->reserve(small_size * 2 + 1);
if (middle_size <= remaining_size) collect_colors(middle_root, *small_colors);
else collect_colors(remaining_root, *small_colors);
unique_ptr<unordered_map<int, int> > old_colors = move(board_colors[source_board]);
unordered_map<int, int>::iterator iterator = small_colors->begin();
while (iterator != small_colors->end()) {
unordered_map<int, int>::iterator old_iterator = old_colors->find(iterator->first);
old_iterator->second -= iterator->second;
if (old_iterator->second == 0) old_colors->erase(old_iterator);
++iterator;
}
if (middle_size <= remaining_size) {
board_root[source_board] = remaining_root;
board_root[operation + 1] = middle_root;
board_colors[source_board] = move(old_colors);
board_colors[operation + 1] = move(small_colors);
} else {
board_root[source_board] = remaining_root;
board_root[operation + 1] = middle_root;
board_colors[source_board] = move(small_colors);
board_colors[operation + 1] = move(old_colors);
}
if (middle_root == 0) cout << "0 0\n";
else cout << board_colors[operation + 1]->size() << ' '
<< sequence_tree[middle_root].run_count << '\n';
}
return 0;
}复杂度
设段数
- 时间:Treap 分裂、合并期望
;小块扫描的总长度 ,总时间复杂度期望 。 - 空间:频率表有效条目
,考虑哈希桶转移后保留的容量,记为 。
总结
把“按原始编号取出一段”转化为 Treap 的按键分裂,再用小块扫描 + 大块频率表转移维护不同颜色数。颜色段数由序列聚合信息直接维护,整体避免了反复扫描大木板,是“序列分裂合并 + 启发式维护统计信息”的典型结合。