数据结构进阶

news/2025/11/8 8:19:12/文章来源:https://www.cnblogs.com/hjm0703/p/19187596

数列分块

基础

对于 分块模板 ,思路是最直接的一个,将原来的整个序列分成几块。

  • 对于要处理的区间内被完全包含的整块,直接打下懒标记 tag ,然后进行批量处理。

  • 对于左右两边零散的块,暴力处理。

单次修改时间复杂度: \(\mathcal O (\sqrt N)\)

可食用范围:

  1. 对于要写 区修区查 ,但是不想写 线段树 \(or\) 树状数组 的人十分适用。

  2. 有一些操作比较复杂,可能只能用 分块 做。

CODE:

数列分块入门2-区修区查

#include<bits/stdc++.h>
#define int long long
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=50005;
int n;
int a[N],sum[N],add[N];
int L[N],R[N],pos[N];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
void build(){int len=sqrt(n);for(int i=1;i<=len;i++){L[i]=(i-1)*len+1;R[i]=i*len;}if(R[len]<n) len++,L[len]=R[len-1]+1,R[len]=n;for(int i=1;i<=len;i++){for(int j=L[i];j<=R[i];j++)sum[i]+=a[j],pos[j]=i;}
}
void change(int l,int r,int c){int x=pos[l],y=pos[r];if(x==y){for(int i=l;i<=r;i++)a[i]+=c;sum[x]+=(r-l+1)*c;return;}for(int i=l;i<=R[x];i++) a[i]+=c,sum[x]+=c;for(int i=L[y];i<=r;i++) a[i]+=c,sum[y]+=c;for(int i=x+1;i<=y-1;i++) add[i]+=c,sum[i]+=(R[i]-L[i]+1)*c;
}
int ask(int l,int r,int m){int x=pos[l],y=pos[r],ans=0;if(x==y){for(int i=l;i<=r;i++)ans=(ans+a[i]+add[x])%m;return ans;}for(int i=l;i<=R[x];i++) ans=(ans+a[i]+add[x])%m;for(int i=L[y];i<=r;i++) ans=(ans+a[i]+add[y])%m;for(int i=x+1;i<=y-1;i++) ans=(ans+sum[i])%m;return ans;
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n;for(int i=1;i<=n;i++) cin>>a[i];build();for(int i=1;i<=n;i++){int opt,l,r,c;cin>>opt>>l>>r>>c;if(opt==0) change(l,r,c);else cout<<ask(l,r,c+1)<<'\n';}return 0;
}               

数列分块应用

数列分块入门8-区间众数

这个也是不想说些什么了,仅作示例,以供展示 数列分块 的作用。

CODE:

#include<bits/stdc++.h>
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=100005,M=2020;
int n,len;
int a[N],f[M][M][3],back[N];
int L[M],R[M],pos[N];
unordered_map<int,int> mp;
vector<int> v[N];
int vis[M][N];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
void build(){len=min((int)sqrt(n),80);int i;for(i=1;;i++){L[i]=R[i-1]+1;R[i]=min(L[i]+len,n);if(R[i]==n)break;}len=i;for(int i=1;i<=len;i++){for(int j=L[i];j<=R[i];j++)pos[j]=i;}for(int i=1;i<=len;i++){int ans=0,maxn=-1;for(int j=i;j<=len;j++){for(int k=L[j];k<=R[j];k++){int id=a[k];vis[i][id]++;if(vis[i][id]>maxn) maxn=vis[i][id],ans=id;if(vis[i][id]==maxn&&back[a[k]]<back[ans]) maxn=vis[i][id],ans=id;}f[i][j][1]=ans,f[i][j][2]=maxn;}}
}
int get(int l,int r,int x){int ans1,ans2,ll=0,rr=v[x].size()-1;while(ll<rr){int mid=(ll+rr)>>1;if(v[x][mid]>=l) rr=mid;else ll=mid+1;}ans1=ll;ll=0,rr=v[x].size()-1;while(ll<rr){int mid=(ll+rr+1)>>1;if(v[x][mid]<=r) ll=mid;else rr=mid-1;}ans2=ll;return ans2-ans1+1;
}
inline void work(int &ans,int &mx,int i,int l,int r){int an=get(l,r,a[i]);if(an>mx) mx=an,ans=a[i];else if(an==mx and back[a[i]]<back[ans]) ans=a[i];	
}
int ask(int l,int r){int x=pos[l],y=pos[r];if(x==y){int ans=0,maxn=-1;for(int i=l;i<=r;i++) work(ans,maxn,i,l,r);return ans;}int ans=0,maxn=-1;if(x+1<=y-1) ans=f[x+1][y-1][1],maxn=f[x+1][y-1][2];for(int i=l;i<=R[x];i++) work(ans,maxn,i,l,r);for(int i=L[y];i<=r;i++) work(ans,maxn,i,l,r);return ans;
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n;for(int i=1;i<=n;i++)cin>>a[i];int cnt=0;for(int i=1;i<=n;i++){if(mp[a[i]]==0)mp[a[i]]=++cnt,back[cnt]=a[i];a[i]=mp[a[i]];v[a[i]].push_back(i);}build();back[0]=1e18;for(int i=1;i<=n;i++){int l,r;cin>>l>>r;cout<<back[ask(l,r)]<<'\n';}return 0;
}

