2022.9.27 Standard 闲话
WintersRain 让我加涩图:
涩图
HE 出初赛成绩了 /hsh
3、往届获奖认证者名额(C类)
为减少高水平认证者的意外情况,符合以下条件之一的往届获奖认证者可以获得C类名额。
(1)上一年获得过CSP-S2三等奖、或NOIP三等奖及以上奖项,在参加本次认证S组第一轮认证的情况下,可以无条件进入S组第二轮;
(2)上一年获得过CSP-J2三等奖及以上的初中或小学认证者,在参加本次认证J组第一轮情况认证的情况下,可以无条件进入J组第二轮。
upd. 改了“高水平认证者”这个称呼 .
呃呃
颜色段均摊 / Old Driver Tree
upd. 一个玄学玩意,我也不知道是啥,好像等价于块链 .
这个叫法很多啊 /hsh
可以叫颜色段均摊,珂朵莉树,或者 ODT (Old Driver Tree) .
颜色段均摊或许是正统叫法,但是 lxl 当时写 CF896C 题解的时候叫的 ODT(据他说他那时候比较 naive 瞎起的名,好像是).
就像半在线卷积和分治 FFT / NTT 的关系一样 .
呃呃跑偏了,回到 ODT 上,后面因为颜色段均摊太长了懒得打所以统一叫 ODT .
ODT 的基本原理
CF896C Willem, Chtholly and Seniorious
维护一个序列 \(\{a\}\),支持:
1 l r x
,将 \([l,r]\) 内所有数加上 \(x\) .2 l r x
,将 \([l,r]\) 内所有数改成 \(x\) .3 l r x
,询问区间 \([l,r]\) 内的第 \(x\) 小值 .4 l r x y
,询问区间 \([l,r]\) 内所有数的 \(x\) 次方和模 \(y\),即求 \(\displaystyle\left(\sum_{i=1}^na_i^x\right)\bmod y\) .保证数据随机,\(n,m\le 10^5\) .
结构
由于 2 操作的存在,我们可以用一个 std :: set
(后面写作 set)记录值相同的所有区间,set 的 operator <
按左端点比较即可 .
这里用 set 记录的原因是方便后面的 split 操作 .
简单写一下代码:
struct Node
{
mutable int l, r, val;
Node(int L, int R, int v) : l(L), r(R), val(v){}
bool operator < (const Node& rhs) const {return l < rhs.l;}
};
set<Node> s;
typedef set<Node> :: iterator IT;
因为修改的时候 set 里面的内容可能会变,所以要用 mutable
(其实有点牵强,看了后面的操作大概就明白了).
这个 IT
定义不定义都行,反正都能用 auto
代替 /youl
split
split 操作是 ODT 的核心操作 .
\(\operatorname{split}(pos)\) 方法将 \(pos\) 所在的区间 \([l,r]\) 分裂开来,变成 \([l,pos-1]\) 和 \([pos,r]\) .
注意到这样我们就可以提取出 \([pos,r]\),进而就可以提取任意一个区间出来了,于是就可以用类似平衡树的思路去做了 .
split 的实现也是非常简单,在 set 里二分一下即可:
inline IT split(int pos)
{
auto it = s.lower_bound(Node(pos, 0, 0));
if ((it != s.end()) && (it -> l == pos)) return it;
--it;
if (it -> r < pos) return s.end();
int l = it->l, r = it->r, v = it->val; s.erase(it);
s.insert(Node(l, pos-1, v)); // 左边的区间丢回去,右边的区间返回来
return s.insert(Node(pos, r, v)).first; // insert 返回一个 pair<iterator, bool>
}
可以注意到单次 split 操作是 \(O(\log n)\) 的,并且只会在 ODT 里增加 \(O(1)\) 个区间 .
assign
ass♂ign
assign 操作即区间赋值,提取出区间然后删掉换成新区间即可 .
inline void assign(int l, int r, int v)
{
auto R = split(r+1), L = split(l); // 还是要存一下的 /hsh
s.erase(L, R); s.insert(Node(l, r, v));
}
值得一提的是这里必须先 \(\operatorname{split}(r+1)\) 再 \(\operatorname{split}(l)\),因为如果先 \(\operatorname{split}(l)\) 的话 \(\operatorname{split}(r+1)\) 的时候有可能将 \(L\) 这个迭代器 erase 了,于是就会 Runtime Error .
注一下,C++98 标准规定 set 的 \(\operatorname{erase}(\mathrm{first},\mathrm{last})\) 是用来删除 \([\mathrm{first},\mathrm{last})\) 区间的 .
其他操作
提取出来暴力跳迭代器即可,类似 assign,提取区间的时候必须先 \(\operatorname{split}(r+1)\) 再 \(\operatorname{split}(l)\)
.
以区间加举例:
inline void add(int l, int r, int x)
{
auto R = split(r+1), L = split(l);
for (auto it = L; it != R; ++it) it -> val += x;
}
非常的暴力啊 /oh
然而由于数据随机所以复杂度是对的 /hsh
完整代码
仅供参考,需要加 #define int long long
才能 AC .
Generator 我魔改了一波,不要喷我 .
懒得用 template
了 /youl
const int N = 222222;
inline int qpow(int a, int n, int P)
{
int ans = 1;
while (n)
{
if (n & 1) ans = 1ll * ans * a % P;
a = 1ll * a * a % P; n >>= 1;
} return ans % P;
}
struct ODT
{
struct Node
{
mutable int l, r, val;
Node(int L, int R, int v) : l(L), r(R), val(v){}
bool operator < (const Node& rhs) const {return l < rhs.l;}
};
typedef set<Node> :: iterator IT;
set<Node> s;
inline IT split(int pos)
{
auto it = s.lower_bound(Node(pos, 0, 0));
if ((it != s.end()) && (it -> l == pos)) return it;
--it;
if (it -> r < pos) return s.end();
int l = it->l, r = it->r, v = it->val; s.erase(it);
s.insert(Node(l, pos-1, v));
return s.insert(Node(pos, r, v)).first;
}
inline void assign(int l, int r, int v)
{
auto R = split(r+1), L = split(l);
s.erase(L, R); s.insert(Node(l, r, v));
}
inline void add(int l, int r, int x)
{
auto R = split(r+1), L = split(l);
for (auto it = L; it != R; ++it) it -> val += x;
}
inline int kth(int l, int r, int k)
{
auto R = split(r+1), L = split(l);
vector<pii> v;
for (auto it = L; it != R; ++it) v.emplace_back(make_pair(it->val, it->r-it->l+1));
stable_sort(v.begin(), v.end());
for (auto _ : v)
if ((k -= _.second) <= 0) return _.first;
return -1;
}
inline int pows(int l, int r, int x, int p)
{
auto R = split(r+1), L = split(l); int ans = 0;
for (auto it = L; it != R; ++it) (ans += 1ll * (it->r - it->l + 1) * qpow(it->val % p, x, p) % p) %= p;
return ans;
}
}T;
int n, m, seed, vmax;
int rnd(int k){int r = seed; seed = (seed * 7ll + 13) % 1000000007; return r % k + 1;}
int main()
{
scanf("%d%d%d%d", &n, &m, &seed, &vmax);
for (int i=1; i<=n; i++) T.s.insert(ODT::Node(i, i, rnd(vmax)));
int opt, l, r, x, y;
while (m--)
{
opt = rnd(4); l = rnd(n); r = rnd(n);
if (l > r) swap(l, r); // 先交换再随机 x
x = rnd(opt == 3 ? r-l+1 : vmax);
if (opt == 4) y = rnd(vmax);
if (opt == 1) T.add(l, r, x);
if (opt == 2) T.assign(l, r, x);
if (opt == 3) printf("%d\n", T.kth(l, r, x));
if (opt == 4) printf("%d\n", T.pows(l, r, x, y));
}
return 0;
}
时间复杂度证明
我们就以 CF896C 为例说一下复杂度证明 .
令 set 中存在的区间数量为 \(r\) .
随机区间长度
首先考虑一个问题:
Randomize Range
在 \([1,n]\) 间随机两个数 \(l\le r\),则 \(r-l+1\) 的期望是?
joke3579 使用 二重积分 解决了这个问题的连续情况,但是我不会二重积分,只好用朴素方法 .
第二步是 dirty-work,所以不展开 .
事实上 CF896C 这个题还有一点区别,它的随机方式实际上是
Fake Randomize Range
在 \([1,n]\) 间随机两个数 \(l,r\),则 \(|r-l|+1\) 的期望是?
这样在 \(l=r\) 的时候会少算一次,产生一些微小区别,但是问题不大 .
期望 \(r=O(\log n)\)
理性理解 .
考虑每次 assign 操作都会让 \(r\gets \dfrac 23r+\dfrac 23\) 且有概率在分裂出两个新区间,由于是期望意义的我们就认为是 \(r\gets \dfrac 23r\) 并且永远分裂出两个新区间 .
于是经过 \(q\) 次 assign,\(r=\left(\dfrac23\right)^k\),也就是 \(k=\log_{\frac 23}r\) .
从而最终的区间数量就是 \(2k=2\log_{\frac23}r\) 个 .
根据初始条件 \(r=n\) 我们可以得到 assign 足够多的时候就有 \(r=2\log_{\frac23}n=O(\log n)\) .
然后就证完了 .
均摊分析
下面列出一些操作的单次时间复杂度与均摊时间复杂度,均摊的可以由单次的推出(其实也就是把 \(r\) 换成 \(O(\log n)\)).
操作名和代码对应,\(\verb!pows!\) 中 \(L\) 是值域(快速幂贡献的复杂度).
到这里也就分析完了,\(r=O(\log n)\) 实际上是最关键的部分,也是其时间复杂度保障,为了保证 \(r=O(\log n)\),就必须有足够的 assign 操作,首尾呼应 .
奥秘结论
joke3579 说的:
- 当有 \(\dfrac14\) 的操作为区间赋值时,珂朵莉树内维护的区间数量的确界为 \(\Theta(\log n)\) .
- lxl 说当有 \(\dfrac13\) 的操作为赋值时有上界 \(O(\log\log n)\) .
比较玄幻,感觉是把上面那个感性理解的 \(r=O(\log n)\) 收敛速度给算出来了 /yun
NGGYU / 如何防止 ODT 被 Hack
Hack ODT 的原理
ODT 是如何被 Hack 的呢?
我们注意到保证 ODT 复杂度的核心就在于区间赋值操作,所以如果我们要卡 ODT 的话区间赋值就要非常的少 .
比如全程一个区间赋值都没有,那么单次复杂度就是 \(\Theta(n)\),和暴力一样了 .
可以说 ODT 的适用范围就是随机数据和每次操作都是区间赋值了……吗?
防止被卡
如果一道题去掉区间赋值之后有比较优的做法,那么这种做法就是可行的,比如 CF896C 的非随机数据就是不满足条件的一种题 .
注意到 Hack ODT 的话区间赋值非常少,于是基本就可以暴力维护这个区间赋值,这个可以通过一些别的数据结构维护 .
有些比较聪明的 Hack 可能过不掉,但还是能过一部分 Hack 的 .
看完有没有感觉被骗了?这就对了(?
以下是博客签名,正文无关
本文来自博客园,作者:Jijidawang,转载请注明原文链接:https://www.cnblogs.com/CDOI-24374/p/16726377.html
版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0)进行许可。看完如果觉得有用请点个赞吧 QwQ