数据结构

bitset

用法

  • b.set() 全部设为 \(1\)b.set(p)\(p\) 设为 \(1\)
  • b.reset() 全部设为 \(0\)b.reset(p)\(p\) 设为 \(0\)
  • b.flip() 全部位置的值反转。b.flip(p) 将位置 \(p\) 的值反转。
  • b[p] 读写。
  • b.count() \(1\) 个数。
  • b.size() 返回大小。
  • b.any() 是否至少有 \(1\)\(1\)
  • b.none() 是否全 \(0\)
  • b.all() 是否全 \(1\)

ST 表

  1. st 表擅长处理静态区间问题。把区间拆成若干个长度为 \(2\) 的幂次的区间,利用倍增思想进行转移。值得注意的是这类区间问题需要注意不重不漏,例如区间最大值,最小值这类重复计算不会出错的问题。这是因为 st 表计算区间 \([l,r]\) 时是使用 \([l,l+2^k-1]\)\([r-2^k+1,r]\) 的结果,其中 \(k=\log (r-l+1)\),结合而成的。这样类似区间和,区间 gcd 的问题 st 表是无法维护的。
  2. st 表带修是线性的。当然在开头、末尾可以做到 \(O(\log)\) 次。也可以结合根号分治,设定重构的阈值。小的重构,大的分裂利用小的计算,以做到根号复杂度。

P3865

ST 表板子。记 \(f_{i,j}\) 表示 \([i, i+2^j-1]\) 的最值。枚举 \(j\) 转移即可。\(f_{i,j} = \max\{f_{i,j-1}, f_{i + 2^{j-1}, j-1}\}\)。在查询 \([x,y]\) 时,记 \(k = \log (y - x + 1)\),答案即为 \(\max\{f_{i, k}, f_{y - 2^k + 1, k}\}\)

当然 st 表也是可以这么写:\(f_{i,j}\) 表示 \([i - 2^j + 1,i]\) 的最值。

P1198

各种 ds 随便做的。不过这是 st 带修板子题。

\(f_{i,j}\) 表示 \([i - 2^j + 1,i]\) 的最值。每次在插入 \(n + 1\) 的时候,容易发现这对前面的 \(f\) 没有影响。于是在 \(\log n\) 时间内完成插入。总的时间复杂度 \(O(n\log n)\)

当然也可以翻转序列,这样就是正常的 \([i,i+2^j-1]\) 了。

P10680

好题。比较典。需要结合根分做。

注意到这个神秘的 \([l, l+2^k-1]\) 以及这个神秘的合并操作,启发我们使用 st 表。用 st 表可以高效合并两个一半区间。

问题在于它是带修的。这题里修改是线性的。考虑根分。查询是 \(O(1)\),修改是 \(O(n)\),使用根分均摊复杂度。只维护 \(2^j \le \sqrt n\) 的部分,此时 \(j = 10\)。所以当 \(j \le 10\) 时,暴力重构,否则分裂下去。可以证明时间复杂度 \(O(n\sqrt n)\),不会证。

线段树

朴素线段树

P2048

牛逼题目。
问题实际上是求 \(k\) 个不同的区间,要求长度在 \([L,R]\) 中,使得 \(k\) 个区间的区间和之和最大。
不妨固定右端点为 \(i\),那么左端点的取值范围 \(j\in[i-R+1,i-L+1]\),只需要在这里面求 \(s_i - s_{j-1}\) 的最大值,其中 \(s\) 为前缀和。这个可以用 ds 求。
这是一个很牛逼的 trick:记五元组为 \((w,i,l,r,p)\) 表示右端点为 \(i\),左端点的取值范围在 \([l,r]\) 内,取到最优解的位置为 \(j\),最优解为 \(w\)。那么用大根堆维护五元组,取 \(k\) 次堆顶。每次取出堆顶后将五元组分裂成 \((w',i,l,p-1,p')\)\((w'', i,p+1,r,p'')\) 即可。\(w',p',w'',p''\) 可以用 ds 求。这样我们在 \(O(n\log^2 n)\) 的时间解决了问题。
牛逼 trick。后续一般直接称这个 trick 为炒鸡钢琴。

