二至济南--数据结构

$ update: 2025/9/15 $

不好写,更不好调

线段树

本质上就是归并

理解

将序列转化为树的形式,然后操作

多用于区间(修改,查询)问题
记得开四倍空间

例题

Loj 6029

势能分析

在一些奇奇怪怪的要求下,线段树可能会卡爆
于是需要分析在线段树退化为暴力时的操作次数
显然我们通过分析使得线段树在暴力情况下操作次数尽可能小

更好的线段树

我们思考一下线段树的定义是什么 (虽然我不知道)
显然能得出 高效处理区间(修改查询)问题,代价是需要更大空间 (口胡的)
于是我们发现它好像和树没什么关系

事实上, 线段树的树状结构并没有什么用, 其通过分治的方法来定位区间 (也只是定位区间), 树状结构只起到辅助作用

因此, 比起纠结线段树的树状结构, 我们应该将重心放在线段树真正起作用的地方: 修改和查询
不过查询容易理解,这里重点放在修改

理解

首先,我们需要明白我们 "需要的东西"(对于每个节点)
这里以线段树区间加,区间乘,区间查询为例

每个节点要有节点的值, 加的懒标记, 乘的懒标记
注意到其可以被分成两类 : 信息(\(info\)),和标记(\(tag\))
这里有一个需要注意的点: 我们更新区间的值时,有 info[x]=info[x]*tag[x].cheng+tag[x].jia*(R-L+1)
有没有发现那个你习惯的, 下意识忽略的 R-L+1?
因此, 区间长度也在信息中, 请不需要忽略

现在,每个节点只有两种东西了( \(info\)\(tag\) ),而线段树要做的,就是将这些东西合并
一共三种( \(info\)\(info\), \(info\)\(tag\), \(tag\)\(tag\) )
至于其他的一切, 都明了
详见例题

例题

CPU 监控

\(code\)

//May all the beauty be blessed.
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mrx=0x7f7f7f7f;
int n,ori[100010],q;

struct infos{ //信息
	int ma,mal;
	infos(int ma=0,int mal=0):ma(ma),mal(mal){};
}info[100010<<2];
struct tags{ //懒标记
	bool f; //f为赋值标记,ad加法,adl最大加法,fd赋值,fdl最大赋值
	int ad,adl,fd,fdl; //加法表示赋值之前的加法
	tags(bool f=0,int ad=0,int adl=0,int fd=0,int fdl=0):f(f),ad(ad),adl(adl),fd(fd),fdl(fdl){};
}tag[100010<<2];

