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 使用 二重积分 解决了这个问题的连续情况,但是我不会二重积分,只好用朴素方法 .

\[\begin{aligned}\mathbb E(X)&=\dfrac{\displaystyle \sum_{1=1}^n\sum_{r=l}^n(r-l)}{\displaystyle\sum_{l=1}^n\sum_{r=l}^n}\\&=\dfrac{\frac 23n(n+1)(n-1)}{\frac12n(n+1)}\\&=\dfrac{n-1}3\end{aligned} \]

第二步是 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)\)).

\[\newcommand{\arraystretch}{1.5} \begin{array}{c|c|c}\hline\hline \textbf{操作名} & \textbf{单次时间复杂度} & \textbf{均摊时间复杂度} \\\hline \verb!split! & \Theta(\log r) & O(\log\log n) \\\hline \verb!assign! & \Theta(\log r) & O(\log\log n)\\\hline \verb!add! & \Theta(r) & O(\log n)\\\hline \verb!kth! & \Theta(r\log r) & O(\log n\log\log n)\\\hline \verb!pows! & \Theta(r\log L) & O(\log n\log L)\\\hline\hline \end{array} \]

操作名和代码对应,\(\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 的 .

看完有没有感觉被骗了?这就对了(?

posted @ 2022-09-27 17:08  Jijidawang  阅读(138)  评论(5编辑  收藏  举报
😅​