CF438D

难度:蓝

与花神那题类似。取模不好取,考虑暴力就是对的。这是因为你的数 \(x\) 对某个模数 \(p(p < x)\) 取模,有 \(x\bmod p < \frac{x}{2}\)。于是一个数至多 \(\log\) 次,sgt 维护区间最大值即可。
思考:考虑暴力。考虑取模的性质。

P1502

难度:青

你考虑枚举长,为 \([l,r]\),然后你把 \(x\) 坐标在 \([l,r]\) 里的点加入贡献。你考虑对于一个 \(y\) 坐标 \(p\),它会对 \(y\) 坐标在 \([p - H + 1, p]\) 内的点造成 \(w\) 贡献。使用线段树维护即可。加入贡献的话可以双指针。

可持久化 sgt

P2617

带修区间第 \(k\) 小。
回顾静态的过程,我们在用第 \(r\) 珂可持久化线段树减去第 \(l-1\) 珂,然后进行二分。但是现在要带修,第 \(x\) 个位置的修改会影响到 \([x,n]\) 珂线段树。朴素修改是 \(O(n\log n)\) 难以通过。
使用 bit 的思想,每次只维护 \(x\) 往后 $\log $ 珂可持久化线段树。询问的时候同理。

树状数组

P6619

难度:蓝
【模板】树状数组上二分。

妙妙 trick。首先容易在 bit 外面二分做到 \(O(n\log^2 n)\),获得 \(60\) 分。
可以在 sgt 上二分去掉一只 log,但是常数太大(好像能卡过去),考虑 bit 上二分。
在 bit 上,第 \(i\) 个点储存的实际上是 \((i-lowbit(i),i]\) 的和。然后二分的时候,考虑一个倍增状物,每次考虑 \(2^{20},2^{19},2^{18},\cdots 2^1,2^0\) 能不能跳,怎么快速计算和呢?假设当前在 \(p\),考虑步长 \(2^k\),显然 \(2^k\) 肯定是 \(p+2^k\) 的 lowbit,我们现在计算了 \([1,p]\) 的和,需要计算 \((p,p+2^k]\) 的和,然后因为 bit 上节点 \(p+2^k\) 表示的是 \((p+2^k-2^k,p+2^k]\) 的和,所以这个点的值就是对和新的贡献。那么去掉一只 log。

分块

数列分块入门

P13979

难度:黄

简单题。

void add(ll l, ll r, ll v) {
	ll bl = bel[l]; 
	ll br = bel[r]; 
	if (bl == br) {
		for (ll i = l; i <= r; ++ i ) A[i] += v; 
		tc[bl] += (r - l + 1) * v; 
		return ;  
	}
	for (ll i = bl + 1; i < br; ++ i ) tag[i] += v; 
	for (ll i = l; i <= R[bl]; ++ i ) A[i] += v; 
	tc[bl] += (R[bl] - l + 1) * v; 
	for (ll i = L[br]; i <= r; ++ i ) A[i] += v; 
	tc[br] += (r - L[br] + 1) * v; 
}
ll Q(ll l, ll r) {
	ll ans = 0; 
	ll bl = bel[l]; 
	ll br = bel[r];  
	if (bl == br) {
		for (ll i = l; i <= r; ++ i ) ans += A[i]; 
		ans += (r - l + 1) * tag[bl]; 
//		ans %= mod; 
//		ans = (ans + mod) % mod; 
		return ans;  
	}
	for (ll i = bl + 1; i < br; ++ i ) 
		ans += tc[i] + (R[i] - L[i] + 1) * tag[i]; 
	for (ll i = l; i <= R[bl]; ++ i ) ans += A[i]; 
	ans += (R[bl] - l + 1) * tag[bl]; 
	for (ll i = L[br]; i <= r; ++ i ) ans += A[i]; 
	ans += (r - L[br] + 1) * tag[br]; 
//	ans %= mod; 
//	ans = (ans + mod) % mod; 
	return ans; 
}

