暑假集训__STL 进阶

STL 进阶

一、 set,multiset

建议提前熟悉一下指针的用法,不用过多,随便看看就行

大家应该已经学过平衡树了, $ set $ 是一个用平衡树实现的 集合 ,可以实现平衡树基本的 $ ins , del , find ,lower_bound $ 之类,下面是用法表和复杂度(deepseek)

其实 $ multiset $ 和 $ set $ 的区别就在于 , set中每个元素最多只能存在一个,而multiset则可以存在多次。

set 的常用用法和时间复杂度

set 底层是平衡树(通常是红黑树),元素会自动有序,且不允许重复。

set<int> s;
s.insert(5);
s.insert(3);
s.insert(5); // 重复插入不会生效

常用操作:

set<int> s = {1, 3, 5};

s.insert(4);          // 插入元素
s.erase(3);           // 删除指定值
s.erase(s.begin());   // 删除迭代器指向的元素
s.find(4);            // 查找,找不到返回 s.end()
s.lower_bound(4);    // 找第一个 >= 4 的位置
s.upper_bound(4);    // 找第一个 > 4 的位置
s.begin(); s.end();   // 头尾迭代器
s.size(); s.empty();  // 大小和是否为空
s.clear();            // 清空

时间复杂度:

操作 说明 复杂度
insert(x) 插入元素 \(O(\log n)\)
erase(x) 删除指定值 \(O(\log n)\)
erase(it) 删除迭代器指向元素 \(O(\log n)\)
find(x) 查找元素 \(O(\log n)\)
lower_bound(x) 找第一个 \(\ge x\) 的位置 \(O(\log n)\)
upper_bound(x) 找第一个 \(> x\) 的位置 \(O(\log n)\)
count(x) 统计元素个数(set 里只有 0/1) \(O(\log n)\)
begin / end 获取头尾迭代器 \(O(1)\)
size / empty 获取大小/判断是否为空 \(O(1)\)
clear 清空整个集合 \(O(n)\)
swap 交换两个集合 \(O(1)\)
遍历整个集合 例如 for(auto x : s) \(O(n)\)

注意:set 通常不能直接修改已有元素的值;如果要改,通常先删再插。

set<int> s;
s.insert(1);s.insert(3);
s.insert(3);//这次是不会再插入一个3,所以最终 s 就是 {1,3} . 并且insert是有返回值的,返回一个pair数组,first -> 一个指针,指向新插入的位置 ,second -> 一个bool,表示是否成功插入(1 -> yes)
multiset<int> s;
s.insert(1);s.insert(3);
s.insert(3);//最终s为 {1,3,3},同时insert返回的值只有一个指针指向新插入的位置。

给以先做一下 【深基17.例5】木材仓库 熟悉一下set的用法

简单熟悉set的用法后,可以开始学习珂朵莉了。

珂朵莉

珂朵莉其实是用来解决一种可以实现区间推平的操作的题,而且数据得随机,以实现区间分类尽量少。

原理就是如果一段区间内的值都相同,那么我没必要分开来维护每一个点的值,可以挂一个 $ {l,r,v} $ 来统一维护他们。通过暴力枚举来操作,通过区间推平操作来平衡复杂度。

下面接着珂朵莉源题讲具体实现。

例题 : CF896C Willem, Chtholly and Seniorious

题目描述

——威廉……

——怎么了?

——好像瑟尼欧里斯出问题了……

——我去看看……

瑟尼欧里斯是通过特定顺序连接特殊护符制成的。

经过了 500 多年,这个圣剑现在状况不太好,所以威廉决定彻底检查它。

瑟尼欧里斯有 \(n\) 片护符。威廉将它们排成一行,第 \(i\) 片护符是一个整数 \(a _ i\)

为了维护它,威廉需要执行 \(m\) 次操作。

