数据结构
早就学过了,学了就只学了
不能再颓下去了
应该要持续更新
莫队
一般用来解决静态区间询问问题,如果 的答案能扩展到 ,那么我们就能使用莫队在 复杂度内完成所有询问的答案,这里设 同阶。这个根号怪死了,还有我删除线咋还没了/fn
思想很简单,将询问离线后排序,暴力从上一个区间的答案转移到下一个区间答案(一步一步移动即可)。
排序一般按 所在块的编号为第一关键字, 为第二关键字,一般也会加玄学的奇偶性排序。还有一般的块长都设 ,不同的莫队理论最优的块长也不同,至于为什么分块不想写了。
一般核心在于这里:
ll l=1,r=0,res=0; //res是答案
for(int i=1;i<=m;++i){
阿巴阿巴;
while(l>q[i].l) add(--l);
while(l<q[i].l) del(l++);
while(r>q[i].r) del(r--);
while(r<q[i].r) add(++r);
ans[q[i].id]=res;
}
做几道题就懂了。
回滚莫队
因为要做题就先写这个了。
OI-wiki上的描述:有些题目在区间转移时,可能会出现增加或者删除无法实现的问题。在只有增加不可实现或者只有删除不可实现的时候,就可以使用回滚莫队在 的时间内解决问题。回滚莫队的核心思想就是:既然只能实现一个操作,那么就只使用一个操作,剩下的交给回滚解决。回滚莫队分为只增和只减,具体看题目。
例题:歴史の研究
我们发现增加一个元素很好解决,它若要对答案产生影响就一定是新的最大值,但删除元素时我们不好判断剩下区间中的最大重要值是啥,这个时候就是只增加莫队了。
操作过程如下(不想写直接粘了):
-
对原序列进行分块,对询问按以左端点所属块编号升序为第一关键字,右端点升序为第二关键字的方式排序。
-
按顺序处理询问:
-
如果询问左端点所属块 和上一个询问左端点所属块的不同,那么将莫队区间的左端点初始化为 的右端点加 , 将莫队区间的右端点初始化为 的右端点;
-
如果询问的左右端点所属的块相同,那么直接扫描区间回答询问;
-
如果询问的左右端点所属的块不同:
-
如果询问的右端点大于莫队区间的右端点,那么不断扩展右端点直至莫队区间的右端点等于询问的右端点;
-
不断扩展莫队区间的左端点直至莫队区间的左端点等于询问的左端点;
-
回答询问;
-
撤销莫队区间左端点的改动,使莫队区间的左端点回滚到 的右端点加 。
-
-
复杂度不想证,块长最优取 ,当然有时候更玄学的更好,但稳妥起见还是用这个,正式赛应该不怎么卡常吧。
还是把代码写了才能理解,多做几道题就上手了。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=114514,M=1919810,mod=1e9+7;
ll n,m,a[N],ans[N],res;
ll ns,nq,st[N],ed[N],bel[N];
ll b[N],v[N],cnt[N];
struct xx{
ll l,r,id;
}q[N];
bool cmp(xx x,xx y){
return bel[x.l]==bel[y.l]?x.r<y.r:bel[x.l]<bel[y.l];
}
ll tot[N];
ll calc(ll l,ll r){
ll ans=0;
for(int i=l;i<=r;++i) tot[b[i]]=0; //服了
for(int i=l;i<=r;++i) tot[b[i]]++,ans=max(ans,tot[b[i]]*a[i]);
return ans;
}
void add(ll x){
++cnt[b[x]];
res=max(res,cnt[b[x]]*a[x]);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>m; ns=sqrt(n),nq=ceil(n*1.0/ns);
for(int i=1;i<=n;++i) cin>>a[i],b[i]=v[i]=a[i];
sort(v+1,v+n+1);
ll nm=unique(v+1,v+n+1)-v-1;
for(int i=1;i<=n;++i) b[i]=lower_bound(v+1,v+nm+1,b[i])-v;
for(int i=1;i<=n;++i){
st[i]=ns*(i-1)+1,ed[i]=min(ns*i,n);
for(int j=st[i];j<=ed[i];++j) bel[j]=i;
}
for(int i=1;i<=m;++i) cin>>q[i].l>>q[i].r,q[i].id=i;
sort(q+1,q+m+1,cmp);
ll l=1,r=0; res=0;
for(int i=1,id=1;id<=nq;++id){
l=ed[id]+1,r=ed[id],res=0;
for(int i=1;i<=n;++i) cnt[i]=0;
while(bel[q[i].l]==id){
if(bel[q[i].l]==bel[q[i].r]){
ans[q[i].id]=calc(q[i].l,q[i].r);
++i;
continue; //同块内暴力
}
while(r<q[i].r) add(++r);
ll tmp=res;
while(l>q[i].l) add(--l);
ans[q[i].id]=res;
while(l<=ed[id]) --cnt[b[l]],++l;
res=tmp; //回滚
++i;
}
}
for(int i=1;i<=m;++i) cout<<ans[i]<<'\n';
return 0;
}
静态区间 mex
发现删除元素好做增加元素难搞,那么就用只减莫队。注意排序是按右端点从大到小排,并且区间初始有答案,注意写法。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*114514,M=1919810;
ll n,m,a[N],ans[N],res;
ll ns,nq,st[N],ed[N],bel[N];
ll cnt[N];
struct xx{
ll l,r,id;
}q[N];
bool cmp(xx x,xx y){
return bel[x.l]^bel[y.l]?bel[x.l]<bel[y.l]:x.r>y.r;//只删
}
bool f[N];
ll calc(ll l,ll r){
ll mex=0;
for(int i=l;i<=r;++i){
f[a[i]]=1;
while(f[mex]) ++mex;
}
for(int i=l;i<=r;++i) f[a[i]]=0;
return mex;
}
void del(ll x){
--cnt[a[x]];
if(!cnt[a[x]]) res=min(res,a[x]);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>m; ns=n/sqrt(m),nq=ceil(n*1.0/ns);
for(int i=1;i<=n;++i) cin>>a[i],++cnt[a[i]];
for(int i=1;i<=m;++i) cin>>q[i].l>>q[i].r,q[i].id=i;
for(int i=1;i<=nq;++i){
st[i]=ns*(i-1)+1,ed[i]=min(ns*i,n);
for(int j=st[i];j<=ed[i];++j) bel[j]=i;
}
sort(q+1,q+m+1,cmp);
ll l,r,las=0;
while(cnt[las]) ++las;
for(int i=1,id=1;id<=nq;++id){
l=st[id],r=n,res=las;
while(bel[q[i].l]==id){
if(bel[q[i].l]==bel[q[i].r]){
ans[q[i].id]=calc(q[i].l,q[i].r);
++i; continue;
}
while(r>q[i].r) del(r--);
ll tmp=res;
while(l<q[i].l) del(l++);
ans[q[i].id]=res;
while(l>st[id]){
--l;
++cnt[a[l]];
}
res=tmp; ++i;
}
while(r<n){
++r;
++cnt[a[r]];
}
while(l<st[id+1]){
--cnt[a[l]];
if(!cnt[a[l]]) las=min(las,a[l]);
++l;
}
}
for(int i=1;i<=m;++i) cout<<ans[i]<<'\n';
return 0;
}
【模板】回滚莫队&不删除莫队
洛谷上的模板是求区间 中相同元素的最远距离,这里我们记录每个数最左边的位置和最右边的位置记为 ,然后在加点的过程中不断更新最远位置,注意每次整完一个块内的询问后我们要把 清零否则会对后面的询问产生影响,但是直接把整个区间扫一遍清零显然会爆,于是我们开一个桶,记录修改了的位置然后清零,就不会爆了。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2*1145140,M=1919810,mod=1e9+7;
ll n,m,a[N],b[N],ans[N],res;
ll ns,nq,st[N],ed[N],bel[N];
ll las[N],nex[N],bu[N];//那个做法不显然假了吗/fn
//不能记录旁边的第一个,记录区间内最远的
struct xx{
ll l,r,id;
}q[N];
bool cmp(xx x,xx y){
return bel[x.l]==bel[y.l]?x.r<y.r:bel[x.l]<bel[y.l];
}
ll ls[N];
ll calc(ll l,ll r){
ll ans=0;
for(int i=l;i<=r;++i) ls[a[i]]=0; //又是这里
for(int i=l;i<=r;++i)
if(!ls[a[i]]) ls[a[i]]=i;
else ans=max(ans,i-ls[a[i]]);
return ans;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n; ns=sqrt(n),nq=ceil(n*1.0/ns);
for(int i=1;i<=n;++i) cin>>a[i],b[i]=a[i];
sort(b+1,b+n+1);
ll nm=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;++i) a[i]=lower_bound(b+1,b+nm+1,a[i])-b;
for(int i=1;i<=n;++i){
st[i]=ns*(i-1)+1,ed[i]=min(ns*i,n);
for(int j=st[i];j<=ed[i];++j) bel[j]=i;
}
cin>>m;
for(int i=1;i<=m;++i) cin>>q[i].l>>q[i].r,q[i].id=i;
sort(q+1,q+m+1,cmp);
ll l,r,cnt; res=0;
for(int i=1,id=1;id<=nq;++id){
l=ed[id]+1,r=ed[id],res=cnt=0;
while(bel[q[i].l]==id){
if(bel[q[i].l]==bel[q[i].r]){
ans[q[i].id]=calc(q[i].l,q[i].r);
++i; continue;
}
while(r<q[i].r){
++r;
nex[a[r]]=r,bu[++cnt]=a[r];
if(!las[a[r]]) las[a[r]]=r;
res=max(res,r-las[a[r]]);
}
ll tmp=res;
while(l>q[i].l){
--l;
if(!nex[a[l]]) nex[a[l]]=l;
else res=max(res,nex[a[l]]-l);
}
ans[q[i].id]=res;
while(l<=ed[id]){
if(nex[a[l]]==l) nex[a[l]]=0;
++l;
}
res=tmp; ++i;
}
for(int j=1;j<=cnt;++j) las[bu[j]]=nex[bu[j]]=0;
}
for(int i=1;i<=m;++i) cout<<ans[i]<<'\n';
return 0;
}
ZQUERY - Zero Query
一个很智障的题。你把原数列前缀和一下就把问题转化为在前缀和数组上区间 中相同元素的最远距离,就直接是板子了。注意有些细节,我们在向左扩展左端点时注意是要扩展到 q[i].l-1
,并且同块暴力求的时候也是从 开始,还要注意前缀和珂能是负数就没法当下标,你就把 赋成 就行了。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1145140,M=1919810;
ll n,m,a[N],ans[N],res;
ll ns,nq,st[N],ed[N],bel[N];
ll las[N],nex[N],bu[N];
struct xx{
ll l,r,id; //只增
}q[N];
bool cmp(xx x,xx y){
return bel[x.l]^bel[y.l]?bel[x.l]<bel[y.l]:x.r<y.r;
}
ll ls[N];
ll calc(ll l,ll r){
ll ans=0;
for(int i=l-1;i<=r;++i)
if(!ls[a[i]]) ls[a[i]]=i;
else ans=max(ans,i-ls[a[i]]);
for(int i=l-1;i<=r;++i) ls[a[i]]=0;
return ans;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>m; ns=n/sqrt(m),nq=ceil(n*1.0/ns);
a[0]=5e4+5; //会有负数啊
for(int i=1;i<=n;++i) cin>>a[i],a[i]+=a[i-1];
for(int i=1;i<=m;++i) cin>>q[i].l>>q[i].r,q[i].id=i;
for(int i=1;i<=nq;++i){
st[i]=ns*(i-1)+1,ed[i]=min(ns*i,n);
for(int j=st[i];j<=ed[i];++j) bel[j]=i;
}
sort(q+1,q+m+1,cmp);
ll l,r,cnt;
for(int i=1,id=1;id<=nq;++id){
l=ed[id]+1,r=ed[id],res=0,cnt=0;
while(bel[q[i].l]==id){
if(bel[q[i].l]==bel[q[i].r]){
ans[q[i].id]=calc(q[i].l,q[i].r);
++i; continue;
}
while(r<q[i].r){
++r;
nex[a[r]]=r,bu[++cnt]=a[r];
if(!las[a[r]]) las[a[r]]=r;
res=max(res,r-las[a[r]]);
}
ll tmp=res;
while(l>=q[i].l){ //a[r]-a[l-1]是这个道理
--l;
if(!nex[a[l]]) nex[a[l]]=l;
else res=max(res,nex[a[l]]-l);
}
ans[q[i].id]=res;
while(l<=ed[id]){
if(nex[a[l]]==l) nex[a[l]]=0;
++l;
}
res=tmp; ++i;
}
for(int j=1;j<=cnt;++j) las[bu[j]]=nex[bu[j]]=0;
}
for(int i=1;i<=m;++i) cout<<ans[i]<<'\n';
return 0;
}
结合值域分块
其实我连值域分块的详细应用技巧都还没搞明白,但是他就那个很好懂的意思,就是对值域分个块,有时候能共用序列分块的数组。反正应用挺多的,就慢慢做题吧。
P3730 曼哈顿交易
题意:求静态区间中数的第 小出现次数。考虑莫队套值域分块,记录每个数的出现次数,每种出现次数的个数,还有对出现次数值域分块后每个块的总个数。这样查询的时候就是个惯用套路了:先枚举块并不断去减 ,如果 则答案就在这个块中,然后就枚举这个值域块就行了。
不过为什么我另开数组搞值域分块错完了?
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll int
const ll N=114514,M=1919810;
ll n,m,a[N],b[N],ans[N];
ll ns,nq,bel[N],st[N],ed[N];
ll cnt1[N],cnt2[N],val[N];
struct que{
ll l,r,k,id;
bool operator <(const que &lxl)const{
return bel[l]^bel[lxl.l]?bel[l]<bel[lxl.l]:r<lxl.r;
}
}q[N];
void add(ll x){
--cnt2[cnt1[x]],--val[bel[cnt1[x]]];
++cnt1[x];
++cnt2[cnt1[x]],++val[bel[cnt1[x]]];
}
void del(ll x){
--cnt2[cnt1[x]],--val[bel[cnt1[x]]];
--cnt1[x];
++cnt2[cnt1[x]],++val[bel[cnt1[x]]];
}
ll query(ll k){
ll pos=0;
for(int i=1;i<=nq;++i){
if(k-val[i]<=0){
pos=i;
break;
}
k-=val[i];
}
if(pos==0) return -1;
for(int i=st[pos];i<=ed[pos];++i){
if(k-cnt2[i]<=0) return i;
k-=cnt2[i];
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>m; ns=n/sqrt(m),nq=ceil(n*1.0/ns);
for(int i=1;i<=n;++i) cin>>a[i],b[i]=a[i];
sort(b+1,b+n+1);
ll nm=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;++i) a[i]=lower_bound(b+1,b+nm+1,a[i])-b;
for(int i=1;i<=nq;++i){
st[i]=ns*(i-1)+1,ed[i]=min(ns*i,n);
for(int j=st[i];j<=ed[i];++j) bel[j]=i;
}
for(int i=1;i<=m;++i) cin>>q[i].l>>q[i].r>>q[i].k,q[i].id=i;
sort(q+1,q+m+1);
ll l=q[1].l,r=l-1;
for(int i=1;i<=m;++i){
while(r<q[i].r) add(a[++r]);
while(l>q[i].l) add(a[--l]);
while(r>q[i].r) del(a[r--]);
while(l<q[i].l) del(a[l++]);
ans[q[i].id]=query(q[i].k);
}
for(int i=1;i<=m;++i) cout<<ans[i]<<'\n';
return 0;
}
P4396 [AHOI2013] 作业
题意:每次询问给定 ,求数列上的区间 中值域在 中的 数的个数 和 不同的数的个数。这个就是明显要值域分块了,我们开三个数组: 记每个数的个数,用于散块; 记每个值域块中的数的个数,用于整块; 记每个值域块中不同的数的个数,用于整块。简单维护这三个数组就能直接查询了。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll int
const ll N=114514,M=1919810;
ll n,m,a[N],ns,nq,bel[N],st[N],ed[N];
ll num[N],cnt[N],tot[N];
ll out1[N],out2[N],ans1,ans2;/*
cnt记每个数的个数,用于散块
num记每个值域块中的数的个数,用于整块
tot记每个值域块中不同的数的个数,用于整块*/
struct xx{
ll l,r,a,b,id;
}q[N],out[N];
bool cmp(xx x,xx y){
return bel[x.l]^bel[y.l]?bel[x.l]<bel[y.l]:((bel[x.l]&1)?x.r<y.r:x.r>y.r);
}
void add(ll x){
++num[bel[a[x]]],++cnt[a[x]];
if(cnt[a[x]]==1) ++tot[bel[a[x]]];
}
void del(ll x){
--num[bel[a[x]]],--cnt[a[x]];
if(!cnt[a[x]]) --tot[bel[a[x]]];
}
void calc(ll A,ll B){
ans1=ans2=0;
if(bel[A]==bel[B]){
for(int i=A;i<=B;++i)
ans1+=cnt[i],ans2+=(cnt[i]>0);
return;
}
for(int i=A;i<=ed[bel[A]];++i) ans1+=cnt[i],ans2+=(cnt[i]>0);
for(int i=st[bel[B]];i<=B;++i) ans1+=cnt[i],ans2+=(cnt[i]>0);
for(int i=bel[A]+1;i<bel[B];++i) ans1+=num[i],ans2+=tot[i];
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>m; ns=max(n/sqrt(m),1.0),nq=ceil(n*1.0/ns); //啊咧?
for(int i=1;i<=nq;++i){
st[i]=ns*(i-1)+1,ed[i]=min(ns*i,n);
for(int j=st[i];j<=ed[i];++j)
cin>>a[j],bel[j]=i;
}
for(int i=1;i<=m;++i) cin>>q[i].l>>q[i].r>>q[i].a>>q[i].b,q[i].id=i;
sort(q+1,q+m+1,cmp);
ll l=1,r=0;
for(int i=1;i<=m;++i){
while(l>q[i].l) add(--l);
while(l<q[i].l) del(l++);
while(r>q[i].r) del(r--);
while(r<q[i].r) add(++r);
calc(q[i].a,q[i].b);
out1[q[i].id]=ans1;
out2[q[i].id]=ans2;
}
for(int i=1;i<=m;++i) cout<<out1[i]<<" "<<out2[i]<<'\n';
return 0;
}
链剖分
干脆就放这里了,而且也经常和数据结构一起用。
有一般用的重链剖分,有时有奇效的长链剖分,还有LCT的实链剖分。
重链剖分
一些定义:
-
的重儿子是他的儿子中子数最大的那个节点,轻儿子是剩下的点。
-
到重儿子的边叫重边,其余为轻边。
-
重边组成的链叫重链。
把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。
实现
第一个 dfs 记录每个结点的父节点、深度、子树大小、重子节点。
第二个 dfs 记录所在链的链顶、重边优先遍历时的 dfs 序、DFS 序对应的节点编号。
然后就珂以在dfs序上用数据结构维护了。
为什么能使用重链剖分来剖?因为一些性质:
-
所有重链将树完全剖分。
-
经过一条轻边时,所在子树大小会至少除以二,所以对于任意一条路径,把它拆分成从 LCA 分别向两边往下走最多走 次。
这样假设我们用线段树维护信息,那么时间复杂度就是 。
自己看看模板怎么写吧。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ls now<<1
#define rs now<<1|1
const ll N=114514,M=1919810;
struct edge{
ll next,to;
}e[2*N];
ll head[2*N],cnt;
void add(ll x,ll y){
e[++cnt].next=head[x];
e[cnt].to=y;
head[x]=cnt;
}
ll n,m,rt,p,a[N];
ll siz[N],f[N],dept[N],son[N];
ll dfn[N],top[N],rk[N],t_cnt;
void dfs1(ll u,ll fa){
siz[u]=1;
for(int i=head[u];i;i=e[i].next){
ll v=e[i].to;
if(v==fa) continue;
f[v]=u;
dept[v]=dept[u]+1;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
}
void dfs2(ll u,ll fa,ll tp){
top[u]=tp,rk[++t_cnt]=u;
dfn[u]=t_cnt;
if(!son[u]) return;
dfs2(son[u],u,tp);
for(int i=head[u];i;i=e[i].next){
ll v=e[i].to;
if(v==fa||v==son[u]) continue;
dfs2(v,u,v);
}
}
struct tree{
ll l,r,len;
ll sum,tag;
}t[4*N];
void pushup(ll now){
t[now].sum=(t[ls].sum+t[rs].sum)%p;
}
void pushdown(ll now){
ll k=t[now].tag;
if(!k) return;
t[ls].sum=(t[ls].sum+k*t[ls].len)%p;
t[rs].sum=(t[rs].sum+k*t[rs].len)%p;
t[ls].tag=(t[ls].tag+k)%p,t[rs].tag=(t[rs].tag+k)%p;
t[now].tag=0;
}
void build(ll now,ll l,ll r){
t[now].l=l,t[now].r=r;
t[now].len=r-l+1;
if(l==r){
t[now].sum=a[rk[l]];
return;
}
ll mid=l+r>>1;
build(ls,l,mid);
build(rs,mid+1,r);
pushup(now);
}
void update(ll now,ll x,ll y,ll k){
if(t[now].l>=x&&t[now].r<=y){
t[now].sum=(t[now].sum+k*t[now].len)%p;
t[now].tag=(t[now].tag+k)%p;
return;
}
pushdown(now);
ll mid=t[now].l+t[now].r>>1;
if(x<=mid) update(ls,x,y,k);
if(y>mid) update(rs,x,y,k);
pushup(now);
}
ll query(ll now,ll x,ll y){
if(t[now].l>=x&&t[now].r<=y) return t[now].sum;
pushdown(now);
ll mid=t[now].l+t[now].r>>1,ans=0;
if(x<=mid) ans=(ans+query(ls,x,y))%p;
if(y>mid) ans=(ans+query(rs,x,y))%p;
return ans;
}
void update1(ll x,ll y,ll k){
while(top[x]!=top[y]){
if(dept[top[x]]<=dept[top[y]]) swap(x,y);
update(1,dfn[top[x]],dfn[x],k);
x=f[top[x]];
}
if(dept[x]>dept[y]) swap(x,y);
update(1,dfn[x],dfn[y],k);
}
void update2(ll x,ll k){
update(1,dfn[x],dfn[x]+siz[x]-1,k);
}
ll query1(ll x,ll y){
ll ans=0;
while(top[x]!=top[y]){
if(dept[top[x]]<=dept[top[y]]) swap(x,y);
ans=(ans+query(1,dfn[top[x]],dfn[x]))%p;
x=f[top[x]];
}
if(dept[x]>dept[y]) swap(x,y);
return (ans+query(1,dfn[x],dfn[y]))%p;
}
ll query2(ll x){
return query(1,dfn[x],dfn[x]+siz[x]-1)%p;
}
int main(){
//ios::sync_with_stdio(0);
//cin.tie(0); cout.tie(0);
cin>>n>>m>>rt>>p;
for(int i=1;i<=n;++i) cin>>a[i];
for(int i=1;i<n;++i){
ll a,b;
cin>>a>>b;
add(a,b),add(b,a);
}
dfs1(rt,0),dfs2(rt,0,rt);
build(1,1,n);
for(int i=1;i<=m;++i){
ll opt,x,y,z;
cin>>opt;
if(opt==1){
cin>>x>>y>>z; z%=p;
update1(x,y,z);
}
if(opt==2){
cin>>x>>y;
cout<<query1(x,y)<<'\n';
}
if(opt==3){
cin>>x>>z; z%=p;
update2(x,z);
}
if(opt==4){
cin>>x;
cout<<query2(x)<<'\n';
}
}
return 0;
}
长链剖分
这个好像就优化dp有点用,鸽了
分块
能用来做普及的题和NOI+的题。
平衡复杂度
这个之前不会。我们一般都是 的区间修改和查询,但有些时候操作数量不均匀,如果操作不是很变态就可以考虑升高部分操作复杂度而使其他操作复杂度下降。
-
区间修改, 区间求和
考虑维护块内前缀和以及每个块之间的前缀和,查询的时候直接整+散就行了,修改时暴力 分别重新计算块内前缀和以及块之间的前缀和。
-
区间加, 区间求和
差分,转化成 单点修改,区间求和变成两个前缀求和,那么就只需要 求前缀 的值,维护 和 即可。感觉还是有点抽象,不如说就是上面做法的前缀和变成了差分。
然后我们引入一个利器:值域分块。当值域大小为 时,可以用类似权值线段树的形式维护 “权值 叉树”。
-
插入一个数, 查询第 小
值域分块,维护整块里有多少个数,询问的时候直接从小到大遍历整块直到不够的时候再查散块。
-
插入一个数, 查询第 小
有点毒瘤,放张图片得了。
结合值域并查集
当值域不大并且有类似于“对区间内的某一类不连续的数进行修改和查询”这样的操作,珂以考虑分块结合值域并查集,下面是一道例题:
P8360 [SNOI2022] 军队
看到鬼畜操作考虑分块。先考虑如果没有修改颜色的操作怎么做:很简单,整块维护加法 和块中每个 的个数,修改时散块暴力加整块加 就行了。
然后考虑怎么解决改颜色的问题,这里引入一个 trick:值域并查集,在最初分块和第二分块中都有用到。简单来说就是用并查集维护值域不大的信息。思路比较容易:我们初始记录下每个并查集里的元素个数 、父亲 、加法标记 和这个并查集的颜色 ,我用了个结构体存。先搞 find 怎么写,我们考虑记录跳到的点的编号 和当前点的 ,注意我们还是要路径压缩。我们顺便记录一个 表示每个整块的权值和。
接下来考虑如何构建这个并查集:我们记录 表示对于原序列中的 它所属的并查集的编号,再记录 表示在第 块中的所有颜色为 的元素所在的并查集编号。构建方式如下:
点击查看代码
for(int i=1;i<=nq;++i){
st[i]=ns*(i-1)+1,ed[i]=min(ns*i,n); //顺便把块分了,少点常数
for(int j=st[i];j<=ed[i];++j){
bel[j]=i,sum[i]+=a[j];
if(!rt[i][c[j]]){
rt[i][c[j]]=f[j]=++tot; //tot是新增并查集的编号
t[tot].fa=t[tot].tag=0;
t[tot].siz=1,t[tot].col=c[j];
}
else f[j]=rt[i][c[j]],++t[f[j]].siz;
}
}
然后考虑修改颜色的操作。我们先考虑如何修改散块:我们直接遍历散块,直接对每一个元素进行 find,设得到的点编号为 ,其加法标记为 ,如果并查集的 点颜色不为 就直接跳过。否则就将 的 减一,如果减一后 那么要清零: rt[bel[i]][t[p].col]=0
。然后 加上所得权值,而且注意,如果修改成的颜色 初始不存在,那么要在并查集里新给它开一个点,不能直接放在原来的 的点上,同时如果 在并查集里那么 要减去 t[f[i]].tag
,还有改大小这些比较显然的操作。
考虑对整块修改颜色,其实很简单,我们先取出两个颜色在并查集中的点,令 p1=rt[i][x],p2=rt[i][y];
,如果 为 那么跳过,有就合并到 上,还有如果 那么就直接改一下 的颜色并且 swap 一下两个 就行了,具体来说如下:
点击查看代码
if(p2){
t[p2].siz+=t[p1].siz;
t[p1].fa=p2;
t[p1].tag-=t[p2].tag;
rt[i][x]=0;//这个要删了
}
else{
t[p1].col=y;
swap(rt[i][x],rt[i][y]);
}
然后其他操作就比较简单了。区间加的话就散块 find 一下,如果得到的点的颜色是 那么 和 都加上 ,整块就直接把 提出来然后对并查集加 ,对 也加一下就行了。
查询的话也简单,散块每个点 find 一下,然后 ans+=a[i]+u.val+t[u.id].tag;
,注意 t[u.id].tag
是不包含在 u.val
中的,整块直接加维护的 就行了。
然后就做完了,也不需要卡常,不需要逐块处理,细节也还好,比某些煞笔卡常题美好多了。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define in inline
#define ll long long
const ll N=250005,M=1919810;
int n,m,C,tot;
int ns,nq,st[N],ed[N],bel[N];
ll a[N],sum[N];
int c[N],f[N],siz[N],rt[550][N]; //f单个点指向的根,rt每块每颜色的根
struct xx{
int fa,siz,col;
ll tag;
}t[M];
struct gx{
int id;
ll val;
};
in gx find(int x){
if(!t[x].fa) return (gx){x,0ll};
gx y=find(t[x].fa);
y.val+=t[x].tag;
t[x].fa=y.id,t[x].tag=y.val;
return y;
}
in void calc(int l,int r,int x,int y,int id){ //暴力修改散块颜色
for(int i=l;i<=r;++i){
gx u=find(f[i]);
int p=u.id; ll val=u.val;
if(t[p].col!=x) continue;
if(!(--t[p].siz)) rt[id][t[p].col]=0;
a[i]+=val+t[p].tag;
if(!rt[id][y]){
rt[id][y]=f[i]=++tot;
t[tot].fa=t[tot].tag=0;
t[tot].siz=1,t[tot].col=y;
}
else{
f[i]=rt[id][y];
a[i]-=t[f[i]].tag;
++t[f[i]].siz;
}
}
}
in void update1(int l,int r,int x,int y){
if(x==y) return;
if(bel[l]==bel[r]){
calc(l,r,x,y,bel[l]);
return;
}
calc(l,ed[bel[l]],x,y,bel[l]);
calc(st[bel[r]],r,x,y,bel[r]);
for(int i=bel[l]+1;i<bel[r];++i){
ll p1=rt[i][x],p2=rt[i][y];
if(!p1) continue;
if(p2){
t[p2].siz+=t[p1].siz;
t[p1].fa=p2;
t[p1].tag-=t[p2].tag;
rt[i][x]=0;//删了
}
else{
t[p1].col=y;
swap(rt[i][x],rt[i][y]);
}
}
}
in void update2(int l,int r,int x,ll k){
if(bel[l]==bel[r]){
for(int i=l;i<=r;++i){
gx u=find(f[i]);
if(t[u.id].col==x) a[i]+=k,sum[bel[l]]+=k;
}
return;
}
for(int i=l;i<=ed[bel[l]];++i){
gx u=find(f[i]);
if(t[u.id].col==x) a[i]+=k,sum[bel[l]]+=k;
}
for(int i=st[bel[r]];i<=r;++i){
gx u=find(f[i]);
if(t[u.id].col==x) a[i]+=k,sum[bel[r]]+=k;
}
for(int i=bel[l]+1;i<bel[r];++i){
ll p=rt[i][x];
if(!p) continue;
t[p].tag+=k,sum[i]+=t[p].siz*k;
}
}
in ll query(int l,int r){
ll ans=0;
if(bel[l]==bel[r]){
for(int i=l;i<=r;++i){
gx u=find(f[i]);
ans+=a[i]+u.val+t[u.id].tag;
}
return ans;
}
for(int i=l;i<=ed[bel[l]];++i){
gx u=find(f[i]);
ans+=a[i]+u.val+t[u.id].tag;
}
for(int i=st[bel[r]];i<=r;++i){
gx u=find(f[i]);
ans+=a[i]+u.val+t[u.id].tag;
}
for(int i=bel[l]+1;i<bel[r];++i) ans+=sum[i];
return ans;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>m>>C; ns=sqrt(n),nq=ceil(n*1.0/ns);
for(int i=1;i<=n;++i) cin>>a[i];
for(int i=1;i<=n;++i) cin>>c[i];
for(int i=1;i<=nq;++i){
st[i]=ns*(i-1)+1,ed[i]=min(ns*i,n);
for(int j=st[i];j<=ed[i];++j){
bel[j]=i,sum[i]+=a[j];
if(!rt[i][c[j]]){
rt[i][c[j]]=f[j]=++tot;
t[tot].fa=t[tot].tag=0;
t[tot].siz=1,t[tot].col=c[j];
}
else f[j]=rt[i][c[j]],++t[f[j]].siz;
}
}
//写不来逐块处理/fn
for(int i=1;i<=m;++i){
int opt,l,r,x,y; ll k;
cin>>opt>>l>>r;
if(opt==1){
cin>>x>>y;
update1(l,r,x,y);
}
if(opt==2){
cin>>x>>k;
update2(l,r,x,k);
}
if(opt==3) cout<<query(l,r)<<'\n';
//debug();
}
return 0;
}//甚至不用加快读
题
P5046 [Ynoi2019 模拟赛] Yuno loves sqrt technology I
一个排列,强制在线询问区间逆序对数。
有点毒瘤,待办
写了,自己看这里第一题