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 天,初始都是工作日。两种操作:

  1. 把 [l, r] 全部变成工作日;
  2. 把 [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,最后输出大小。

区间排序可以暴力:

  1. 把 [l, r] 覆盖的段全部拿出来;
  2. 展开成 (字符, 出现次数);
  3. 按字符排序;
  4. 重新 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 在这里主要承担“维护颜色段”的角色,而不是单独解决全部问题。

posted @ 2026-04-17 22:09  ywrow  阅读(20)  评论(0)    收藏  举报