有四种操作类型:

  • \(1\ l\ r\ x\) :对于 \(i\) 满足 \(l \le i \le r\),将 \(a _ i + x\) 赋值给 \(a _ i\)
  • \(2\ l\ r\ x\) :对于 \(i\) 满足 \(l \le i \le r\),将 \(x\) 赋值给 \(a _ i\)
  • \(3\ l\ r\ x\) :输出范围 \([l, r]\) 中第 \(x\) 小的数字,即在所有满足 \(l \le i \le r\)\(a _ i\) 排序后第 \(x\) 小的数字。保证 \(1 \le x \le r - l + 1\)
  • \(4\ l\ r\ x\ y\) :输出范围 \([l, r]\) 中所有 \(a _ i\)\(x\) 次幂之和模 \(y\),即 \(\left( \sum _ {i = l} ^ r {a _ i} ^ x \right) \bmod y\)
输入格式

第一行包含四个整数 \(n, m, seed, v _ {max}\)\(1 \le n, m \le 10 ^ 5\)\(0 \le seed < 10 ^ 9 + 7\)\(1 \le v _ {max} \le 10 ^ 9\))。

初始值和操作通过如下伪代码生成:

def rnd():

    ret = seed
    seed = (seed * 7 + 13) mod 1000000007
    return ret

for i = 1 to n:

    a[i] = (rnd() mod vmax) + 1

for i = 1 to m:

    op = (rnd() mod 4) + 1
    l = (rnd() mod n) + 1
    r = (rnd() mod n) + 1

    if (l > r): 
         swap(l, r)

    if (op == 3):
        x = (rnd() mod (r - l + 1)) + 1
    else:
        x = (rnd() mod vmax) + 1

    if (op == 4):
        y = (rnd() mod vmax) + 1

这里的 \(op\) 是题目中提到的操作类型。

输出格式

对于每个类型为 \(3\)\(4\) 的操作,输出答案。


其实这题主要是因为有了操作二区间推平和数据的随即构造(是的,我甚至没发现这是随即构造),才得以让使用珂朵莉不同于暴力。

首先我们应构造一个结构体来存储各区间

struct jj{
	mutable int l,r,v;//我们说了set一般不能改,但mutable 表示可以改变,用以改变被const限制的变量,主要是减少常数(提升较大),毕竟珂朵莉本身就是卡常
	inline bool operator <(const jj a)const {return l<a.l;}
};
set<jj> s;

接下来我们考虑在维护过程中的 $ 1,3,4 $ 操作都是对一段区间进行操作或询问,首先我们需要把 $ [l,r] $ 这段区间都找出来,这时就需要一个珂朵莉中的核心操作:

split

用以找到以 $ x $ 为起始的一段区间的指针,如果 $ x $ 在珂朵莉中一段 $ [l1,r1] $ , 的区间内,就把其分成 $ [l1,x-1] , [x,r1] $ ,并返回后者的指针。

inline auto fen(int x){
	auto it=s.lower_bound({x,0,0});// it 是指针
	if(it!=s.end()&&it->l==x)return it;//刚好找到了一段以 x  为起始的区间,直接返回 it
	--it;//没有刚好以x为起始的区间,it指向的是更大的,--后指向的就是 x 所在的一段区间
	if(it->r<x)return s.end();//此类情况出现于 x > 整段区间长度len,所以根本找不到,但要特判,不然会传回错误的指针
	int r=it->r,v=it->v;//暂存,下面要修改it
	it->r=x-1;//mutable 直接修改卡常,分出 [l,x-1]
    return s.insert({x,r,v}).fi;//插入[x,r],值为v,返回的是指针
}

推平

将一整段区间 $ [l,r] $ 全部修改为值为 $ v $ ,直接看代码

inline void tui(int l,int r,int v){
	auto i2=fen(r+1),i1=fen(l);i1->r=r,i1->v=v;
	s.erase(++i1,i2);
}

值得注意的是,我们在找 $ i1,i2 $ 时,必须先找到后面的 $ i2 $ 再找前面的 $ i1 $ ,我们考虑这样一种情况