树状数组

我认为 树状数组 绝对是最简单实用的数据结构。

基本操作

如下图:

对于 add 操作: (对于 0b101)

我们需要对于这些数加: 0b1010b1100b10000b10000

我么发现如果现在处理了 x ,则下一次为 x' = x + lowbit(x)

对于 ask 操作:(对于 0b1111

我们需要将答案加上: 0b11110b11100b11000b1000

我么发现如果现在处理了 x ,则下一次为 x' = x - lowbit(x)

CODE:

struct tree{int c[N];void add(int x,int a){for(int i=x;i<=n;i+=(i&-i)) c[i]+=a;}int ask(int x){int ans=0;for(int i=x;i;i-=(i&-i)) ans+=c[i];return ans;}
}BIT;

优劣之处

优点: 时间复杂度 : \(\mathcal O (Q log N)\) ,空间复杂度 \(\mathcal O (N)\),常数十分小,代码短。

劣势: 对于维护 max,min , 则无法完成。

注意事项

树状数组 只能维护下标大于0,否则会死循环。

线段树

普通线段树

线段树可以说是 数据结构进阶 中的相对重要的部分了。

原理其实和 树状数组 比较相近,如下图:

4f0b5a1c900663d06c1bc0ed0f271186

当我们要处理区间 [3,8],其实是将其分为:[3,3],[4,5],[6,7],[8,8],然后整块进行批量处理。

CODE:

/*由于普通线段树需要build,这里直接写的动态开点*/
const int N=1e6+5;
namespace SMT{int root;int cnt;struct SigmentTree{int l,r;int lc,rc;int sum,tag;#define ls T[p].lc#define rs T[p].rc}T[N<<2];void push_down(int p){if(T[p].tag){int mid=T[p].l+T[p].r>>1;if(ls==0) {ls=++cnt;T[ls]={T[p].l,mid,0,0,0,0};}if(rs==0) {rs=++cnt;T[rs]={mid+1,T[p].r,0,0,0,0};}T[ls].sum+=(T[ls].r-T[ls].l+1)*T[p].tag;T[rs].sum+=(T[rs].r-T[rs].l+1)*T[p].tag;T[ls].tag+=T[p].tag;T[rs].tag+=T[p].tag;T[p].tag=0;}}void push_up(int p){T[p].sum=T[ls].sum+T[rs].sum;}void change(int &p,int l,int r,int x,int y,int k){if(!p) p=++cnt,T[p]={l,r,0,0,0,0};if(x<=l && r<=y){T[p].sum+=(r-l+1)*k;T[p].tag+=k;return;}push_down(p);int mid=l+r>>1;if(x<=mid) change(ls,l,mid,x,y,k);if(y>mid) change(rs,mid+1,r,x,y,k);push_up(p);}int ask(int p,int l,int r,int x,int y){if(!p) return 0;if(x<=l && r<=y) return T[p].sum;push_down(p);int mid=l+r>>1,ans=0;if(x<=mid) ans=(ans+ask(ls,l,mid,x,y));if(y>mid) ans=(ans+ask(rs,mid+1,r,x,y));return ans;}
}
using namespace SMT;

权值线段树

及将权值当作下标存贮,可以处理 前驱,后缀,一个数字的个数 等问题。

CODE:

/*
这就不放了,反正和前面的一样,只是用法不同。
*/

标记永久化

这里主要是处理下面的 主席树(可持久化线段树) ,因为在 主席树 中下传标记容易出错,因而我们可以想出一种办法不用标记下传。

及把标记留在原地,让后面查询时加上。

CODE:

长序列区修区查

#include<bits/stdc++.h>
#define int long long
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=1e6+5;
int n,m,root;
int lc[N],rc[N],sum[N],tag[N],cnt;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
inline void change(int &p,int l,int r,int x,int y,int k){if(!p) p=++cnt;if(x<=l&&r<=y){sum[p]+=k*(r-l+1);tag[p]+=k;return;}sum[p]+=k*(min(r,y)-max(x,l)+1);int mid=l+r>>1;if(x<=mid) change(lc[p],l,mid,x,y,k);if(y>mid) change(rc[p],mid+1,r,x,y,k);
}
inline int ask(int &p,int l,int r,int x,int y,int val){if(!p) return val*(min(r,y)-max(x,l)+1);if(x<=l&&r<=y) return sum[p]+val*(r-l+1);int mid=l+r>>1,ans=0;if(x<=mid) ans+=ask(lc[p],l,mid,x,y,val+tag[p]);if(y>mid) ans+=ask(rc[p],mid+1,r,x,y,val+tag[p]);return ans;
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n>>m;while(m--){int opt,l,r;cin>>opt>>l>>r;if(opt==1){int k;cin>>k;change(root,1,n,l,r,k);}else cout<<ask(root,1,n,l,r,0)<<'\n';}return 0;
}

线段树合并与分裂

合并

首先 线段树合并 分两种,一种为会覆盖原树,另一种反之。

第一种

void Merge(int &a,int b){if(!a||!b) {a=a+b;return;}sum[a]=sum[a]+sum[b];Merge(lc[a],lc[b]);Merge(rc[a],rc[b]);
}

第二种

int Merge(int &a,int b){if(a==0||b==0) {return a+b;}int p=++cnt;sum[p]=sum[a]+sum[b];lc[p]=Merge(lc[a],lc[b]);rc[p]=Merge(rc[a],rc[b]);return p;
}

但这其实是骗你的,第二种也会覆盖,如果我们changea==0 || b==0 , 的节点,其仍然会将原来的点更改,这种不被覆盖是相对的,覆盖指的是在下一次merge的时候不被更改。

分裂

分裂可以参考 FHQ Treap 的分裂方法。

CODE:

void split(int &a,int &b,int l,int r,int x,int y){if(!a) return;if(x<=l&&r<=y){b=a,a=0;return;}if(!b) b=++cnt;int mid=l+r>>1;if(x<=mid) split(lc[a],lc[b],l,mid,x,y);if(y>mid) split(rc[a],rc[b],mid+1,r,x,y);push_up(a),push_up(b);
}
/*转了一圈发现自己好像除了模板题其他没有写过一道*/

可持久化线段树

原理如下图:

当我们进行 change 操作时,除了对于访问的节点新建节点,其他的直接连上以前的节点,因而空间复杂度为:

\[\mathcal O(4N+Q \log N) \]

因而这样可以记录下每一个时间辍的线段树,再结合前缀和思想,可以处理对于以前版本的访问与更改。

CODE:
可持久化数组

#include<bits/stdc++.h>
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=1e6+5;
int n,m;
int tot,a[N],root[N];
int cnt,lc[N*32],rc[N*32],sum[N*32];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
inline void build(int &p,int l,int r){p=++cnt;if(l==r){sum[p]=a[l];return;}int mid=l+r>>1;build(lc[p],l,mid);build(rc[p],mid+1,r);
}
inline void change(int &a,int b,int l,int r,int x,int k){a=++cnt;if(l==r){sum[a]=k;return;}lc[a]=lc[b],rc[a]=rc[b];int mid=l+r>>1;if(x<=mid) change(lc[a],lc[b],l,mid,x,k);if(x>mid) change(rc[a],rc[b],mid+1,r,x,k);
}
inline int ask(int &p,int l,int r,int x){if(!p) return 0;if(l==r) return sum[p];int mid=l+r>>1;if(x<=mid) return ask(lc[p],l,mid,x);if(x>mid) return ask(rc[p],mid+1,r,x);
}
inline int read(){//快读int ans=0,j=1;char c=getchar();while(c>'9' or c<'0'){if(c=='-')j=-1;c=getchar();}while(c>='0' and c<='9'){ans=ans*10+c-'0';c=getchar();}return ans*j;
}
inline void write(int x){//快写if(x<0){putchar('-');x=-x;}if(!x)return;write(x/10);putchar(x%10+'0');
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){n=read(),m=read();for(int i=1;i<=n;i++) a[i]=read();build(root[0],1,n);for(int i=1;i<=m;i++){int a=read(),opt=read(),b=read();if(opt==1){int k=read();change(root[i],root[a],1,n,b,k);}else {root[i]=root[a];write(ask(root[a],1,n,b));puts("");}}return 0;
}

而对于区修区查,则需要使用前面提到的标记永久化。

CODE:

时光旅行者的数组维护

#include<bits/stdc++.h>
#define ll long long
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=1e6+6;
int n,m,cnt;
int a[N],root[N];
struct node{int lc,rc;ll sum,tag;
}t[N*60];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
inline int read(){//快读int ans=0,j=1;char c=getchar();while(c>'9' or c<'0'){if(c=='-')j=-1;c=getchar();}while(c>='0' and c<='9'){ans=(ans<<3)+(ans<<1)+(c^48),c=getchar();}return ans*j;
}
void build(int &p,int l,int r){p=++cnt;if(l==r){t[p].sum=a[l];return;}int mid=l+r>>1;build(t[p].lc,l,mid);build(t[p].rc,mid+1,r);t[p].sum=t[t[p].lc].sum+t[t[p].rc].sum;
}
void change(int &a,int b,int l,int r,int x,int y,int k){a=++cnt,t[a]=t[b];if(x<=l && r<=y){t[a].sum+=(r-l+1)*k;t[a].tag+=k;return;}t[a].sum+=(min(y,r)-max(x,l)+1)*k;int mid=l+r>>1;if(x<=mid) change(t[a].lc,t[b].lc,l,mid,x,y,k);if(y>mid) change(t[a].rc,t[b].rc,mid+1,r,x,y,k);
}
ll ask(int p,int l,int r,int x,int y,ll val){if(!p) return (min(y,r)-max(x,l)+1)*val;if(x<=l && r<=y) return t[p].sum+(r-l+1)*val;int mid=l+r>>1;ll ans=0;if(x<=mid) ans+=ask(t[p].lc,l,mid,x,y,val+t[p].tag);if(y>mid) ans+=ask(t[p].rc,mid+1,r,x,y,val+t[p].tag);return ans;
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){n=read(),m=read();for(int i=1;i<=n;i++) a[i]=read();build(root[0],1,n);for(int i=1;i<=m;i++){int v=read(),opt=read(),l=read(),r=read(),k;if(opt==1){k=read();change(root[i],root[v],1,n,l,r,k);}else{root[i]=root[v];ll ans=ask(root[v],1,n,l,r,0);printf("%lld\n",ans);}}return 0;
}

线段树分治

线段树分治 并不是在线段树上分支,而是以操作的时间戳建树,能够处理操作需要反悔的情况。

可以直接上例题:

动态图连通性

我们发现这道题其实只有在一段时间内才会加上某一条边,于是可以使用 线段树分治 ,以操作的时间戳建树,然后遍历整棵线段树。

所以我们仍需要知道如何反悔,因为在 如下图 中的处理时候,需要删去一些边。

屏幕截图 2025-11-06 164814

这时候可能会出现一些疑问,既然这里也要删去一些边,为和不直接暴力做呢?

这是因为如果我们把插入了边当作放入一个栈中,现在我们删去的边一定在栈顶。

所以我们可以直接在记录下加入一条边时,两个(一个)点的fa[]值,可以发现一定不会出现影响。

而如果在暴力中这样做,会发现有可能现在删除的边更新的fa[]已经影响到了其他边,故直接删除会有错。

CODE:

#include<bits/stdc++.h>
#define lc p<<1
#define rc p<<1|1
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=5e3+5,M=5e5+5;
int n,m,top;
struct node{int op,x,y;
}opt[M],s[M];
int fa[M],h[M];
int lst[N][N];
vector<node> v[M<<2];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
void change(int p,int l,int r,int x,int y,int a,int b){if(x<=l && r<=y){v[p].push_back({0,a,b});return;}int mid=l+r>>1;if(x<=mid) change(lc,l,mid,x,y,a,b);if(y>mid) change(rc,mid+1,r,x,y,a,b);
}
void init(){for(int i=1;i<=m;i++){int op=opt[i].op,x=opt[i].x,y=opt[i].y;if(op==1) lst[x][y]=i;if(op==2) {change(1,1,m,lst[x][y],i-1,x,y);lst[x][y]=0;}}for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){if(lst[i][j]) change(1,1,m,lst[i][j],m,i,j);}}
}
int find(int x){if(x==fa[x]) return x;return find(fa[x]);
}
void Merge(int x,int y){//把y放在xint rx=find(x),ry=find(y);if(rx==ry) return;if(h[rx]<h[ry]) swap(rx,ry);s[++top]={h[rx],rx,ry};fa[ry]=rx,h[rx]+=(h[rx]==h[ry]);
}
void dfs(int p,int l,int r){int now=top;for(int i=0;i<v[p].size();i++){Merge(v[p][i].x,v[p][i].y);}int mid=l+r>>1;if(l==r){if(opt[l].op==3){if(find(opt[l].x)==find(opt[l].y)) puts("Y");else puts("N");}}else{dfs(lc,l,mid);dfs(rc,mid+1,r);}while(now<top){fa[s[top].y]=s[top].y;h[s[top].x]=s[top].op;top--;}
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){scanf("%d%d",&n,&m);for(int i=1;i<=m;i++){int op,x,y;scanf("%d%d%d",&op,&x,&y);if(x>y) swap(x,y);opt[i]={op+1,x,y};}for(int i=1;i<=n;i++) fa[i]=i;init();dfs(1,1,m);return 0;
}