P13977 & P13978

难度:青

本质相同。考虑用 vector 维护每个块的元素,查询的时候散块暴力,整块二分。修改的时候整块维护 tag,散块暴力修,再暴力重构。代码是容易的,但常数有点大。当然也可以在块里维护 set,常数会小一点。时间复杂度 \(O(n\sqrt n\log \sqrt n)\)

莫队

板子莫队

P2709

板子。把序列分块,把所有询问区间按 \(l\) 所在块排序,若相同按 \(r\) 排序,那么每次移动 \(l\) 不会移动超过块长 \(B\) 次,\(q\) 次询问不会超过 \(qB\) 次。左端点在同一块时,由于 \(r\) 单增,\(r\) 的移动不会超过 \(n\) 次,总共不会超过 \(n^2/B\) 次。时间复杂度 \(O(qB + \frac{n^2}{B})\),平衡一下 \(B = \sqrt \frac{n^2}{q}\),由于本题 \(q,n\) 同阶,可以直接 \(B = \sqrt n\)
再考虑一个东西叫奇偶化排序,如果 \(l\) 所在块编号相同,如果编号是奇数,\(r\) 小到大排序,否则 \(r\) 大到小排序。

P4137

难度:蓝

这里是几种做法:

Sol1

莫队。问题在于如何快速找到 mex?引入 bitset,每个位置表示它是否出现过,出现过为 \(0\),没有为 \(1\),使用函数 _Find_first() 快速找到第一个 \(1\) 的位置。时间复杂度 \(O(n\sqrt n + \frac{nq}{w})\)

Sol2

离线下来。询问按右端点排序,使用值域线段树维护每个数的出现次数,然后二分答案即可。时间复杂度 \(O((n+q)\log^2 n)\)。好像可以在 sgt 上二分去掉一只 log。也可以写 bit 降低常数。

Sol3

考虑可持久化线段树,每个节点维护区间内的数出现位置的最小值。查询的时候,在第 \(r\) 棵线段树上,查询所有数出现位置的最小值是否 \(< l\) 即可。

P3604

难度:紫

神秘。考虑回文串要求啥,显然要求至多存在一种字符出现次数为奇数。考虑状压,用 \(26\) 个二进制位来表示每种字符出现次数的奇偶性。那么一个区间合法当且仅当这个区间的状态 \(S\)\(0\) 或为 \(2\) 的某个幂。
维护这个东西的前缀,即 \(S_i\) 表示区间 \([1,i]\),的状态。惊喜地发现这个是可差分的,区间 \([x,y]\) 的状态可以通过 \(S_y \oplus S_{x-1}\) 来得到。
考虑来莫队,每次加入一个 \(S_i\) 的时候,先把满足 \(S_j \oplus S_i = 0/2^?\) 的贡献加进去,减的时候同理。

P4396

难度:蓝

考虑莫队。然后使用值域 bit 维护数出现的次数。时间复杂度 \(O(n\sqrt n\log n)\)。也可以使用值域分块。

CF1806E

难度:青

Sol1

考虑欧拉序,一个很好的性质是出现在 \((x,y)\) 路径上的点,在欧拉序上 \([en_x,st_y]\) 里只出现了一次。可以使用莫队来维护区间的贡献。

Sol2

根号分治。

  • 对于节点数 \(x \le \sqrt n\) 的层,函数调用次数 \(\le \binom{x}{2}\),至多 \(\frac{n}{x}\) 层,总调用次数 \(\frac{n}{x}\binom{x}{2} \approx nx\),由于 \(x \le \sqrt n\),时间复杂度 \(O(n\sqrt n)\)
  • 对于节点数 \(x > \sqrt n\) 的层,至多 \(\sqrt n\) 层,每层函数至多调用 \(q\) 次,时间复杂度 \(O(q\sqrt n)\)

总的时间复杂度 \(O((n + q)\sqrt n)\)。注意节点数 \(> \sqrt n\) 的层不要记搜,因为常熟太大了。记搜使用数组。

