数据结构
(~如果有的话~) 读者请先看底部说明
1.并查集
并查集是一种能够判断两个元素是否属于同一个集合的数据结构,主要有操作有:将两个集合合并;查询一个元素所在的集合。
查询:
int fid(int x){
if(fa[x]==x) return x;
return fa[x]=fid(fa[x]);
}
合并
void merge(int x,int y){
x=fa[x],y=fa[y];
if(x==y) return;
fa[x]=y;
}
2.线段树
(1).基础
略
(2).扫描线
_1.求面积
首先将点离散化。将每一根线段用线段树上的一个点表示,线段树上的每一个点代表的线段都是右端点
_2.特殊意义的扫描线
可以处理除去
同样将每根线段的左右端点单独列出来,并打上
_3.李超线段树
P4097
李超线段树用于解决:在平面直角坐标系中插入线段,问一个
原理:两条线段最多只有一个交点,根据这点,对一个区间,李超线段树只维护在该区间内在大多数时候
代码:
#include<bits/stdc++.h>
using namespace std;
int m,ans,tot;
const int inf=1e9+7;
const double eps=1e-10;
struct o{
int xma,xmi;
double k,b;
double operator ()(int x){
if(x<=xma&&x>=xmi) return 1.0*(k*x+b);
return -inf;
}
}a[5000100];
struct oo{
int tree[5000100];
pair<double,int> ma(pair<double,int>a,pair<double,int>b ){
if(a.first-b.first>eps) return a;
if(b.first-a.first>eps) return b;
return a.second<b.second?a:b;
}
int ls(int k){return k<<1;}
int rs(int k){return k<<1|1;}
void change(int k,int l,int r,int x,int y,int v){
if(x<=l&&y>=r){
if(!tree[k]){
tree[k]=v;
return;
}
int mid=(l+r)>>1;
if(a[v](mid)-a[tree[k]](mid)>eps) swap(v,tree[k]);
if(a[v](l)-a[tree[k]](l)>eps||(fabs(a[v](l)-a[tree[k]](l))<=eps&&v<tree[k])) change(ls(k),l,mid,x,y,v);
if(a[v](r)-a[tree[k]](r)>eps||(fabs(a[v](r)-a[tree[k]](r))<=eps&&v<tree[k])) change(rs(k),mid+1,r,x,y,v);
return;
}
int mid=(l+r)>>1;
if(x<=mid) change(ls(k),l,mid,x,y,v);
if(y>mid) change(rs(k),mid+1,r,x,y,v);
return;
}
pair<double,int> ask(int k,int l,int r,int x){
pair<double,int>res;
if(tree[k]) res=make_pair(a[tree[k]](x),tree[k]);
if(l==r){
return res;
}
int mid=(l+r)>>1;
if(x<=mid) res=ma(ask(ls(k),l,mid,x),res);
else res=ma(ask(rs(k),mid+1,r,x),res);
return res;
}
}t;
int read(){
char ch=getchar();int x=0,f=1;
while(ch<'0'||ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch<='9'&&ch>='0'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
int main(){
m=read();
while(m--){
int op=read();
if(op){
int x0=read(),y0=read();
int x1=read(),y1=read();
x0=(x0+ans-1)%39989+1;
y0=(y0+ans-1)%(1000000000)+1;
x1=(x1+ans-1)%39989+1;
y1=(y1+ans-1)%(1000000000)+1;
if(x1==x0){
++tot;
a[tot].xma=a[tot].xmi=x1;
a[tot].b=max(y1,y0);
a[tot].k=0;
}else{
a[++tot].xma=max(x1,x0);
a[tot].xmi=min(x1,x0);
a[tot].k=1.0*(y0-y1)/(x0-x1);
a[tot].b=1.0*(1.0*y1-1.0*a[tot].k*x1);
}
t.change(1,1,39990,min(x1,x0),max(x1,x0),tot);
}else{
int x=read();x=(x+ans-1)%39989+1;
ans=t.ask(1,1,39990,x).second;
printf("%d\n",ans);
}
}
return 0;
}
当然李超线段树也可以维护区间最值:
P4069
代码:
#include<bits/stdc++.h>
using namespace std;
long long dis[1000100],atid[1000100],e,head[1000100],dep[1000100],siz[1000100],fa[1000100],son[1000100];
long long top[1000100],id[1000100],dfn,n,m,tot;
const long long inf=123456789123456789;
struct o{
long long ne,to,w;
}a[1000100];
struct oo{
long long xma,xmi,k,b;
long long operator ()(long long x){
return k*x+b;
}
}g[1000100];
struct ooo{
long long tree[1000100],mi[1000100];
void init(){
for(long long i=0;i<=1000000;i++){
mi[i]=inf;
}
}
long long ls(long long k){return k<<1;}
long long rs(long long k){return k<<1|1;}
void change(long long k,long long l,long long r,long long x,long long y,long long v){
if(x<=l&&y>=r){
mi[k]=min(mi[k],min(g[v](dis[atid[l]]),g[v](dis[atid[r]])));//维护答案
long long mid=(l+r)>>1;
if(g[v](dis[atid[mid]])<g[tree[k]](dis[atid[mid]])){
swap(v,tree[k]);//维护大多数时候最优的 注意答案不能用mid更新
}
if(l==r) return;
if(g[v](dis[atid[l]])<g[tree[k]](dis[atid[l]])) change(ls(k),l,mid,x,y,v);
if(g[v](dis[atid[r]])<g[tree[k]](dis[atid[r]])) change(rs(k),mid+1,r,x,y,v);
mi[k]=min(mi[k]/*不要忘了和自己比较*/,min(mi[ls(k)],mi[rs(k)]));//
return;
}
long long mid=(l+r)>>1;
if(x<=mid) change(ls(k),l,mid,x,y,v);
if(y>mid) change(rs(k),mid+1,r,x,y,v);
mi[k]=min(mi[k],min(mi[ls(k)],mi[rs(k)]));//
}
long long ask(long long k,long long l,long long r,long long x,long long y){
if(x<=l&&y>=r){
return mi[k];
}
long long mid=(l+r)>>1,res=min(g[tree[k]](dis[atid[max(l,x)]]),g[tree[k]](dis[atid[min(r,y)]]));//
if(x<=mid) res=min(res,ask(ls(k),l,mid,x,y));
if(y>mid) res=min(res,ask(rs(k),mid+1,r,x,y));
return res;
}
}t;
void dd(long long u,long long v,long long w){
a[++e].ne=head[u];
a[e].to=v;
a[e].w=w;
head[u]=e;
}
void dfs1(long long x,long long f){
dep[x]=dep[f]+1;
siz[x]=1;
fa[x]=f;
for(long long i=head[x];i;i=a[i].ne){
long long v=a[i].to;
if(v==f) continue;
dis[v]=dis[x]+a[i].w;
dfs1(v,x);
siz[x]+=siz[v];
if(!son[x]||siz[son[x]]<siz[v]) son[x]=v;
}
}
void dfs2(long long x,long long tp){
top[x]=tp;
id[x]=++dfn;
atid[dfn]=x;
if(!son[x]) return;
dfs2(son[x],tp);
for(long long i=head[x];i;i=a[i].ne){
long long v=a[i].to;
if(v==fa[x]||v==son[x]) continue;
dfs2(v,v);
}
}
long long lca(long long x,long long y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=fa[top[x]];
}
return dep[x]<dep[y]?x:y;
}
long long getdis(long long x,long long y){
return dis[x]+dis[y]-(dis[lca(x,y)]<<1);
}
void change(long long x,long long y,long long v){
while(top[x]!=top[y]){
t.change(1,1,n,id[top[x]],id[x],v);
x=fa[top[x]];
}
t.change(1,1,n,id[y],id[x],v);
}
long long ask(long long x,long long y){
long long res=inf;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
res=min(res,t.ask(1,1,n,id[top[x]],id[x]));
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
res=min(res,t.ask(1,1,n,id[x],id[y]));
return res;
}
long long read(){
char ch=getchar();long long x=0,f=1;
while(ch<'0'||ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch<='9'&&ch>='0'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
int main(){
n=read();m=read();
for(long long i=1;i<n;i++){
long long u=read(),v=read(),w=read();
dd(u,v,w);dd(v,u,w);
}
t.init();
dfs1(1,0);dfs2(1,1);
g[0].b=inf; //
while(m--){
long long op=read(),x=read(),y=read();
if(op==1){
long long k=read(),b=read();
long long anc=lca(x,y);
g[++tot].k=-k;
g[tot].b=k*dis[x]+b;
g[tot].xma=dis[x];
g[tot].xmi=dis[anc];
change(x,anc,tot);
g[++tot].k=k;
g[tot].b=b+k*dis[x]-2*k*dis[anc];
g[tot].xma=dis[y];
g[tot].xmi=dis[anc];
change(y,anc,tot);
}else{
printf("%lld\n",ask(x,y));
}
}
return 0;
}
可以和斜率优化结合:
P4655
转移显然有:
暴力
先把平方展开:
因为我们肯定是从
因为
代码:
#include<bits/stdc++.h>
using namespace std;
long long h[1000100],w[1000100],sum[1000100],tot;
struct oo{
long long k,b;
long long operator ()(long long x){
return k*x+b;
}
}a[1000100];
struct o{
long long tree[8000100];
long long ls(long long k){return k<<1;}
long long rs(long long k){return k<<1|1;}
void change(long long k,long long l,long long r,long long x,long long y,long long v){
if(x<=l&&y>=r){
long long mid=(l+r)>>1;
if(a[v](mid)<a[tree[k]](mid)) swap(v,tree[k]);
if(l==r) return;
if(a[v](l)<a[tree[k]](l)) change(ls(k),l,mid,x,y,v);
if(a[v](r)<a[tree[k]](r)) change(rs(k),mid+1,r,x,y,v);
return;
}
long long mid=(l+r)>>1;
if(x<=mid) change(ls(k),l,mid,x,y,v);
if(y>mid) change(rs(k),mid+1,r,x,y,v);
}
long long ask(long long k,long long l,long long r,long long x){
if(l==r){
return a[tree[k]](l);
}
long long mid=(l+r)>>1;
long long res=a[tree[k]](x);
if(x<=mid) res=min(res,ask(ls(k),l,mid,x));
else res=min(res,ask(rs(k),mid+1,r,x));
return res;
}
}t;
long long read(){
char ch=getchar();long long x=0,f=1;
while(ch<'0'||ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch<='9'&&ch>='0'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
long long pf(long long x){return x*x;}
int main(){
int n=read();
for(int i=1;i<=n;i++) h[i]=read();
for(int i=1;i<=n;i++){
long long x=read();sum[i]=sum[i-1]+x;
}
a[0].b=1e18+7;
a[++tot].k=-2*h[1];
a[tot].b=pf(h[1])-sum[1];
t.change(1,0,1000000,0,1000000,tot);
for(int i=2;i<n;i++){
long long x=t.ask(1,0,1000000,h[i])+pf(h[i])+sum[i-1];
a[++tot].k=-2*h[i];
a[tot].b=pf(h[i])-sum[i]+x;
t.change(1,0,1000000,0,1000000,tot);
}
printf("%lld\n",t.ask(1,0,1000000,h[n])+pf(h[n])+sum[n-1]);
return 0;
}
_4.可持久化线段树
@1基础
先看最简单的场景:对一个序列,每次更新只修改一个数,并把新序列当作一个新的版本。要查询某个版本序列的区间和。如果只保留最后版本的话很容易想到用线段树维护。但现在要维护多个版本,最简单的线段树已经不能达到目标了。思考一下,每次只改一个数,那么序列每次的变化其实是很小的,如果用线段树维护的话,不需要对整个序列重新建立一棵完整的线段树,只维护变化的节点就行了。根据这个思路,我们就可以创造出可持久化线段树了。
具体是这样操作的:每次修改,沿途新建,其余复制。在经典线段树上,每次修改都会有一个树上路径,由于新版本的线段树和旧版本的不一样,但又不能覆盖旧版本,所以沿途的每个节点都要新建。根据之前的思路,新节点的大部分信息与旧节点相同,所以可以先复制旧节点信息,只修改不同的部分。具体见代码:
int change(int pre,int l,int r,int x,int v){
int k=++tot;
tree[k]=tree[pre];
if(l==r){
tree[k].sum=v;
return;
}
int mid=(l+r)>>1;
if(x<=mid) tree[k].ls=change(tree[pre].ls,l,mid,x,v);
else tree[k].rs=change(tree[pre].rs,mid+1,r,x,v);
tree[k].sum=tree[tree[k].ls].sum+tree[tree[k].rs].sum;
return k;
}
不难发现一次修改最多修改
那么区间修改、新建版本呢?其实也是可以的。由于懒标记的存在,我们可以积蓄修改操作,直到需要的时候再下传。主要操作依旧是沿途修改,其余复制,有些不同的是,在下传操作时也需要新建节点(因为下传之后信息发生变化,而旧节点可能仍在某个版本中需要被复用,所以需要新节点维护),但如果是在修改操作时下传,在下传之后立马就要修改,那么刚刚新建的节点可能就直接被浪费了。但其实这不要紧,即使有浪费的情况,一次修改消耗的空间依然是
模板题:
SP11470
代码:
#include<bits/stdc++.h>
using namespace std;
long long n,q,a[1000100],tot,rt[1000100];
struct o{
long long ls,rs,sum,lan;
}tree[10000100];
long long clone(long long v){
long long k=++tot;
tree[k]=tree[v];
return k;
}
void pup(long long k){
tree[k].sum=tree[tree[k].ls].sum+tree[tree[k].rs].sum;
}
void build(long long &k,long long l,long long r){
if(!k) k=++tot;
if(l==r){
tree[k].sum=a[l];
return;
}
long long mid=(l+r)>>1;
build(tree[k].ls,l,mid);
build(tree[k].rs,mid+1,r);
pup(k);
}
void add(long long k,long long l,long long r,long long v){
tree[k].sum+=(r-l+1)*v;
tree[k].lan+=v;
}
void pud(long long k,long long l,long long r,long long mid){
if(tree[k].lan){
tree[k].ls=clone(tree[k].ls);
tree[k].rs=clone(tree[k].rs);
add(tree[k].ls,l,mid,tree[k].lan);
add(tree[k].rs,mid+1,r,tree[k].lan);
tree[k].lan=0;
}
}
long long change(long long pre,long long l,long long r,long long x,long long y,long long v){
long long k=clone(pre);
if(x<=l&&y>=r){
add(k,l,r,v);
return k;
}
long long mid=(l+r)>>1;
pud(k,l,r,mid);
if(x<=mid) tree[k].ls=change(tree[k].ls,l,mid,x,y,v);
if(y>mid) tree[k].rs=change(tree[k].rs,mid+1,r,x,y,v);
pup(k);
return k;
}
long long ask(long long k,long long l,long long r,long long x,long long y){
if(x<=l&&y>=r){
return tree[k].sum;
}
long long mid=(l+r)>>1,res=0;
pud(k,l,r,mid);
if(x<=mid) res=ask(tree[k].ls,l,mid,x,y);
if(y>mid) res+=ask(tree[k].rs,mid+1,r,x,y);
return res;
}
long long read(){
char ch=getchar();long long x=0,f=1;
while(ch<'0'||ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch<='9'&&ch>='0'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
int main(){
n=read();q=read();
for(long long i=1;i<=n;i++) a[i]=read();
build(rt[0],1,n);
long long t=0;
while(q--){
char op;cin>>op;
long long x=read();
if(op=='C'){
long long y=read(),z=read();
rt[t+1]=change(rt[t],1,n,x,y,z);
++t;
}else if(op=='Q'){
long long y=read();
printf("%lld\n",ask(rt[t],1,n,x,y));
}else if(op=='H'){
long long y=read(),z=read();
printf("%lld\n",ask(rt[z],1,n,x,y));
}else{
t=x;
}
}
return 0;
}
@2经典应用
最基本的就是版本问题,即:修改但不抛弃,即我不仅会用到修改后的,修改前的我也会用到。
另一种就是可以把可持久化值域线段树当成前缀和处理。
即:除了版本问题,还有就是特殊的区间问题了,通常与“一个数在某个区间出现了多少次”有关(比如区间第
3.树状数组
略
4.分块
易错点
一定要考虑左右端点在同一个块内的情况,要特殊处理!!!
正文
分块就是将暴力做法放到一个块里面处理。询问时,两边的散块可以暴力处理,中间的整块可以非常快速地得到答案,从而将时间复杂度降到
经验而言,块内对问题的答案是一定要维护的。
经典套路:
(1).带单点修改;找一段区间有多少数大于
新创建一个数组
询问:对散块直接暴力处理,整块用
修改:单点直接单个元素冒泡排序。如果是区间修改,尽量不要使用
总复杂度
(2).求区间众数(具体到是哪一个数)(不带修改)
维护
5.莫队
根据排序对暴力进行优化的离线算法。
(1).经典莫队
按左端点所在块排序,相同则
(2).带修莫队
带修改时,多出一个变量:修改的时间。变成
(3).回滚莫队
当增加操作很好处理,但删除操作异常困难的时候,就可以上回滚莫队。
因为右端点单调递增,所以只有增加操作,要删除的只有左端点。那么可以考虑让左端点也只有增加操作,那就是从左端点所在块的右端点
(4).莫队二次离线
适用于:当区间(当前为
含义:答案的计算需要多个量(这里简化为两个:
模板:https://www.luogu.com.cn/problem/P4887
做法(高度概括):先求出
模板2:https://www.luogu.com.cn/problem/P5047
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,pos[100010],e,headsuf[100010],headpre[100010],block,a[100010];
int b[100010],t,bg[100010],ed[100010];
long long ans[100010],add[100010],addblock[100010],tree[100010],pre[100010],suf[100010];
struct o{
int l,r,id;
bool operator <(const o d) const{
if(pos[l]!=pos[d.l]) return pos[l]<pos[d.l];
if(pos[l]&1) return r<d.r;
return r>d.r;
}
}q[100010];
struct oo{
int id,l,r,ne;
long long op;
}qsuf[200010],qpre[200010];
int lowbit(int x){return x&(-x);}
void change(int x,int v){
for(int i=x;i<=n;i+=lowbit(i)){
tree[i]+=v;
}
}
int ask(int x){
int res=0;
for(int i=x;i>0;i-=lowbit(i)){
res+=tree[i];
}
return res;
}
void ddsuf(int id,int x,int l,int r,long long op){
qsuf[++e].ne=headsuf[x];
qsuf[e].id=id;
qsuf[e].l=l;
qsuf[e].r=r;
qsuf[e].op=op;
headsuf[x]=e;
}
void ddpre(int id,int x,int l,int r,long long op){
qpre[++e].ne=headpre[x];
qpre[e].id=id;
qpre[e].l=l;
qpre[e].r=r;
qpre[e].op=op;
headpre[x]=e;
}
int read(){
char ch=getchar();int x=0,f=1;
while(ch<'0'||ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch<='9'&&ch>='0'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
int main(){
// freopen("ce.in","r",stdin);
// freopen("ce.out","w",stdout);
n=read();m=read();
block=sqrt(n);
for(int i=1;i<=n;i++) a[i]=read(),b[i]=a[i],pos[i]=(i-1)/block+1;
for(int i=1;i<=m;i++){
q[i].id=i;
q[i].l=read();
q[i].r=read();
}
t=n/block;
if(n%block) t++;
for(int i=1;i<=t;i++){
bg[i]=(i-1)*block+1;
ed[i]=i*block;
}
ed[t]=n;
sort(q+1,q+1+m);
int cnt=unique(b+1,b+1+n)-b-1;
sort(b+1,b+1+cnt);
for(int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+1+cnt,a[i])-b;
}
for(int i=1;i<=n;i++){
pre[i]=pre[i-1]+ask(n)-ask(a[i]);
change(a[i],1);
}
for(int i=1;i<=n;i++) tree[i]=0;
for(int i=n;i>0;i--){
suf[i]=suf[i+1]+ask(a[i]-1);
change(a[i],1);
}
int l=1,r=0;
for(int i=1;i<=m;i++){
if(q[i].l<l){
ddsuf(q[i].id,r+1,q[i].l,l-1,-1);
ans[q[i].id]+=suf[q[i].l]-suf[l];
}
if(q[i].l>l){
ddsuf(q[i].id,r+1,l,q[i].l-1,1);
ans[q[i].id]-=suf[l]-suf[q[i].l];
}
l=q[i].l;
if(q[i].r>r){
ddpre(q[i].id,l-1,r+1,q[i].r,-1);
ans[q[i].id]+=pre[q[i].r]-pre[r];
}
if(q[i].r<r){
ddpre(q[i].id,l-1,q[i].r+1,r,1);
ans[q[i].id]-=pre[r]-pre[q[i].r];
}
r=q[i].r;
}
for(int x=0;x<=n;x++){
if(x>0){
for(int i=bg[pos[a[x]]];i<a[x];i++){
add[i]++;
}
for(int i=1;i<pos[a[x]];i++){
addblock[i]++;
}
}
for(int i=headpre[x];i;i=qpre[i].ne){
for(int j=qpre[i].l;j<=qpre[i].r;j++){
ans[qpre[i].id]+=qpre[i].op*(add[a[j]]+addblock[pos[a[j]]]);
}
}
}
for(int i=1;i<=t;i++) addblock[i]=0;
for(int i=1;i<=n;i++) add[i]=0;
for(int x=n+1;x>0;x--){
if(x<=n){
for(int i=a[x]+1;i<=ed[pos[a[x]]];i++){
add[i]++;
}
for(int i=pos[a[x]]+1;i<=t;i++){
addblock[i]++;
}
}
for(int i=headsuf[x];i;i=qsuf[i].ne){
for(int j=qsuf[i].l;j<=qsuf[i].r;j++){
ans[qsuf[i].id]+=qsuf[i].op*(add[a[j]]+addblock[pos[a[j]]]);
}
}
}
for(int i=1;i<=m;i++){
ans[q[i].id]+=ans[q[i-1].id];
}
for(int i=1;i<=m;i++){
printf("%lld\n",ans[i]);
}
return 0;
}
声明(一定要看)
~虽然我的文章应该没人看但还是要~说明:这篇文章的初衷是自用的,主要写给自己看以总结提高、方便复习的,对内容的正确性不保证(当然大部分应该是对的),有错误可以指出
原始链接:a