tags addtags(tags a,tags b){ //标记相加,注意顺序
	if(a.f){ //a有赋值
		if(b.f){ //b有赋值
			return tags(
			1,
			a.ad,a.adl, //赋值会覆盖掉以前的加法(这里的加法是赋值之前的)
			b.fd,max({a.fd+b.adl,b.fdl,a.fdl}) //赋值之后的加法等价于赋值
			);
		}else{ //b不赋值
			return tags(
			1,
			a.ad,a.adl,
			a.fd+b.ad,max(a.fdl,a.fd+b.adl) //赋值之后的加法等价于赋值
			);
		}
	}else{
		if(b.f){
			return tags(
			1,
			a.ad+b.ad,max({a.adl,a.ad+b.adl}), //更新一下赋值之前的加法(由于要保留历史最大值,这里不做清空)
			b.fd,b.fdl
			);
		}else{
			return tags(
			0,
			a.ad+b.ad,max({a.adl,a.ad+b.adl}),
			a.fd,a.fdl
			);
		}
	}
}
infos addintags(infos a,tags b){ 
	if(b.f) return infos(b.fd,max({a.ma+b.adl,b.fdl,a.mal}));
	else return infos(a.ma+b.ad,max(a.ma+b.adl,a.mal));
}
infos addinfos(infos a,infos b){ //这个就是pushup
	return infos(max(a.ma,b.ma),max(a.mal,b.mal));
}
void build(int L,int R,int x){ //建树
	if(L==R){
		info[x].ma=info[x].mal=ori[L];
		return;
	}
	int mid=L+R>>1;
	build(L,mid,x<<1);
	build(mid+1,R,x<<1|1);
	info[x]=addinfos(info[x<<1],info[x<<1|1]);
}
void pushdown(int L,int R,int x){ //下放
	tag[x<<1]=addtags(tag[x<<1],tag[x]);
	tag[x<<1|1]=addtags(tag[x<<1|1],tag[x]);
	info[x<<1]=addintags(info[x<<1],tag[x]);
	info[x<<1|1]=addintags(info[x<<1|1],tag[x]);
	tag[x]=tags();
}
void add(int L,int R,int l,int r,int s,int sk,int x){ //sk为赋值标记
	if(l<=L&&R<=r){
		if(sk){
			info[x]=addintags(info[x],tags(1,0,0,s,s));
			tag[x]=addtags(tag[x],tags(1,0,0,s,s));
		}else{
			info[x]=addintags(info[x],tags(0,s,s));
			tag[x]=addtags(tag[x],tags(0,s,s));
		}
		return;
	}
	pushdown(L,R,x);
	int mid=L+R>>1;
	if(l<=mid) add(L,mid,l,r,s,sk,x<<1);
	if(r>mid) add(mid+1,R,l,r,s,sk,x<<1|1);
	info[x]=addinfos(info[x<<1],info[x<<1|1]);
}
infos findl(int L,int R,int l,int r,int x){ //直接返回一个info类型,省时省力
	if(l<=L&&R<=r) return info[x];
	pushdown(L,R,x);
	int mid=L+R>>1;
	infos a=infos(-mrx,-mrx),b=infos(-mrx,-mrx);
	if(l<=mid) a=findl(L,mid,l,r,x<<1);
	if(r>mid) b=findl(mid+1,R,l,r,x<<1|1);
	return addinfos(a,b);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>ori[i];
	build(1,n,1);
	
	cin>>q;
	while(q--){
		char o;
		cin>>o;
		if(o=='Q'){
			int x,y;
			cin>>x>>y;
			cout<<findl(1,n,x,y,1).ma<<'\n';
		}else if(o=='A'){
			int x,y;
			cin>>x>>y;
			cout<<findl(1,n,x,y,1).mal<<'\n';
		}else if(o=='P'){
			int x,y,z;
			cin>>x>>y>>z;
			add(1,n,x,y,z,0,1);
		}else{
			int x,y,z;
			cin>>x>>y>>z;
			add(1,n,x,y,z,1,1);
		}
	}
}

平衡树

确实平衡

理解

目前最难的数据结构
写法很多,这里介绍无旋\(Treap\),即\(FHQ-Treap\)

\(Treap\)的节点有权值和一个随机优先级(用随机数实现)

其中,权值满足二叉搜索树,及对于每一个节点,它的左儿子的权值小于它,右儿子的权值大于它
同时,它还满足堆的性质,子节点的优先级比父亲小

为什么要随机优先级:正常的二叉搜索树会出现单链的情况,复杂度\(O(n)\),\(Treap\)通过随机赋优先级的方法尽可能使树均匀分布,复杂度\(\log n\)

\(FHQ-Treap\)

对于每个节点,我们需要记录它的权值,优先级,左右儿子
并在之后通过分裂合并来维护它

以下是P3369 【模板】普通平衡树的代码
\(code\):

//May all the beauty be blessed.
#include<bits/stdc++.h>
#define int long long
using namespace std;
int m;

mt19937 rnd(time(0)); //更好的随机数 
struct aaa{
	int idt,val,siz,idl,idr;  //随机优先级,权值,以自身为根的子树大小,左儿子,右儿子 
	aaa(int idt=0,int val=0,int siz=0,int idl=0,int idr=0):
		idt(idt),val(val),siz(siz),idl(idl),idr(idr){};
}a[100010];
int root,rtl; //根 根的编号 