假设有一段区间 $ [1,10] $ ,要推平的区间为 $ [3,5] $ ,如果先找到 $ i1 $ 就会有 $ [1,2],[3,10] $ ,再次找 $ i2 $ 时,会涉及到对 $ i1 $ 所存在指针的修改,这样 $ i1 $ 可能会被扰动,可能RE或WA。(当然基本上只要不出现erase这种操作就不会爆炸,按照我这种写法,前后不重要,但是如果你自己换了一种写法很可能会爆炸,而且后面有不少题需要很多不同操作,建议按照这种顺序写)

其他的操作很容易实现,就不写了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int ll
typedef unsigned long long ull;
typedef pair<int,int> pii;
#define fi first
#define se second
#define ps push_back
#define mk make_pair
const int N=2e5+10,inf=0x3f3f3f3f,mod=1e9+7;
inline ll read(){
	char c=getchar();ll x=0;bool f=0;
	while(!isdigit(c))f=c=='-'?1:0,c=getchar();
	while(isdigit(c))x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return f?-x:x;
}
struct jj{
	mutable int l,r,v;
	inline bool operator <(const jj a)const {return l<a.l;}
};
set<jj> s;
inline auto fen(int x){
	auto it=s.lower_bound({x,0,0});
	if(it!=s.end()&&it->l==x)return it;
	--it;
	if(it->r<x)return s.end();
	int r=it->r,v=it->v;
	it->r=x-1;return s.insert({x,r,v}).fi;
}
int n,m,sd,vmax;
inline int rd(){
	int ans=sd;
	sd=(sd*7+13)%mod;
	return ans;
}
inline void add(int l,int r,int v){
	auto i1=fen(l),i2=fen(r+1);
	while(i1!=i2){
		i1->v+=v;++i1;
	}
}
inline void tui(int l,int r,int v){
	auto i1=fen(l),i2=fen(r+1);
	i1->r=r,i1->v=v;
	s.erase(++i1,i2);
}
inline int kth(int l,int r,int x){
	auto i1=fen(l),i2=fen(r+1);
	vector<pii> v;
	while(i1!=i2)v.ps({i1->r-i1->l+1,i1->v}),++i1;
	sort(v.begin(),v.end(),[](const pii x,const pii y){return x.se<y.se;});
	for(auto i:v){
		if(x<=i.fi)return i.se;
		x-=i.fi;
	}
}
inline int qpow(int x,int y,int mo){
	int ans=1;x%=mo;
	while(y){
		if(y&1)ans=ans*x%mo;
		x=x*x%mo,y>>=1;
	}
	return ans;
}
inline int ask(int l,int r,int x,int y){
	auto i1=fen(l),i2=fen(r+1);
	int ans=0;
	while(i1!=i2)ans=(ans+qpow(i1->v,x,y)*(i1->r-i1->l+1))%y,++i1;
	return ans;
}
signed main(){
	#ifndef ONLINE_JUDGE
	freopen("in.in","r",stdin);
	freopen("out.out","w",stdout);
	#endif
	n=read(),m=read(),sd=read(),vmax=read();
	for(int i=1;i<=n;++i)
		s.insert({i,i,(rd()%vmax)+1});
	for(int i=1,op,l,r,x,y;i<=m;++i){
		op=(rd()%4)+1,l=(rd()%n)+1,r=(rd()%n)+1;
		if(l>r)swap(l,r);
		if(op==3)x=(rd()%(r-l+1))+1;
		else x=(rd()%vmax)+1;
		if(op==4)y=(rd()%vmax)+1;
		if(op==1)add(l,r,x);
		else if(op==2)tui(l,r,x);
		else if(op==3)cout<<kth(l,r,x)<<'\n';
		else cout<<ask(l,r,x,y)<<'\n';
	}
}

那么这个东西是什么复杂度呢,详见 珂朵莉树/颜色段均摊 ,其实我根本不会他的证明,一直把他当卡常来用,一般有推平和随机的题就用就行了。

建议做题顺序

2->1->3->5->4

posted @ 2026-07-25 14:35  lzrG23  阅读(33)  评论(0)    收藏  举报