Sol3

树分块做法。
考虑在树上随机撒 \(\sqrt n\) 个关键点,预处理关键点与同层之间的贡献,时间复杂度 \(O(n\sqrt n)\)。查询的时候暴力往上跳直到遇到关键点,期望跳 \(\sqrt n\) 次。时间复杂度 \(O((n+q)\sqrt n)\)

珂朵莉树

BY OI WIKI:

这个名称指代的是一种「使用平衡树(std::set、std::map 等)或链表(std::list、手写链表等)维护颜色段均摊」的技巧,而不是一种特定的数据结构.其核心思想是将值相同的一段区间合并成一个结点处理.相较于传统的线段树等数据结构,对于含有区间覆盖的操作的问题,珂朵莉树可以更加方便地维护每个被覆盖区间的值.

也就是说,区间推平、数据随机的问题是珂朵莉树喜欢的。可能除了推平以外,还夹杂其他操作。这里记录一下这个东西的原理:

原理

这个东西比较常见的维护是用 set 维护连续段。具体的用 set 维护段 \([l,r,v]\),表示区间 \([l,r]\) 的值为 \(v\)

#define iter set <node> iterator
struct node
{
    ll l, r; 
    mutable ll v; 
    node(ll l, ll r = 0, ll v = 0) : l(l), r(r), v(v) {}
    bool operator < (const node &a) const { return l < a.l; }
} ;

分裂(split)

给定参数 \(p\),把区间 \([l,r]\) 分裂成 \([l,p)\)\([p,r]\)。返回的是 \([p,r]\) 的迭代器。

然后过程就是我们找第一个左端点 \(\ge p\) 的区间迭代器 \(it\)。如果 \(p\) 是某个区间开头,直接返回这个区间对应的迭代器。如果找不到就退出。那么 --it 就是 \(p\) 对应的区间。那么直接 erase 掉即可。

iter split(ll p)
{
	iter it = s.lower_bound(node(p)); 
	if (it != s.end() && it.l == p) return it; 
	-- it; 
	if (it.r < p) return s.end(); 
	ll l = it.l; 
	ll r = it.r; 
	ll v = it.v; 
	s.erase(it); 
	s.insert(node(l, p - 1, v)); 
	return s.insert(node(p, r, v)).first; 
}

推平(assign)

把区间 \([l,r]\) 赋值为 \(v\)

void assign(ll l, ll r, ll v) 
{
    iter itr = split(r + 1), itl = split(l);
    s.erase(itl, itr);
    s.insert(node(l, r, v));
}

修改

对区间 \([l,r]\) 进行修改操作。以区间加 \(x\) 为例子:

void add(ll l, ll r, ll x)
{
    iter itr = split(r + 1), itl = split(l);
	for (iter it = itl; it != itr; ++ it ) 
		it.v += x; 
}

其他修改操作同理。

例题

CF896C

板子。

struct node 
{
	ll l, r; 
	mutable ll v; 
	node(ll l, ll r = 0, ll v = 0) : l(l), r(r), v(v) {} 
	bool operator < (const node &t) const { return l < t.l; }
} ; 
#define iter set <node>::iterator 
set <node> s; 

bool pp(node x, node y) { return x.v < y.v; }
ll fstpow(ll x, ll n, ll mod)
{
	ll s = 1; x %= mod; 
	while (n)
	{
		if (n & 1) s = s * x % mod; 
		n >>= 1; 
		x = x * x % mod; 
	}
	return s; 
}