void pushup(int k){ //用于更新节点状态 
	a[k].siz=a[a[k].idl].siz+a[a[k].idr].siz+1; //将自己也计入子树大小 
}
pair<int,int> split(int k,int x){ //从权值为x处分裂,返回左子树根和右子树根 
	if(!k) return {0,0}; 
	if(a[k].val<=x){ //在右子树 
		pair<int,int> aa=split(a[k].idr,x); //处理右子树 
		a[k].idr=aa.first; //first为左边原树,second为分出去的树 
		pushup(k);
		return {k,aa.second};  //一起返回 
	}else{ //在右子树,同理 
		pair<int,int> aa=split(a[k].idl,x);
		a[k].idl=aa.second;
		pushup(k);
		return {aa.first,k};
	}
}
int merge(int k1,int k2){ //合并 k1,k2为要合并的两根,切记k1的权值小于k2 
	if(!k1||!k2) return k1|k2;//k1 有就返回k1.k2有就返回k2 
	
	if(a[k1].idt<=a[k2].idt){ //根据优先级判断谁为根 
		a[k1].idr=merge(a[k1].idr,k2); //递归合并 
		pushup(k1);
		return k1;
	}else{
		a[k2].idl=merge(k1,a[k2].idl);
		pushup(k2);
		return k2;
	}
}
void add(int x){ //添加节点 
	pair<int,int> aa=split(root,x); //将树从权值为x处(如果有的话)分裂,左边为权值不大于x的树,右边为权值大于x的树 
	a[++rtl]=aaa(rnd(),x,1);//定义新节点 
	root=merge(merge(aa.first,rtl),aa.second); //将新节点合并进去,注意merge传参要求左边权值不大于右边 
}
void del(int x){ //删除 
	pair<int,int> aa=split(root,x); //分成不大于x和大于x两部分 
	pair<int,int> ab=split(aa.first,x-1);//将不大于x的部分 分为不大于x-1(严格小于x)和等于x的部分 
	root = merge(merge(ab.first, merge(a[ab.second].idl, a[ab.second].idr)), aa.second); //合并,有严格的顺序要求
	//将等于x的根的左右儿子连在一起(删除等于x的根),再与小于x的部分合并,然后与大于x的部分合并 
}
int rnk(int x){ //求出多少个数据比x小 
	pair<int,int> aa=split(root,x-1);//将不大于x-1的树分出来 
	int ans=a[aa.first].siz+1;//题目要求+1 
	root=merge(aa.first,aa.second); //合并回去删除影响 
	return ans;
}
int findl(int k,int x){ //查找第x大的数 
	if(!k) return -0x7f7f7f7f7f7f7f7f; //进入到未定义节点 
	if(a[a[k].idl].siz>=x){ //答案在左子树 
		return findl(a[k].idl,x);//前往左子树 
	}else if(x==a[a[k].idl].siz+1){//答案为根 
		return a[k].val;//直接返回 
	}else return findl(a[k].idr,x-a[a[k].idl].siz-1);//答案在右子树,特别的,我们将x减去左子树大小和根(别忘减去根) 
}
int findlfr(int k,int x){//找到小于x且最大的数 
	if(!k) return -0x7f7f7f7f7f7f7f7f;
	if(a[k].val<x) return max(findlfr(a[k].idr,x),a[k].val);//当前的根满足条件, 但不一定是最大的,因此向右子树找,并更新答案 
	else return findlfr(a[k].idl,x);//不符合条件,向左子树找,不更新答案 
}
int findlen(int k,int x){
	if(!k) return 0x7f7f7f7f7f7f7f7f;//初始化的目的是为了更新答案,因为取小,因此赋极大值 
	if(a[k].val>x) return min(findlen(a[k].idl,x),a[k].val);//同上 
	else return findlen(a[k].idr,x);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>m;
	while(m--){
		int opt,x;
		cin>>opt>>x;
		if(opt==1) add(x);
		if(opt==2) del(x);
		if(opt==3) cout<<rnk(x)<<'\n';
		if(opt==4) cout<<findl(root,x)<<'\n';
		if(opt==5) cout<<findlfr(root,x)<<'\n';
		if(opt==6) cout<<findlen(root,x)<<'\n';
	}
}