CDQ分治

普通 CDQ分治

CDQ分治 主要用于求解偏序问题,如这道题目 P3810 【模板】三维偏序(陌上花开)

CDQ分治 是分区间解决这个问题,假设我们现在正在处理 [l,r] 的信息,我们可以将其分为:[l,mid][mid+1,r] 解决,左右两边可以递归解决,现在考虑 i \(\in\) [l,mid]j \(\in\) [mid+1,r]。的信息。

对于三维偏序,我们如下处理:

  • 第一层: 直接排序,则如果ij,满足i < j 则满足。

  • 第二层: 在 CDQ分治 内部左边右边分别按第二层排序,然后在左右区间设定 双指针 ,即可满足。 这时候第一层依然满足,因为我们仅对 左右区间打乱,左右 i < j

  • 第三层: 在 双指针 实现同时将元素打入 树状数组 中,查询即可。

于是就有了代码:

CODE:

#include<bits/stdc++.h>
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=2e5+5;
int n,m,k;
int f[N];
struct node{//元素信息int a,b,c;int cnt,ans;
}e[N],a[N];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
struct tree{int c[N];void add(int x,int a){for(int i=x;i<N;i+=(i&-i)) c[i]+=a;}int ask(int x){int ans=0;for(int i=x;i;i-=(i&-i)) ans+=c[i];return ans;}
}BIT;
bool operator != (node a,node b){return a.a!=b.a||a.b!=b.b||a.c!=b.c;
}
bool cmpA(node a,node b){if(a.a!=b.a) return a.a<b.a;if(a.b!=b.b) return a.b<b.b;return a.c<b.c;
}
bool cmpB(node a,node b){if(a.b!=b.b) return a.b<b.b;return a.c<b.c;
}
void CDQ(int l,int r){if(l==r) return;int mid=l+r>>1;CDQ(l,mid),CDQ(mid+1,r);sort(a+l,a+mid+1,cmpB);sort(a+mid+1,a+r+1,cmpB);int i=l,j=mid+1;while(j<=r&&i<=mid){if(a[i].b<=a[j].b) BIT.add(a[i].c,a[i].cnt),i++;else a[j].ans+=BIT.ask(a[j].c),j++;}while(i<=mid) BIT.add(a[i].c,a[i].cnt),i++;while(j<=r) a[j].ans+=BIT.ask(a[j].c),j++;for(int ii=l;ii<=mid;ii++) BIT.add(a[ii].c,-a[ii].cnt);
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){cin>>n>>k;for(int i=1;i<=n;i++) cin>>e[i].a>>e[i].b>>e[i].c;sort(e+1,e+n+1,cmpA);int t=0;for(int i=1;i<=n;i++){t++;if(e[i]!=e[i+1]) a[++m]=e[i],a[m].cnt=t,t=0;}CDQ(1,m);for(int i=1;i<=m;i++) f[a[i].ans+a[i].cnt-1]+=a[i].cnt;for(int i=0;i<n;i++) cout<<f[i]<<'\n';return 0;
}