iter split(ll p)
{
	// [l, r] -> [l, p) + [p, r]
	iter it = s.lower_bound(node(p)); 
	if (it != s.end() && it -> l == p) return it; 
	-- it; 
	if (it -> r < p) return s.end(); 
	ll l = it -> l; 
	ll r = it -> r; 
	ll v = it -> v; 
	s.erase(it); 
	s.insert(node(l, p - 1, v)); 
	return s.insert(node(p, r, v)).first; 
}
void assign(ll l, ll r, ll v)
{
	iter itr = split(r + 1), itl = split(l); 
	s.erase(itl, itr); 
	s.insert(node(l, r, v)); 
}
void add(ll l, ll r, ll x)
{
	iter itr = split(r + 1), itl = split(l); 
	for (auto it = itl; it != itr; ++ it ) 
		it -> v += x; 
}
ll kth(ll l, ll r, ll k)
{
	vector <node> vec; 
	iter itr = split(r + 1), itl = split(l); 
	for (auto it = itl; it != itr; ++ it ) 
		vec.pb(node(it -> l, it -> r, it -> v)); 
	sort(vec.begin(), vec.end(), pp); 
	for (auto x : vec)
	{
		if (k > x.r - x.l + 1)
			k -= (x.r - x.l + 1);
		else 
			return x.v; 
	}
	return 114514; 
}
ll getS(ll l, ll r, ll x, ll y)
{
	iter itr = split(r + 1), itl = split(l); 
	ll Res = 0; 
	for (auto it = itl; it != itr; ++ it ) 
	{
		ll d = fstpow(it -> v, x, y); 
		d = d * (it -> r - it -> l + 1) % y; 
		Res = (Res + d) % y; 
	}
	return Res; 
}

P1840

仍然是板子,这个用 sgt 和分块也可以做。这里记录一下简单做法:

\(f_i\) 表示 \(i\) 较远的一个被染白的点,满足 \([f_i,i]\) 都被染白。初始 \(f_i = i\)。每次对 \([l,r]\) 染色的时候,从右往左扫,如果 \(i\) 是白色的就跳到 \(f_i\),这个过程可以用并查集优化。如果 \(i\) 是黑色的,就令 \(f_i \leftarrow l\)\(i \leftarrow i-1\) 即可。这样就在几乎线性的时间复杂度解决了问题。

ll Q(ll l, ll r)
{
	iter itr = split(r + 1), itl = split(l); 
	ll Res = 0; 
	for (auto it = itl; it != itr; ++ it )
		Res += (it -> v == 1); 
	return Res; 
}

CF915E

同样是板子。不过这个题要注意优化,我们在 assign 的时候直接计算总和,这样跑的很快。

void assign(ll l, ll r, ll v)
{
	iter itr = split(r + 1), itl = split(l); 
	for (auto it = itl; it != itr; ++ it ) Res -= (it -> r - it -> l + 1) * (it -> v); 
	s.erase(itl, itr); 
	s.insert(node(l, r, v)); 
	Res += (r - l + 1) * v; 
}

P4979

究极 sb 题。这个题直接朴素地写 odt 会 tle。你需要直接维护值相同的尽量长的段,这个在 assign 的时候可以暴力维护。细节很多,很难写。

void assign(ll l, ll r, ll v)
{
	iter itr = split(r + 1), itl = split(l); 
	itl -> v = v; 
	while (itl != s.begin() && (-- itl) -> v == v) ; 
	if (itl -> v != v) ++ itl; 
	
	-- itr; itr -> v = v; 
	auto rend = s.end(); -- rend; 
	while (itr != rend && (++ itr) -> v == v) ; 
	if (itr -> v != v) -- itr; 
	
	l = itl -> l; 
	r = itr -> r; ++ itr; 
	s.erase(itl, itr); 
	s.insert(node(l, r, v));  
}
 
bool check(ll l, ll r)
{
//	for (auto t : s) cout << t.l << ' ' << t.r << ' ' << t.v << '\n';  
	 
	iter itl = s.upper_bound(node(l)); 
	iter itr = s.upper_bound(node(r)); 
	
	-- itl; -- itr; 
	
	if (itl != itr) return false; 
	if (l == 1 || r == n) return true; 
	
	if (itl -> l == l) -- itl; 
	if (itr -> r == r) ++ itr; 
	return (itl -> v != itr -> v); 
}
posted @ 2026-08-04 11:06  RainyRadio  阅读(6)  评论(0)    收藏  举报