ODT学习笔记
算法
ODT 就是把序列里连续且值相同的一段压成一个节点 (l, r, v),放进有序集合里。
只有两个核心操作:
- split(pos):把包含
pos的段切开,保证存在左端点恰好是pos的段。 - assign(l, r, v):先把
r+1切开,再把l切开,然后删掉[l, r]里的所有段,插入一个[l, r] = v。
所有区间操作都套这个模板:
split(r + 1)
split(l)
遍历 [l, r] 覆盖的段,暴力处理
能用的前提很简单:有区间赋值,且数据随机。assign 会不断合并段,所以段数通常很少;如果没有推平操作,ODT 就可能退化成暴力。
例题 1:CF896C Willem, Chtholly and Seniorious
题意:维护序列,支持区间加、区间赋值、区间第 k 小、区间幂次和取模。数据随机生成。
思路:
- 区间赋值:直接
assign。 - 区间加:切开后遍历段,把每段的
v加上值。 - 区间第 k 小:把覆盖段拿出来,按值排序,累减长度。
- 区间幂次和:每段贡献
长度 × v^x mod y。
这题就是 ODT 的起源题。
关键代码:
auto split(int pos) {
auto it = odt.lower_bound(Node(pos));
if (it != odt.end() && it->l == pos) return it;
--it;
int L = it->l, R = it->r; ll V = it->v;
odt.erase(it);
odt.insert(Node(L, pos - 1, V));
return odt.insert(Node(pos, R, V)).first;
}
void assign(int l, int r, ll v) {
auto itr = split(r + 1), itl = split(l);
odt.erase(itl, itr);
odt.insert(Node(l, r, v));
}
第 k 小:
vector<pair<ll,int>> vec;
for (auto it = itl; it != itr; ++it)
vec.push_back({it->v, it->r - it->l + 1});
sort(vec.begin(), vec.end());
for (auto [val, cnt] : vec) {
k -= cnt;
if (k <= 0) return val;
}
幂次和:
ll res = 0;
for (auto it = itl; it != itr; ++it)
res = (res + (ll)(it->r - it->l + 1) % y * qpow(it->v, x, y)) % y;
例题 2:CF915E Physical Education Lessons
题意:有 n 天,初始都是工作日。两种操作:
- 把
[l, r]全部变成工作日; - 把
[l, r]全部变成非工作日。
每次操作后输出当前工作日天数。
思路:
这就是典型的区间赋值。把工作日记为 1,非工作日记为 0。每次 assign 后遍历所有段,累加 v == 1 的段长度即可。
void assign(int l, int r, int v) {
auto itr = split(r + 1), itl = split(l);
odt.erase(itl, itr);
odt.insert(Node(l, r, v));
}
ll query() {
ll ans = 0;
for (auto &nd : odt)
if (nd.v == 1) ans += nd.r - nd.l + 1;
return ans;
}
复杂度靠 assign 合并段维持。
例题 3:P2572 [SCOI2010] 序列操作
题意:01 序列,支持:
- 区间赋 0;
- 区间赋 1;
- 区间取反;
- 区间求 1 的个数;
- 区间最长连续 1。
思路:
前四个操作 ODT 很顺手:
- 赋 0 / 赋 1:
assign(l, r, 0/1); - 取反:切开后遍历段,把
v变成1 - v; - 求 1 的个数:遍历段,累加
v == 1的长度。
但“区间最长连续 1”不太适合 ODT。因为 ODT 只保存了同值段,不能直接维护最长连续子段信息。这题正解通常是线段树。
所以这题适合说明一点:ODT 不是万能模板。如果查询需要跨段合并信息,ODT 会很麻烦。
例题 4:P2787 语文1(chin1)- 理理思维
题意:维护字符串序列,支持:
- 区间赋值成某个字符;
- 查询区间内某个字符出现次数;
- 查询区间不同字符种类数;
- 区间排序。
思路:
前三个操作很自然:
- 赋值:
assign; - 查某字符次数:切开后遍历,值相等就累加长度;
- 查不同字符数:把覆盖段的值放进
set,最后输出大小。
区间排序可以暴力:
- 把
[l, r]覆盖的段全部拿出来; - 展开成
(字符, 出现次数); - 按字符排序;
- 重新
assign回[l, r]。
因为数据有区间赋值,段数不会爆炸,所以暴力排序也能过。
例题 5:P3740 [HAOI2014] 贴海报
题意:一块长板,依次贴海报。每张海报覆盖 [l, r],后贴的会覆盖先贴的。问最后能看到多少种不同海报。
思路:
每次贴海报就是区间赋值。最后遍历所有段,统计不同颜色即可。
用 map 版 ODT 写起来更短:
map<int,int> mp;
void split(int x) {
if (mp.lower_bound(x)->first == x) return;
auto it = prev(mp.lower_bound(x));
mp[x] = it->second;
}
void assign(int l, int r, int v) {
split(l);
split(r + 1);
auto it = mp.lower_bound(l);
while (it != mp.end() && it->first <= r)
it = mp.erase(it);
mp[l] = v;
}
最后遍历 mp,把所有值丢进 set,去掉背景色即可。
例题 6:P4979 矿洞:坍塌
题意:维护序列,支持区间赋值,以及判断某段是否全部相同。
思路:
区间赋值直接用 assign。判断 [l, r] 是否全相同,只需要:
split(r + 1);
split(l);
检查 [l, r] 覆盖的段数是否为 1
如果只有一段,说明整个区间值相同;否则不相同。
这题体现了一个常用技巧:ODT 不仅能改值,还能通过段数判断区间是否“整齐”。
例题 7:P4690 [Ynoi Easy Round 2016] 镜中的昆虫
题意:维护颜色序列,支持:
- 区间赋值颜色;
- 查询区间不同颜色数。
思路:
区间赋值还是 assign。查询不同颜色数时,把覆盖段的颜色收集起来去重即可。
但这题如果继续扩展到单点修改、查询前驱相同颜色位置等,就需要配合 pre 数组、树状数组等结构。ODT 在这里主要承担“维护颜色段”的角色,而不是单独解决全部问题。

浙公网安备 33010602011771号