四位CDQ分治

主要是多的一维:

直接打上标记,然后进行正常CDQ:

  • 第一层: 直接排序,则如果ij,满足i < j 则满足。

  • 第二层: 打上标记。

  • 第三层: 在 CDQ分治 内部左边右边分别按第二层排序,然后在左右区间设定 双指针 ,即可满足。 这时候第一层依然满足,因为我们仅对 左右区间打乱,左右 i < j

  • 第四层: 在 双指针 实现同时将元素打入 树状数组 中,查询即可。

CODE:

[CH弱省胡策R2] TATT

#include<bits/stdc++.h>
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=1e5+5;
int n;//点的数量
int k;//去重后点的数量
int m;//离散化后d的数量
int h[N];//Hash
struct node{int a,b,c,d;//四个位置int w;//贡献int ans;//答案int falg;//标记左边,右边int id;//原位置bool operator ==(const node &p) const{return a==p.a&&b==p.b&&c==p.c&&d==p.d;}
}u[N],tmp[N];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
bool cmpA(node a,node b){if(a.a!=b.a) return a.a<b.a;if(a.b!=b.b) return a.b<b.b;if(a.c!=b.c) return a.c<b.c;return a.d<b.d;
}
bool cmpB(node a,node b){if(a.b!=b.b) return a.b<b.b;if(a.c!=b.c) return a.c<b.c;if(a.d!=b.d) return a.d<b.d;return a.a<b.a;
}
bool cmpC(node a,node b){if(a.c!=b.c) return a.c<b.c;if(a.d!=b.d) return a.d<b.d;if(a.a!=b.a) return a.a<b.a;return a.b<b.b;
}
struct tree{int c[N];void add(int x,int a){for(int i=x;i<=m;i+=(i&-i)) c[i]=max(c[i],a);}int ask(int x){int ans=0;for(int i=x;i;i-=(i&-i)) ans=max(ans,c[i]);return ans;}void clear(int x){for(int i=x;i<=m;i+=(i&-i)) c[i]=0;}
}BIT;
void CDQ2(int l,int r){if(l==r) return;int mid=l+r>>1;CDQ2(l,mid),CDQ2(mid+1,r);int i=l,j=mid+1,tot=l;while(j<=r&&i<=mid){if(u[i].c<=u[j].c){if(u[i].falg==1) BIT.add(u[i].d,u[i].ans);tmp[tot++]=u[i++];}else{if(u[j].falg==0) u[j].ans=max(u[j].ans,BIT.ask(u[j].d)+u[j].w);tmp[tot++]=u[j++];}}while(i<=mid) {if(u[i].falg==1) BIT.add(u[i].d,u[i].ans);tmp[tot++]=u[i++];}while(j<=r) {if(u[j].falg==0) u[j].ans=max(u[j].ans,BIT.ask(u[j].d)+u[j].w);tmp[tot++]=u[j++];}for(i=l;i<=mid;i++) if(u[i].falg) BIT.clear(u[i].d);for(i=l;i<=r;i++) u[i]=tmp[i];
}
void CDQ1(int l,int r){if(l==r) return;int mid=l+r>>1;CDQ1(l,mid);for(int i=l;i<=mid;i++) u[i].falg=1;for(int i=mid+1;i<=r;i++) u[i].falg=0;sort(u+l,u+r+1,cmpB);CDQ2(l,r);for(int i=l;i<=r;i++) tmp[u[i].id]=u[i];for(int i=l;i<=r;i++) u[i]=tmp[i];CDQ1(mid+1,r);
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){cin>>n;for(int i=1;i<=n;i++){cin>>u[i].a>>u[i].b>>u[i].c>>u[i].d;h[i]=u[i].d,u[i].w=1;}sort(h+1,h+n+1);sort(u+1,u+n+1,cmpA);m=unique(h+1,h+n+1)-h-1;//Hashk=1;for(int i=2;i<=n;i++){if(u[i]==u[k]) u[k].w++;else u[++k]=u[i];}for(int i=1;i<=k;i++){u[i].id=i;u[i].ans=u[i].w;u[i].d=lower_bound(h+1,h+m+1,u[i].d)-h;}CDQ1(1,k);int ans=0;for(int i=1;i<=k;i++) ans=max(ans,u[i].ans);cout<<ans;return 0;
}

