二至济南--数据结构
$ update: 2025/9/15 $
不好写,更不好调
线段树
本质上就是归并
理解
将序列转化为树的形式,然后操作
多用于区间(修改,查询)问题
记得开四倍空间
例题
势能分析
在一些奇奇怪怪的要求下,线段树可能会卡爆
于是需要分析在线段树退化为暴力时的操作次数
显然我们通过分析使得线段树在暴力情况下操作次数尽可能小
更好的线段树
我们思考一下线段树的定义是什么 (虽然我不知道)
显然能得出 高效处理区间(修改查询)问题,代价是需要更大空间 (口胡的)
于是我们发现它好像和树没什么关系
事实上, 线段树的树状结构并没有什么用, 其通过分治的方法来定位区间 (也只是定位区间), 树状结构只起到辅助作用
因此, 比起纠结线段树的树状结构, 我们应该将重心放在线段树真正起作用的地方: 修改和查询
不过查询容易理解,这里重点放在修改
理解
首先,我们需要明白我们 "需要的东西"(对于每个节点)
这里以线段树区间加,区间乘,区间查询为例
每个节点要有节点的值, 加的懒标记, 乘的懒标记
注意到其可以被分成两类 : 信息(\(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\) )
至于其他的一切, 都明了
详见例题
例题
\(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\)的长度,其实也就是匹配失败后返回的位置
例题
//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]<<" ";
}