【随笔浅谈】颜色段均摊算法
十分浅显,由很多内容没有提到。有空再来填坑!
std::set 维护颜色段
定义颜色段(广义):放在一起具有某一性质的一段区间。
构造
可以考虑用一个结构体来表示一个颜色段,一个结构体记录了该颜色段的左右端点,以及该颜色段的某信息。然后将这些结构体放入 std::set
中,std::set
内部按照区间的相对顺序排序。
下文的介绍,以维护具有相同颜色的一段区间为例。
namespace PB {
struct node {
int l, r;
mutable int c;
node(int _l, int _r, int _c) : l(_l), r(_r), c(_c) {}
bool operator < (const node &rhs) const {
return l < rhs.l;
}
};
set<node> t;
typedef set<node>::iterator iter;
}
split 分裂
有时候,对于一个颜色段,我们只需要访问它的其中一段信息即可。
这时候就需要分裂该颜色段了。
定义 \(\mathrm{split}(x)\) 函数:将 \(x\) 所在的颜色段 \([l, r]\),分裂成颜色段 \([l, x - 1]\) 与颜色段 \([x, r]\);并返回颜色段 \([x, r]\) 在 std::set
中的位置。
namespace PB {
// ...
iter split(int x) {
if (x > n) return t.end();
iter it = -- t.upper_bound((node){ x, 0, 0 });
if (it->l == x) return it;
int l = it->l, r = it->r, c = it->c;
t.erase(it);
t.insert((node){ l, x - 1, c }); return t.insert((node){ x, r, c }).first;
}
}
区间操作
有时候,对于一个序列,我们需要访问一个区间 \([l, r]\) 的信息。
根据分裂操作,就相当于是访问区间 \(\left[\mathrm{split}(l), \mathrm{split}(r + 1)\right)\) 中颜色段的信息,依次统计即可。
namespace PB {
// ...
void attend(int l, int r) {
iter itr = split(r + 1), itl = split(l);
for (iter it = itl; it != itr; it ++) {
// ...
}
}
}
基于数据随机、区间推平 - 颜色段均摊算法
当一道题目同时保证「数据随机」、「具有区间推平操作」时,大量的区间推平会使得整个序列的势能一直维持在较小的范围内。
可以证明运用 std::set
维护颜色段的方法的时间复杂度为 \(\mathcal{O}(n \log \log n)\)。hqztrue 的证明。
例题选讲
1.【CF 896C】Willem, Chtholly and Seniorious
Description
给出一个长度为 \(n\) 的序列 \(a\),你需要维护这个序列,共 \(m\) 次操作,共 \(4\) 种操作:
1 l r x
:对 \(\forall i \in[l, r]\),令 \(a_i \gets a_i + x\)。2 l r x
:对 \(\forall i \in[l, r]\),令 \(a_i \gets x\)。3 l r x
:求区间 \([l, r]\) 内的第 \(k\) 小,数字相同算多次。4 l r x y
:求区间 \([l, r]\) 内每个数字的 \(x\) 次方模 \(y\) 的值。
数据范围:\(1 \leq n, m \leq 10^5\),\(1 \leq x, y \leq 10^9\)。
时空限制:\(2000 \ \mathrm{ms} / 250 \ \mathrm{MiB}\)。
Solution
该算法起源题。
- 操作 1:将区间 \([l, r]\) 里所有颜色段的颜色加上 \(c\) 即可。
- 操作 2:将区间 \([l, r]\) 里所有颜色段的颜色赋为 \(c\) 即可。
- 操作 3:访问区间 \([l, r]\) 里的所有颜色段,提取出后排序,找到第一个前缀和 \(\geq k\) 的颜色即可。
- 操作 4:访问区间 \([l, r]\) 里的所有颜色段,将每个颜色段用快速幂统一统计即可。
基于操作影响势能 or 与势能无关 - 颜色段均摊算法
当一道题目保证「操作影响势能」或「操作与势能无关」时,则时间复杂度可以运用均摊分析法来测算。
例题选讲
1.【Luogu P2824】「HEOI2016/TJOI2016」排序
Description
给出一个 \(1\) 至 \(n\) 的排列,现在对这个排列序列进行 \(m\) 次局部排序,排序分为两种:
-
0 l r
:表示将区间 \([l, r]\) 的数升序排序。 -
1 l r
:表示将区间 \([l, r]\) 的数降序排序。
最后询问第 \(q\) 位置上的数字。
数据范围:\(1 \leq n \leq 10^5\)。
时空限制:\(4000 \ \mathrm{ms} / 256 \ \mathrm{MiB}\)。
Solution
注意到给区间 \([l, r]\) 内的点 升序 / 降序 排序,是赋予了一个区间有序的性质。
可以用权值线段树来维护一个有序区间内的权值信息,然后将这些有序的区间放入 std::set
中。
套用上述模板即可,不同点只是在于对权值线段树的处理:
- 对于 split 操作,线段树分裂即可;
- 对于区间排序,线段树合并即可;
- 对于查询操作,线段树上二分即可。
由于一次排序操作至多会进行两次线段树分裂,增加的点数是 \(\mathcal{O}(\log n)\) 的,故在整个过程中线段树的总节点个数为 \(\mathcal{O}((n + m) \log n)\) 级别的。
在区间排序中,由于每访问一个颜色段,就会将该颜色段删去,使得势能降低。
故总时间复杂度为 \(\mathcal{O}((n +m) \log n)\)。
该做法支持多测 + 强制在线。