N维CDQ分治

来观摩以下 5维CDQ分治

//在想什么呢,则么可能写的出来

整体二分

题目:

整体二分模板

点进去你应该看到的是 P3834 【模板】可持久化线段树 2 ,请仔细看标签。

这个模板题其实就是解决多次二分而生的,我在写这里时也不知道为什么他会在这里。

我们可以将要查询的区间先离线下来,然后统一递归处理。

这里我们递归的参数为 void solve(int l,int r,int al,int ar); 为 答案左右区间 和 操作左右区间。

然后处理 [al,ar] 的操作,根据其比 mid 大的数的个数分配递归的位置,这里修改操作一定在前面。

CODE:

#include<bits/stdc++.h>
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=2e5+5;
int n,m,tot;
int ans[N];
struct node{int op,x,y,k;int id;
}p[N<<1],ll[N<<1],rr[N<<1];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
struct tree{int c[N];void add(int x,int a){for(int i=x;i<=n;i+=(i&-i)) c[i]+=a;}int ask(int x){int ans=0;for(int i=x;i;i-=(i&-i)) ans+=c[i];return ans;}
}BIT;
void solve(int l,int r,int al,int ar){if(l>r || al>ar) return;if(l==r){for(int i=al;i<=ar;i++)if(p[i].op) ans[p[i].id]=l;return;}int mid=l+r>>1;//假设所有的第k小皆为midint nl=0,nr=0;for(int i=al;i<=ar;i++){if(p[i].op==0){//修改if(p[i].y<=mid){//x<=midBIT.add(p[i].x,1);ll[++nl]=p[i];}else rr[++nr]=p[i];}else{//查询int s=BIT.ask(p[i].y)-BIT.ask(p[i].x-1);if(s>=p[i].k) ll[++nl]=p[i];//在[l,r]的区间内小于mid的个数大于等于k,相当于mid>=anselse p[i].k-=s,rr[++nr]=p[i];//在[l,r]的区间内小于mid的个数小于k,相当于mid<ans}}for(int i=1;i<=nl;i++) p[al+i-1]=ll[i];for(int i=1;i<=nr;i++) p[al+nl+i-1]=rr[i];for(int i=al;i<=ar;i++)if(p[i].id==0&&p[i].y<=mid) BIT.add(p[i].x,-1);solve(l,mid,al,al+nl-1);solve(mid+1,r,al+nl,ar);
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n>>m;for(int i=1;i<=n;i++){int x;cin>>x;p[++tot]={0,i,x,0,0};}for(int i=1;i<=m;i++){int x,y,k;cin>>x>>y>>k;p[++tot]={1,x,y,k,i};}solve(1,1e9,1,tot);for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';return 0;
}