归并树

其实是从济南回来之后偶然学到的

注意到线段树在建树是可以顺便排个序(归并排序),这样线段树单个节点内存储的信息就是有序的,支持二分等操作

\(code\) 查询区间小于\(x\)的数量

//May all the beauty be blessed.
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,q,aa[1000010];

int a[25][1000010];
void build(int L,int R,int x){
	if(L==R){
		a[x][L]=aa[L];
		return;
	}
	int mid=L+R>>1;
	build(L,mid,x+1);
	build(mid+1,R,x+1);
	int ll=L,lr=mid+1;
	for(int i=L;i<=R;i++){
		if(ll>mid) a[x][i]=a[x+1][lr++];
		else if(lr>R) a[x][i]=a[x+1][ll++];
		else{
			if(a[x+1][ll]<a[x+1][lr]) a[x][i]=a[x+1][ll++];
			else a[x][i]=a[x+1][lr++];
		}
	}
}
int findl(int L,int R,int l,int r,int s,int x){
	if(l<=L&&R<=r){
		if(a[x][L]>s) return 0; //这里是一个细节,需要单独拿出来,因为二分全失败后最小返回L,计算为1,因此需要特判
		int ll=L,lr=R,res=L;
		while(ll<=lr){
			int mid=ll+lr>>1;
			if(a[x][mid]>s) lr=mid-1;
			else res=mid,ll=mid+1;
		}
		return res-L+1;
	}
	int mid=L+R>>1,res=0;
	if(l<=mid) res+=findl(L,mid,l,r,s,x+1);
	if(r>mid) res+=findl(mid+1,R,l,r,s,x+1);
	return res;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;i++) cin>>aa[i];
	build(1,n,0);
	while(q--){
		int l,r,s;
		cin>>l>>r>>s;
		cout<<findl(1,n,l,r,s,0)<<'\n';
	}
}

\(KMP\)

\(xby:\)字符串的东西很少考,要么都会要么都不会

显然我就是都不会的那个

理解

在此之前,我们需要先引入一个东西

\(border\)

在字符串中,一个字符串的\(border\)指的是既是该字符串前缀又是其后缀的真子串(不包括该字符串自己)

示例:
\(abcba\)
其一个\(border\)\(ab\)

好,现在我们来理解\(KMP\)

它通过对预处理模式串来构建部分匹配表(失败函数)
匹配文本串时, 遇到匹配失败的情况就将模式串指针向前移(将模式串整体后移), 以达到降低时间复杂度的目的

其中,失败函数(下文代码中的\(KMP\)数组),存的是每个位置前面的字串的最大\(boerder\)的长度,其实也就是匹配失败后返回的位置

例题

P3375 【模板】KMP

//May all the beauty be blessed.
#include<bits/stdc++.h>
#define int long long
using namespace std;
string s1,s2;
int kmp[1000010];
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>s1>>s2;
	
	//匹配模式串 
	for(int i=1,j=0;i<s2.size();i++){ //自己和自己匹配,从1开始 
		while(j&&s2[i]!=s2[j]) j=kmp[j-1];//失配,回到上一个匹配位 
		if(s2[i]==s2[j]) j++;//匹配 
		kmp[i]=j;//记录 
	}
	
	//匹配文本串 
	for(int i=0,j=0;i<s1.size();i++){//和文本串匹配 
		while(j&&s1[i]!=s2[j]) j=kmp[j-1];
		if(s1[i]==s2[j]) j++;
		
		if(j==s2.size()){ //全部匹配完 
			cout<<i-j+2<<'\n'; //此时注意下标 
			j=kmp[j-1];//回到最后一位的匹配位 
		}
		
	}
	for(int i=0;i<s2.size();i++) cout<<kmp[i]<<" ";
}

posted @ 2025-07-15 19:21  破碎中永恒  阅读(13)  评论(0)    收藏  举报