平衡树

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/959378.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

nats nkeys 实际的一些推荐玩法

nats nkeys 实际的一些推荐玩法nsc 是nats 推荐的基于jwt 的认证管理机制,当然使用上会有一些技巧 nsc 一些概念 包含了operator,account,user,一般我们需要先进行初始化,这个比较简单,但是注意存储的位置 参考玩…

successful

successful education is when people can differ pi and pie in 26 letters. so the literature in 26 letters may be also serious. Not everyone can differ Roma and R. Erdős Pl thinks SU is Joe. But US is S…

2025年知名的恩施装修半包热门推荐榜

2025年知名的恩施装修半包热门推荐榜 随着恩施城市化进程的加快,装修需求逐年增长,半包装修因其灵活性和高性价比成为许多业主的首选。半包模式由业主自行采购主材,装修公司负责辅材和施工,既能控制预算,又能保证…

中部经济第一省之争

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087中国战略新兴产业融媒体记者 卜文娟中部经济第一省,究竟是谁?翻开…

裁员下的上海

微信视频号:sph0RgSyDYV47z6快手号:4874645212抖音号:dy0so323fq2w小红书号:95619019828B站1:UID:3546863642871878B站2:UID: 3546955410049087如果裁员也有温度,现在的上海,大概是零下五度。3个月前,我朋友L…

ICPC2025武汉 游记(VP)

在奇葩实现错误的道路上一路狂奔狂吃罚时,$3t$ 罚时 $622$ 直接铁牌。省流 在奇葩实现错误的道路上一路狂奔狂吃罚时,\(3t\) 罚时 \(622\) 直接铁牌。11.3 内含剧透,请vp后再来。 不是题解!!!!!!! 赛前 早上…

2025.11.7 测试

2025.11.7 测试最近不知道咋啦,状态不是很好,老是考倒数... 考试策略大概是顺序开题,先看了一个半小时 T1 还是不会,打了 50pt 跑路 看 T2 ,像数据结构,一眼有 40 pt 暴力 先不打,看 T3 额,题目告诉我们是构造…

开发一个技术栈识别实用工具和共享资源库

开发一个技术栈识别实用工具和共享资源库可以识别第三方库,第三方加固,使用的语言。

不务正业

“当人们开始浪漫化一段故事,说明它已经成为了历史。”————兰德尔柯林斯(我不认识他)“如果你的回忆开始浪漫起来,那你应该记录下来。”————PencilWang(你可能认识他)或许改写点什么了,七年前写《不抖包…

开源项目Url-Shorten-Worker时隔多年再次更新,新增人机验证码功能,创建短链接时需要人机验证--基于Cloudflare Worker的长链接转短链接项目(轻松拥有属于自己的短网址)

RT:时隔多年,Url-Shorten-Worker 短网址程序迎来更新:新增人机验证(CAPTCHA)功能! 此次更新的默认策略为:创建短链接 时必须通过人机验证; 访问短链接 则无需验证(可自行配置)。为什么要进行这次更新? 公开…

1.2.3.4.5.6.7.8.9.10.

星期一:我流浪在上海。 星期二:这个夜晚,我吃药的时候,记忆起我早上的药忘记吃了。我还在寻找自己的爱情,因为我需要有个女朋友,可以在早晨与夜晚的时候提醒我吃药。我虚拟了一个梦境,我驾驶我的000号飞船,抵达…

linux分区扩容

1.检查硬盘容量1 lsblk 我的硬盘总容量200G,但根分区只有61.2G,我计划把剩余的100G分配到根分区2.对磁盘进行分区2 fdisk /dev/sda 输入命令查看:3 lsblk 分区完成,已有/dev/sda3,先重启一下。命令:4 reboot 3.…

DISM-Get-cmds

DISM-Get-cmds导航 (返回顶部)1. DISM-Get-cmds 2. 获取 Windows 映像信息2.1 Get-ImageInfo(Get-WimInfo) 2.2 Get-MountedImageInfo(Get-MountedWimInfo)3. 获取 Windows PE 信息3.1 Get-PESettings4. 获取驱动程序信…

AI元人文:智能理性主体的崛起——当AI成为文明的对话伙伴

AI元人文:智能理性主体的崛起——当AI成为文明的对话伙伴导论:理性形态的历史性跃迁 在人类文明发展的关键转折点,我们正见证着理性存在形式的深刻变革。这一变革不仅体现在技术能力的突破上,更在于一种新型智能理…

《计算机系统结构》学习笔记

现代硬件上的常数优化Lecture 3. ISA and Assembly 指令集 instruction set architecture 不只定义了指令的功能和格式,还定义了系统状态。 RISC 精简指令集 reduced IS computer,如 RISC-V 和 MIPS。CISC 复杂指令集…

Multi-Armed Bandit

问题描述 Bandit是一种常见的赌博机器。一般的赌场里的Bandit只有一个臂,你可以付钱来拉一次臂,机器会按照一个概率分布返回奖励。因为这样的机器常让赌徒输得精光,所以被称为“bandit(强盗)”。 数学上,我们考虑一…

2025年11月美白面霜产品排名榜:持证美白温和修护全解析

站在秋冬交界,肤色暗沉、色斑加深、护肤易脱皮成为高频痛点。很多人把“美白面霜”当成提亮捷径,却担心刺激、反黑、无效。2025年备案系统显示,持“美白特证”的新品同比增42%,但同期药监部门通报18批次标签违规,…

2025年11月北京生殖咨询公司推荐榜:美月国际咨询权威评测

正在备孕却反复受挫、高龄冻卵窗口期逼近、海外辅助生殖信息碎片化——这些真实焦虑让“北京生殖咨询公司”成为搜索热词。北京市卫健委2024年行业白皮书显示,近三年本市居民咨询海外辅助生殖的年增长率保持在18%,但…

2025年11月北京律师推荐榜:十大专业律师对比分析

在选择律师时,许多用户面临信息不对称、专业匹配度不足等痛点。作为首都,北京法律服务市场竞争激烈,用户往往需要综合考虑律师的专业领域、执业经验、行业口碑等因素。根据行业白皮书数据,北京律师行业呈现专业化、…