当前位置: 首页 > news >正文

[蓝桥杯 2023 省 A] 颜色平衡树:从零开始理解树上莫队 一颗颜色平衡树引发的惨案

十四是一名生物工程的学生,他已经7年没碰过信息学竞赛了,有一天他走在蓝桥上看见了一颗漂亮的颜色平衡树:

​​​​​​​[蓝桥杯 2023 省 A] 颜色平衡树 - 洛谷

                       

        十四想用暴力解决问题,他想枚举每个节点,每个节点代表一棵树,他想只要建立一个桶数组把每次出现的颜色放在桶里,每次遍历完一个节点和它的所有子节点后就统计这个桶数组里的所有非零颜色是否相同,如果相同的话这就是一棵颜色平衡树,如果不相同的话这就不是一棵颜色平衡树。

        但是这引来了时间之神乌露提亚的不满:

        时间之神乌露提亚说她用尽了所有的魔力和生命也只能让这个世界回退一分钟,所以你必须在一秒之内解决这个问题,十四看了看CCF老爷机每秒只能计算10^8次,然而使用暴力的话时间复杂度怎么着也来到了O(n^2)的级别,所以愚蠢的十四只能求助题解,题解说要用树上莫队,但是愚蠢的十四并不会用树上莫队,所以让我们和十四一起愉快地学习树上莫队吧~

树上莫队前置知识:分块,普通莫队,修改莫队,回滚莫队

十四是要成为火影的男人,所以他并不会把前置知识列出来然后让看题解小伙伴们自己去找资料学习这些内容,所以让我们和十四一起愉快地学习分块吧~


分块算法

我们先来看一道题:

https://www.acwing.com/problem/content/description/244/

屏幕前的大佬一眼就看出来了这是一道线断树的板子题,但是愚蠢的十四并不会写线断树,所以十四本能地想用暴力解决问题:

十四每次在修改指令的时候从左到右枚举,把数列中的数字一个一个加上指定的值,然后在询问的时候也从做到右一个一个把数列上的数字加起来得到结果,每次操作预期进行n次运算,要进行m次这样的操作,那么时间复杂度就来到了O(nm),但是N和M的最大值是10^5,如果有10^5个数,和10^5次操作的话,一秒内就有10^5*10^5=10^10次操作,但是可怜的CCF老爷机一秒只能进行10^8次运算,所以十四决定对这个暴力的办法进行一些优化:

十四想要把N个数的序列从头到尾分成长度相等的很多很多部分,每次C操作的时候,如果操作的序列[l,r]能覆盖很多区间的话,就会有以下情况:

我们假设5个数字为一部分(一块),于是11~15、16~20、21~25、25~30将这个序列分成了四块,此时我们想进行这样的操作:C 13 27 3 也就是把从13到27的数字每个加上3

数列下标1112131415161718192021222324252627282930
数值11451419198101145141

 我们用黄色和绿色表示分隔开来的分块,用紫色表示需要修改的区间,那么显然我们看见紫色的区域完整地覆盖住了16~20、21~25这两块,十四想到能不能建立一个块标记数组,这个数组存的是完整的块中的每个数字需要加上的数字,例如这里就要把16~20和21~25这两个完整的块中每个数字都要加上3,于是我们引入块标记数组:

数列下标1112131415161718192021222324252627282930
数值11451419198101145141
11~1516~2021~2526~30
块标记0330

十四发现只要这次对块进行标记,只进行了2次标记就操作了2*5=10个数字,非常节省时间,但是这时候就出现了另外一个问题:完整的块之外的数字怎么进行操作?

我们发现13~15和26~27这两部分还没有进行操作,如果改动11~15和26~30这两个块的块标记的话就会牵连到无辜的11~12和28~30,所以十四决定一个一个地把13、14、15、26、27的数加上3:

数列下标1112131415161718192021222324252627282930
数值11784419198101178141
11~1516~2021~2526~30
块标记0330

这样就完美地解决了C 13 27 3的操作,但是屏幕前的你提出了异议(一斤鸭梨):如果对所有没有被完整覆盖的块里需要修改的元素一个一个地修改的话不是仍然有和暴力一样的复杂度吗?这样的话和直接暴力有什么很大的区别吗?

但是通过观察,我们发现:每次操作的过程中没有被覆盖的块最多两个,也就是说,我们需要一个一个操作的块最多是两个。那么我们假设每块里面有a个元素,这样的话序列中的n个元素就被我们分成了(n/a)块,对于我们每次的操作,我们最多要对(n/a)块进行块标记,并且对没有覆盖的2块内最多有2*a=2a个元素一个一个进行修改,所以对于每次操作,我们有(n/a)+2a次操作要进行,要把这个次数降到最少,我们不妨把a定为\sqrt{n} ,这样我们就发现(n/a)和2a的操作次数在同一个数量级,于是我们得出结论,在这一题的分块中,我们把每块的元素个数定为\sqrt{n}可以使得操作的次数降到最低,那么对m次操作,我们题的时间复杂度也就算出来了:O(m\sqrt{n}),怎么样,是不是比之前的O(nm)要优化了一些呢? 机遇这样的时间复杂度,我们这题的所有数据也能在一秒之内算出来了。

最后剩下一个问题:对于询问的Q l r,我们要怎么操作呢?

类似于修改的操作,我们把每块的和计为sum,在输入的时候对每块进行预处理,在修改的时候,加上块内修改元素的个数乘以修改的值,这样我们就能轻松维护块内所有值的和,对于被询问区间完全覆盖的块,我们直接加上这块的和值,对于没有被完全覆盖的块(同样也是最多两个这样的块),我们直接暴力加起来就好啦~

对于所有在块内的询问和操作区间,我们全部暴力就好啦,反正块内的元素最多\sqrt{n}个,暴力同样不会增加时间复杂度~

下面附上十四的AC代码:

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
long long a[100001],devi[100001];
struct node{long long sum=0;long long tag=0;int l=0,r=0;
}blk[100001];
void add(int u,int v,long long k){int LP=devi[u],RP=devi[v];if(LP==RP){for(int i=u;i<=v;i++)a[i]+=k;blk[LP].sum+=k*(v-u+1);}else{for(int i=u;i<=blk[LP].r;i++)a[i]+=k;blk[LP].sum+=k*(blk[LP].r-u+1);for(int i=blk[RP].l;i<=v;i++)a[i]+=k;blk[RP].sum+=k*(v-blk[RP].l+1);for(int i=LP+1;i<RP;i++){blk[i].tag+=k;blk[i].sum+=k*(blk[i].r-blk[i].l+1);}}return ;
}
long long quest(int u,int v){int LP=devi[u],RP=devi[v];long long ans=0;if(LP==RP){for(int i=u;i<=v;i++)ans+=a[i]+blk[LP].tag;return ans;}else{for(int i=u;i<=blk[LP].r;i++)ans+=a[i]+blk[LP].tag;for(int i=blk[RP].l;i<=v;i++)ans+=a[i]+blk[RP].tag;for(int i=LP+1;i<RP;i++)ans+=blk[i].sum;return ans;}
}
int main(){//freopen("test.in","r",stdin);//freopen("test.out","w",stdout);int i,j,m,n;long long k;scanf("%d%d",&n,&m);int len=sqrt(n);for(i=1;i<=n;i++){scanf("%lld",&a[i]);devi[i]=(i-1)/len+1;blk[devi[i]].sum+=a[i];if(i%len==0) blk[devi[i]].r=i;if((i-1)%len==0) blk[devi[i]].l=i;}char c[2];while(m--){int u,v;scanf("%s",c);scanf("%d%d",&u,&v);if(u>v){ int t=u;u=v;v=t; }if(c[0]=='C'){scanf("%lld",&k);add(u,v,k);}else printf("%lld\n",quest(u,v));}return 0;
}

我们再来看一道分块的题:Anton and Permutation - 洛谷

emmm我们先来看看逆序对是什么:逆序对是对于给定的一段正整数序列,存在序列中ai>aj且i<j的有序对。(百度百科就是好,一下就帮我水了十几个字),那么每次交换两个数a[u],a[v](u<v),这两个数坐标之外的数自然不会被影响,序列中在这两个数中间的数,当且仅当数字满足min(a[u],a[v])<a[q]<max(a[u],[v])且u<q<v的时候,如果a[u]>a[v],a[q]就会和a[u]、a[v]a产生两个新的逆序对,但如果a[u]<a[v],a[q]则与a[u]、a[v]原先产生的逆序对就会消失,我们针对这一点对整个序列分块之后,每块内初始的元素都是从小到大的顺序陈列的,我们每次交换之后只需维护每块内从小到大的顺序,每次块外查询直接暴力(分一个不排序的数组a记前后顺序),块内直接二分查看有多少个数字在(a[u],a[v])范围之内(这个操作用到二分,当然就用有顺序的数组v啦)

下面附上十四的AC码:

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
vector<int> v[501];
int a[200001],devi[200001];
struct node{int l,r;
}blk[501];
long long solve(int l,int r){if(l==r) return 0;int xx=a[l],yy=a[r];int LP=devi[xx],RP=devi[yy],k;long long f=1,ans=0;k=find(v[LP].begin(),v[LP].end(),xx)-v[LP].begin();v[LP][k]=yy; devi[yy]=LP;sort(v[LP].begin(),v[LP].end());k=find(v[RP].begin(),v[RP].end(),yy)-v[RP].begin();v[RP][k]=xx; devi[xx]=RP;sort(v[RP].begin(),v[RP].end());if(xx>yy){f=-1;int t=xx; xx=yy; yy=t;}if(LP==RP){for(int i=l+1;i<r;i++)if(xx<a[i] && a[i]<yy)ans++;}else{for(int i=l+1;i<=blk[LP].r;i++)if(xx<a[i] && a[i]<yy)ans++;for(int i=blk[RP].l;i<r;i++)if(xx<a[i] && a[i]<yy)ans++;for(int i=LP+1;i<RP;i++)ans+=lower_bound(v[i].begin(),v[i].end(),yy)-upper_bound(v[i].begin(),v[i].end(),xx);}int t=a[l];a[l]=a[r];a[r]=t;return (ans*2+1)*f;
}
int main(){//freopen("test.in","r",stdin);//freopen("test.out","w",stdout);int i,j,k,m,n,q,tot=0;scanf("%d%d",&n,&q);int len=sqrt(n);for(i=0;i<=n;i++){ devi[i]=0;a[i]=0; }for(i=1;i<=n;i++){a[i]=i;devi[i]=(i-1)/len+1;v[devi[i]].push_back(i);if((i-1)%len==0) blk[++tot].l=i;if(i%len==0) blk[tot].r=i;}long long ans=0;while(q--){int l,r;scanf("%d%d",&l,&r);if(l>r){ int t=l;l=r;r=t; }ans+=solve(l,r);printf("%lld\n",ans);}return 0;
}

十四的内心逐渐膨胀了起来,觉得紫题不过如此,于是决定再切一道分块紫题:作诗 - 洛谷

同一块内出现的偶数元素当然是固定的,但是在一段区间内,有其他的块,也有一些不完整的块内元素,所以对于不完整的块内元素每次固然是暴力统计,然而对于完整的块,我们可以提前预处理出完整的块相连中偶数次出现的元素个数f,同时用cnt记录从每块的头开始记录每个元素出现到整个数列结尾的次数,这样通过相减可以便捷地统计出每个元素在连接的完整的块间的出现次数,这样可以和两边暴力统计出来的元素个数相加,得出新的对偶元素,同时也可以去掉由于完整的块间是偶数次出现次数,而加上边缘暴力统计之后沦落为奇数次出现的元素。

按照惯例,下面附上十四的AC码:

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
using namespace std;
struct node{int l,r;
}blk[100001];
int a[100001],devi[100001],n,c;
int cnt[320][100001],f[320][320];
int lst[100001],num[100001];
int solve(int l,int r){int LP=devi[l],RP=devi[r];if(LP==RP){int sum=0,top=0;for(int i=l;i<=r;i++){num[a[i]]++;lst[++top]=a[i];}while(top){if(num[lst[top]]%2==0 && num[lst[top]]!=0)sum++;num[lst[top]]=0;top--;}return sum;}else{int sum=0,top=0;if(RP>LP+1) sum=f[LP+1][RP-1];for(int i=l;i<=blk[LP].r;i++){num[a[i]]++;lst[++top]=a[i];}for(int i=blk[RP].l;i<=r;i++){num[a[i]]++;lst[++top]=a[i];}while(top){int t=lst[top];if(num[t]!=0 && t!=0){int u=cnt[LP+1][t]-cnt[RP][t];if(u%2==1 && num[t]%2==1) sum++;if(u==0 && num[t]%2==0) sum++;if(u%2==0 && num[t]%2==1 && u>0) sum--;}num[t]=0;top--;}return sum;}
}
int main(){//freopen("test.in","r",stdin);//freopen("test.out","w",stdout);int i,j,k,m,tot=0;scanf("%d%d%d",&n,&c,&m);int len=sqrt(n);for(i=0;i<=len+1;i++)for(j=0;j<=len+1;j++)f[i][j]=0;for(i=0;i<=len+1;i++)for(j=0;j<=c;j++)cnt[i][j]=0;for(i=0;i<=n;i++){ lst[i]=0;num[i]=0; }for(i=1;i<=n;i++){scanf("%d",&a[i]);devi[i]=(i-1)/len+1;if((i-1)%len==0) blk[++tot].l=i;if(i%len==0) blk[tot].r=i;}blk[tot].r=n;for(i=1;i<=n;i+=len){int di=devi[i],sum=0;for(j=i;j<=n;j++){cnt[di][a[j]]++;if(cnt[di][a[j]]%2!=0 && cnt[di][a[j]]>1){sum--;}else if(cnt[di][a[j]]%2==0) sum++;if(j==n) f[di][devi[j]]=sum;else if(j%len==0) f[di][devi[j]]=sum;}}int lans=0;while(m--){int l,r;scanf("%d%d",&l,&r);l=((l+lans)%n)+1;r=((r+lans)%n)+1;if(l>r){ int t=l;l=r;r=t; }int ans=solve(l,r);printf("%d\n",ans);lans=ans;}return 0;
}

十四彻底膨胀了,他决定再来一道紫题目:[Violet] 蒲公英 - 洛谷

我们可以申请n个vector数组储存每种元素出现的坐标,这样我们就可以在之后方便地统计元素的出现次数,同样分块,统计连接的块之间最大的出现次数和加上块外的部分最大出现次数的元素~

那么我们就愉快地和十四一起AC掉这道题吧:

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
struct node{int l,r;
}blk[205];
vector<int> v[40010];
int devi[40010],a[40010];
int cnt[40010],f[205][205];
int c[40010];
int lst[40010],num[40010];
int get_num(int &l,int &r,int &k){return upper_bound(v[k].begin(),v[k].end(),r)-lower_bound(v[k].begin(),v[k].end(),l);
}
int solve(int l,int r){int LP=devi[l],RP=devi[r];if(LP==RP){int ans=0,maxsum=0;for(int i=l;i<=r;i++){int t=get_num(l,r,a[i]);if(t>maxsum || (t==maxsum && a[i]<ans)){ans=a[i];maxsum=t;}}return c[ans];}else{int ans=0,maxsum=0;if(RP>LP+1){ans=f[LP+1][RP-1];maxsum=get_num(l,r,ans);}for(int i=l;i<=blk[LP].r;i++){int t=get_num(l,r,a[i]);if(t>maxsum || (t==maxsum && a[i]<ans)){ans=a[i];maxsum=t;}}for(int i=blk[RP].l;i<=r;i++){int t=get_num(l,r,a[i]);if(t>maxsum || (t==maxsum && a[i]<ans)){ans=a[i];maxsum=t;}}return c[ans];}
}
int main(){//freopen("test.in","r",stdin);//freopen("test.out","w",stdout);int i,j,k,m,n,tot=0;scanf("%d%d",&n,&m);int len=sqrt(n);for(i=1;i<=n;i++){scanf("%d",&a[i]);devi[i]=(i-1)/len+1;if((i-1)%len==0) blk[++tot].l=i;if(i%len==0) blk[tot].r=i;c[i]=a[i];}blk[tot].r=n;sort(c+1,c+n+1);for(i=1;i<=n;i++){a[i]=lower_bound(c+1,c+n+1,a[i])-c;v[a[i]].push_back(i);}for(i=1;i<=n;i+=len){int di=devi[i];int maxn=0;memset(cnt,0,sizeof(cnt));for(j=i;j<=n;j++){cnt[a[j]]++;if(cnt[a[j]]>cnt[maxn] || (cnt[a[j]]==cnt[maxn] && a[j]<maxn)) maxn=a[j];if(j==n) f[di][devi[j]]=maxn;if(j%len==0) f[di][devi[j]]=maxn;}}int lans=0;while(m--){int u,v;scanf("%d%d",&u,&v);u=((u+lans-1)%n)+1;v=((v+lans-1)%n)+1;if(u>v){ int t=u;u=v;v=t; }int ans=solve(u,v);printf("%d\n",ans);lans=ans;}return 0;
}

莫队算法

通过上面的例题,你已经熟练地掌握了分块,那么我们就来看看莫队算法吧~

不知道屏幕前的小伙伴们在做题的时候有没有发现上面有很多题里面提到了“在线”这一概念,其实在线的意思就是通过各种办法强行让你按照题目所给出的询问顺序来进行查询操作,具体能强制你在线的方法大概有:每一次询问区间由上一次询问的结果加密得到,或者是在询问的区间内加上一些修改的操作,那么出题人为什么对在线这么重视呢?这当然是因为“离线”即不按照题给的顺序进行查询能对时间的要求降低很多。

莫队就是这样一种针对离线查询的算法:我们将众多的询问按照以下规则进行排序:1.区间左边所在的块下标靠前的排在前面;2.区间左边在同一个块内的话按照区间右边从小到大排序。

电视机前的小伙伴们于是就有了疑问:对于区间的右边我们按元素从小到大排序,那我们为什么不对区间左边边按元素从小到大排序呢?这样不是更佳精准吗?

我们先来看一道题目:

[SDOI2009] HH的项链 - 洛谷

我们首先按照上述规则对询问区间进行排序,然后设置左指针lz=1和右指针rz=0,对区间按顺序进行如下操作:

		while(lz<l) del(a[lz++],sum);while(lz>l) add(a[--lz],sum);while(rz<r) add(a[++rz],sum);while(rz>r) del(a[rz--],sum);

那么我们如何让时间复杂度尽量低呢?自然是减少指针的移动次数,回到上面提出的疑问,如果我们对区间的左边进行严格排序而不是按照分块排序的话,区间右边自然不能愉快地从小到大排序,所以右指针在这一排序规则下移动次数会大很多,而对于莫队算法的排序,左指针的移动次数仅仅限制在一个区间范围之内,而右指针在左区间相同的条件下也只有一个移动方向,这自然最大程度地避免了指针来回移动,同时能将时间复杂度降到最低。

通过上述思考,我们知道了莫队是基于对离线的询问进行排序,然后用指针进行暴力移动,在此基础上尽量减少指针的移动次数的算法。

下面附上十四的代码:

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
int a[1000010],devi[1000010];
int cnt[1000010];
struct node{int id,l,r;
}q[1000010];
int ans[1000010];
bool cmp(const node& a,const node& b){int LP=devi[a.l],RP=devi[b.l];if(LP!=RP) return LP<RP;return a.r<b.r;
}
void add(int x,int &sum){if(!cnt[x]) sum++;cnt[x]++;
}
void del(int x,int &sum){cnt[x]--;if(!cnt[x]) sum--;
}
int main(){int i,j,k,m,n;scanf("%d",&n);int len=sqrt(n);for(i=1;i<=n;i++){scanf("%d",&a[i]);devi[i]=(i-1)/len+1;cnt[a[i]]=0;}scanf("%d",&m);for(i=1;i<=m;i++){q[i].id=i;scanf("%d%d",&q[i].l,&q[i].r);}sort(q+1,q+m+1,cmp);int lz=0,rz=0,sum=0;for(k=1;k<=m;k++){int l=q[k].l,r=q[k].r,id=q[k].id;while(lz<l) del(a[lz++],sum);while(lz>l) add(a[--lz],sum);while(rz<r) add(a[++rz],sum);while(rz>r) del(a[rz--],sum);ans[id]=sum;}for(i=1;i<=m;i++) printf("%d\n",ans[i]);return 0;
}

修改莫队

通过上述的了解,我们知道了莫队是对于在线非常敏感的算法,但是对于一些修改类的强制在线,我们是可以用一些神奇的膜法绕过去的,我们来看这道题:[国家集训队] 数颜色 / 维护队列 - 洛谷

我们按照操作顺序设置一个时间戳,每次操作推进时间向前一步

	for(i=1;i<=m;i++){char op[2];int u,v;scanf("%s%d%d",op,&u,&v);if(*op=='Q'){mq++; q[mq].id=mq;q[mq].l=u;q[mq].r=v;q[mq].t=mc;}else if(*op=='R'){c[++mc].q=u;c[mc].c=v;}}

那么对于所有的询问来说,有哪些可以被影响到呢?我们通过思考可以想到对于这样的区间当切仅当满足以下条件:1.这个询问的时间戳在修改区间的时间之后,2.这个询问的区间和修改的区间范围有交集

如果不满足以上两点我们自然可以愉快地按照莫队算法进行计算,那么如果这个区间被影响了要怎么办呢?这里涉及到一个小技巧:我们可以把所有的修改操作存在一个数组里,当修改进行时,只需要将当前的数和修改数组里的数进行交换即可,那么在改回来的时候再次交换就又换回了原来的数。

下面附上十四的AC代码:

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=2000000;
int devi[N],a[N],mc,mq,cnt[N],ans[N];
struct Query{int id,l,r,t;
}q[N];
struct Modify{int q,c;
}c[N];
bool cmp(const Query& a,const Query& b){int al=devi[a.l],ar=devi[a.r];int bl=devi[b.l],br=devi[b.r];if(al!=bl) return al<bl;if(ar!=br) return ar<br;return a.t<b.t;
}
void add(int x,int &sum){if(!cnt[x]) sum++;cnt[x]++;
}
void del(int x,int &sum){cnt[x]--;if(!cnt[x]) sum--;
}
int main(){//freopen("test.in","r",stdin);//freopen("test.out","w",stdout);int i,j,k,m,n;scanf("%d%d",&n,&m);for(i=1;i<=n;i++)scanf("%d",&a[i]);mc=0;mq=0;for(i=1;i<=m;i++){char op[2];int u,v;scanf("%s%d%d",op,&u,&v);if(*op=='Q'){mq++; q[mq].id=mq;q[mq].l=u;q[mq].r=v;q[mq].t=mc;}else if(*op=='R'){c[++mc].q=u;c[mc].c=v;}}int len=cbrt((double)n * max(1,mc))+1;for(i=1;i<=max(n,m);i++) devi[i]=i/len;c[0].q=0;sort(q+1,q+mq+1,cmp);int lz=1,rz=0,sum=0,tz=0;memset(cnt,0,sizeof(cnt));for(i=1;i<=mq;i++){int l=q[i].l,r=q[i].r,t=q[i].t,id=q[i].id;while(rz<r) add(a[++rz],sum);while(rz>r) del(a[rz--],sum);while(lz<l) del(a[lz++],sum);while(lz>l) add(a[--lz],sum);while(tz<t){tz++;int cge=c[tz].q;if(l<=cge && cge<=r){add(c[tz].c,sum);del(a[cge],sum);}swap(a[cge],c[tz].c);}while(tz>t){int cge=c[tz].q;if(l<=cge && cge<=r){add(c[tz].c,sum);del(a[cge],sum);}swap(a[cge],c[tz].c);tz--;}ans[id]=sum;}for(i=1;i<=mq;i++) printf("%d\n",ans[i]);return 0;
}

✳️回滚莫队/不删除莫队

我们来看这样一道题目:给你一个长度为n的数组A和m个询问区间,每次询问区间【L,R】内重要度最大的数字,要求输出其重要度,这里i的重要度的定义是i*i在区间内出现的次数

对于区间内出现的次数增加我们自然是很好操作的,那么对于删除呢?我们即便可以将左指针向右推,我们也不能对与区间最大的重要度去除对于删除元素的影响,那么我们可以进行如下操作:

如果询问区间在同一个块内,我们直接从左到右暴力即可

对于所有左区间在一个块内的询问区间我们将指针移动分为两拨,我们设置一个right=blk[LP].r,我们把左指针lz=right+1,右指rz=right。

我们先移动又指针到q[x].r,由于询问区间已经按照规则排序的缘故,在计算之后的区间时,右指针只有向右移动这一个方向。移动好之后我们先用一个临时变量记录一下这时候最重要的状态。

接下来我们向左边移动左指针,移动到q[x].l时候,更新最重要的状态,并且在这时候记录到ans数组里面。

记录完之后,我们将最重要的状态赋值为之前用临时变量记录的状态,这一步的操作就叫做回滚,这样我们就回到了不被左边影响的状态。

接下来我们将左指针移动到right+1处,进行下一个询问的统计。

当所有区间左边在一个块内的询问处理好之后,我们把一切记录状态的数组和变量清零,进行下一个询问区间的操作。

这就是回滚莫队,通过巧妙的指针移动避开了难以维护的状态。

✳️树上莫队

经过了上面的铺垫,我们终于可以来理解树上莫队啦~

按照惯例我们来看一道题目:COT2 - Count on a tree II - 洛谷

这题给出的树是这样的:

我们引入一个新的概念:欧拉数列,即在dfs遍历的时候,最开始的时候记录一下,结束的时候记录一下:

void bjdfs(int x,int father){ora[++oranum]=x;fst[x]=oranum;for(int i=pre[x];i!=0;i=edge[i].next){int y=edge[i].to;if(y==father) continue;bjdfs(y,x);}ora[++oranum]=x;lst[x]=oranum;return ;
}

这样这棵树就被ora序列就记录成了: 1 4 8 8 4 3 7 7 6 6 5 5 3 2 2 1

我们同时可以顺手记录下每个元素出现的位置fst[i]和结束时出现的位置lst[i],我们通过观察可以发现对于两个元素u,v的路径上出现的元素,只会在lst[u]道fst[v]上出现一次,并且路径上两节点的最近公共祖先lca则不会出现,所以对于每条路径,我们只要记录欧拉序列中出现一次的元素和lca即可,记得要考虑好u是u和v的lca这样的情况。

下面附上十四的代码:

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
const int N=1000100;
struct Query{int id,l,r,p;
}q[N];
struct node{int next,to;
}edge[N];
int fst[N],lst[N],a[N],len,tot,pre[N],ans[N];
int ora[N],cnt[N],oranum,depth[N],vis[N],dp[N][20];
int appt[N];
vector<int> px;
int get_devi(int x){return (x-1)/len+1;
}
int cmp(const Query& a,const Query& b){int LP=get_devi(a.l),RP=get_devi(b.l);if(LP!=RP) return LP<RP;return a.r<b.r;
}
void addedge(int u,int v){edge[++tot].next=pre[u];edge[tot].to=v; pre[u]=tot;return ;
}
void bjdfs(int x,int father){ora[++oranum]=x;fst[x]=oranum;for(int i=pre[x];i!=0;i=edge[i].next){int y=edge[i].to;if(y==father) continue;bjdfs(y,x);}ora[++oranum]=x;lst[x]=oranum;return ;
}
void lcadfs(int x){for(int i=1;(i<<i)<depth[x];i++)dp[x][i]=dp[dp[x][i-1]][i-1];for(int i=pre[x];i!=0;i=edge[i].next){int y=edge[i].to;if(vis[y]) continue;vis[y]=1;dp[y][0]=x;depth[y]=depth[x]+1;lcadfs(y);}return ;
}
void add(int x,int &sum){appt[x]^=1;if(appt[x]){if(!cnt[a[x]]) sum++;cnt[a[x]]++;}else{cnt[a[x]]--;if(!cnt[a[x]]) sum--;}return ;
}
int lca(int a,int b){int u=a,v=b;if(depth[u]<depth[v]) swap(u,v);int k=log2(depth[u]-depth[v]);for(int i=k;i>=0;i--)if(depth[dp[u][i]]>=depth[v])u=dp[u][i];if(u==v) return u;k=log2(depth[u]);for(int i=k;i>=0;i--){if(dp[u][i]==dp[v][i]) continue;u=dp[u][i];v=dp[v][i];}return dp[u][0];
}
int main(){//freopen("test.in","r",stdin);//freopen("test.out","w",stdout);int i,j,k,m,n;scanf("%d%d",&n,&m);for(i=1;i<=n;i++){scanf("%d",&a[i]);px.push_back(a[i]);}sort(px.begin(),px.end());px.erase(unique(px.begin(),px.end()),px.end());for(i=1;i<=n;i++) a[i]=lower_bound(px.begin(),px.end(),a[i])-px.begin();for(i=1;i<n;i++){int u,v;scanf("%d%d",&u,&v);addedge(u,v);addedge(v,u);}bjdfs(1,-1);len=sqrt(oranum);memset(depth,0,sizeof(depth));memset(vis,0,sizeof(vis));memset(dp,0,sizeof(dp));depth[1]=1;vis[1]=1;lcadfs(1);for(i=1;i<=m;i++){int u,v;scanf("%d%d",&u,&v);if(fst[u]>fst[v]) swap(u,v);int p=lca(u,v);if(p==u){q[i].id=i;q[i].l=fst[u];q[i].r=fst[v];}else{q[i].id=i;q[i].l=lst[u];q[i].r=fst[v];q[i].p=p;}}sort(q+1,q+m+1,cmp);int lz=1,rz=0,sum=0;memset(cnt,0,sizeof(cnt));memset(appt,0,sizeof(appt));for(int i=1;i<=m;i++){int l=q[i].l,r=q[i].r,p=q[i].p,id=q[i].id;while(rz<r) add(ora[++rz],sum);while(rz>r) add(ora[rz--],sum);while(lz>l) add(ora[--lz],sum);while(lz<l) add(ora[lz++],sum);if(p) add(p,sum);ans[id]=sum;if(p) add(p,sum);}for(int i=1;i<=m;i++)printf("%d\n",ans[i]);return 0;
}

最后我们来看看颜色平衡树这道题

十四经过上面的联系,十四不由自主地想出了这样一个解法:对于欧拉数列,从fst[i]到lst[i]即可表达一颗完整的子树,而通过枚举n个节点,统计其对应的所有fst[i]~lst[i]中的颜色数量即可,可以先自己制造问题:所有的fst[i]~lst[i],然后基于欧拉数列的分块进行排序这些询问区间,然后用莫队的算法通过移动指针得到每个区间的颜色数量。

问题来了,既然维护的是颜色出现的次数,每次要进行判断,我们自然不肯能枚举每个元素,然后挨个挨个查非零的元素是否出现了同样的次数,那我们对于删除元素的处理自然非常麻烦,于是我们想到了用回滚莫队的方法维护last数组

下面是十四的AC代码:

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=400100;
int len,ora[N],oranum,tot,fst[N],lst[N];
int cnt[N],last[N],color[N],top,pre[N];
struct node{int next,to;
}edge[N];
struct Query{int l,r;
}q[N];
struct block{int l,r;
}blk[N];
int get_devi(int x){return (x-1)/len+1;
}
int cmp(const Query& a,const Query& b){int LP=get_devi(a.l),RP=get_devi(b.l);if(LP!=RP) return LP<RP;else return a.r<b.r;
}
void addedge(int u,int v){edge[++tot].next=pre[u];pre[u]=tot;edge[tot].to=v;return ;
}
void bjdfs(int x){ora[++oranum]=x;fst[x]=oranum;for(int i=pre[x];i!=0;i=edge[i].next){int y=edge[i].to;bjdfs(y);}ora[++oranum]=x;lst[x]=oranum;return ;
}
void add(int x){cnt[x]++;return ;
}
void del(int x){cnt[x]--;return ;
}
int main(){//freopen("test.in","r",stdin);//freopen("test.out","w",stdout);int i,j,k,m,n,s;scanf("%d",&n);for(i=1;i<=n;i++){int u,v;scanf("%d%d",&u,&v);color[i]=u;if(v==0){ s=i; continue; }addedge(v,i);}bjdfs(s);len=sqrt(oranum);int blktot=0;for(i=1;i<=oranum;i++){if((i-1)%len==0) blk[++blktot].l=i;if(i%len==0) blk[blktot].r=i;}blk[blktot].r=oranum;int qsnum=0;for(i=1;i<=n;i++){q[++qsnum].l=fst[i];q[qsnum].r=lst[i];}sort(q+1,q+n+1,cmp);memset(cnt,0,sizeof(cnt));memset(last,0,sizeof(last));int lz=1,rz=0;top=0;int ans=0,x;for(x=1;x<=n;){int y=x;while(y<=n && get_devi(q[y].l)==get_devi(q[x].l)) y++;int right=blk[get_devi(q[x].l)].r;while(x<y && q[x].r<=right){int l=q[x].l,r=q[x].r;for(j=l;j<=r;j++){add(color[ora[j]]);last[++top]=color[ora[j]];}int flag=1,asnum=cnt[last[top]];top--;while(top>0){if(cnt[last[top]]!=asnum){flag=0;break;}top--;}top=0;if(flag==1)ans++;for(j=l;j<=r;j++) del(color[ora[j]]);x++;}int rz=right,lz=right+1;while(x<y){int l=q[x].l,r=q[x].r,sign;while(rz<r){add(color[ora[++rz]]);last[++top]=color[ora[rz]];}sign=top;while(lz>l){add(color[ora[--lz]]);last[++top]=color[ora[lz]];}int ttop=top,asnum=cnt[last[ttop]];ttop--; int flag=1;while(ttop>0){if(cnt[last[ttop]]!=asnum){flag=0;break;}ttop--;}if(flag==1) ans++;top=sign;while(lz<right+1) del(color[ora[lz++]]);x++;}top=0;memset(cnt,0,sizeof(cnt));}printf("%d\n",ans);return 0;
}

嗨嗨嗨,十四这下终于能睡个好觉了,让我们来恭喜他吧~

相关文章:

[蓝桥杯 2023 省 A] 颜色平衡树:从零开始理解树上莫队 一颗颜色平衡树引发的惨案

十四是一名生物工程的学生&#xff0c;他已经7年没碰过信息学竞赛了&#xff0c;有一天他走在蓝桥上看见了一颗漂亮的颜色平衡树&#xff1a; ​​​​​​​[蓝桥杯 2023 省 A] 颜色平衡树 - 洛谷 十四想用暴力解决问题&#xff0c;他想枚举每个节点&#xff0c;每个节点代表…...

maya打开bvh脚本

目录 maya打开脚本编辑器 运行打开bvh脚本 maya导出bvh脚本 maya打开脚本编辑器 打开Maya软件&#xff0c;点击右下角 “脚本编辑器” 运行打开bvh脚本 https://github.com/jhoolmans/mayaImporterBVH/blob/master/bvh_importer.py import os import re from typing impo…...

【JavaSE】数据类型和运算符

前言 从这一篇我们开始Java的学习~ 欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 目录 前言 Java第一个程序 字面常量 字面常量的分类 结合代码理解 类型转换 类型提升 byte与byte的运算 正确写法 字符串类型St…...

Docker 哲学 - ip 的组成规则 与 网关介绍

在 IP 地址中&#xff0c;我们通常将 IP 地址分为两部分&#xff1a;网络部分和主机部分。网络部分用于标识网络&#xff0c;主机部分用于标识该网络中的特定主机。 IP 地址的每个部分&#xff08;也被称为一个八位组或一个字节&#xff09;可以是从0到255的任何值。 一个 IPv4…...

数学建模竞赛真的是模型解题一般,但是论文出彩而获奖的吗?

最近&#xff0c;数乐君发现有同学会有这样的问题&#xff1a;在数学建模国赛中&#xff0c;会因为参赛团队的模型解题一般&#xff0c;但论文写得非常精彩而获奖吗&#xff1f; 是的&#xff0c;确实会存在这样的情况。 我们都知道数学建模竞赛最终都是以提交成品论文的形式…...

深度学习常见的三种模型

深度学习模型实际上是一个包含多个隐藏层的神经网络&#xff0c;目前主要有卷积神经网络&#xff08;CNN&#xff09;、深度置信网络&#xff08;DBN&#xff09;、循环神经网络&#xff08;RNN&#xff09;。 1) 卷积神经网络 在机器学习领域&#xff0c;卷积神经网络属于前…...

接口自动化测试分层设计与实践总结

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号&#xff1a;互联网杂货铺&#xff0c;回复1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 接口测试三要素&#xff1a; 参数构造 发起请求&#x…...

集合(下)Map集合的使用

文章目录 前言一、Map接口二、Map接口的实现类 1.HashMap类2.TreeMap类总结 前言 Map集合没有继承Collection接口&#xff0c;不能像List集合和Set集合那样直接使用Collection接口的方法。Map集合其自身通过以key到value的映射关系实现的集合&#xff0c;也有相应的许多方法。类…...

AAPT: error: resource android:attr/dialogCornerRadius not found.

ERROR:D:\android.gradle\caches\transforms-3\b3b98118f65da38d0ad9da84cfc70a72\transformed\appcompat-1.0.0\res\values-v28\values-v28.xml:5:5-8:13: AAPT: error: resource android:attr/dialogCornerRadius not found. 请帮我看看这个错误是什么意思。我改如何做。 这个…...

数字功放VS模拟功放,选择适合你的音频解决方案

数字功放和模拟功放是音频系统中常用的两种功放技术&#xff0c;适用于不同的音频应用&#xff0c;都具有各自的优势和特点。本文将为您详细介绍数字功放和模拟功放的差异&#xff0c;并帮助您找到适合自己的音频解决方案。 1、数字功放是一种利用数字信号处理技术的功放。它将…...

5.88 BCC工具之tcpsynbl.py解读

一,工具简介 tcpsynbl工具以直方图的形式显示SYN到达时的TCP SYN积压大小。这可以让我们了解应用程序距离达到积压限制并丢弃SYN(导致SYN重传产生性能问题)还有多远。 TCP SYN 数据包则通常用于启动 TCP 三次握手过程的第一次握手。 二,代码示例 #!/usr/bin/env python…...

GVRP实现vlan的自动创建和注册

拓扑图 资源已上传 流程 第一、每台交换机的全局及端口下使能GVRP功能 第二、配置交换机的二层连通性&#xff0c;交换机某些端口配置Trunk端口&#xff0c;并允许相应的vlan通过 第三、在交换机4和5配置静态vlan100&#xff0c;然后查看1和3交换机是否有vlan100的定义&…...

Oracle VM VirtualBox修改磁盘大小

一、 修改虚拟机磁盘大小 先把虚拟机停掉。再增加磁盘大小。 路径中有空格的用""包起来。 D:\Program Files\Oracle\VirtualBox>VBoxManage.exe modifyhd "D:\Program Files\VirtualBox VMs\mycentos\mycentos.vdi" --resize 30000 0%...10%...20%...3…...

【嵌入式硬件】步进电机

1.步进电机简介 1.1步进电机基本原理 步进电机的英文是stepping motor。step的中文意思是行走、迈步。所以仅从字面上我们就可以得知,步进电机就是一步一步移动的电动机。说的官方一点儿,步进电机是一种将电脉冲信号转换成相应角位移或者线位移的电动机(直线电机)。下图为…...

FlyControls 是 THREE.js 中用于实现飞行控制的类,它用于控制摄像机在三维空间中的飞行。

demo演示地址 FlyControls 是 THREE.js 中用于实现飞行控制的类&#xff0c;它用于控制摄像机在三维空间中的飞行。 入参&#xff1a; object&#xff1a;摄像机对象&#xff0c;即要控制的摄像机。domElement&#xff1a;用于接收用户输入事件的 HTML 元素&#xff0c;通常…...

【Java程序设计】【C00366】基于(JavaWeb)Springboot的纹理生产图片系统(有论文)

TOC 博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;博客中有上百套程序可供参考&#xff0c;欢迎共同交流学习。 项目简介 项目获取 &#x1f345;文末点击卡片…...

编译原理Lab. 1 初代编译器实验说明和要求

目录 Lab. 1 初代编译器实验说明和要求一、初代编译器功能描述二、初代编译器文法要求三、初代编译器测试样例四、初代编译器提交要求五、初代编译器实验测试框架说明 代码与思路 Lab. 1 初代编译器实验说明和要求 一、初代编译器功能描述 初代编译器将 C 语言顺序语句序列翻…...

python判断工作日,节假日

一、概述 需要判断一个日期是否为工作日&#xff0c;节假日。 找到一个现成的插件&#xff0c;蛮好用的。 插件介绍 https://pypi.org/project/chinesecalendar/ 判断某年某月某一天是不是工作日/节假日。 支持 2004年 至 2020年&#xff0c;包括 2020年 的春节延长。 兼容…...

练习4-权重衰减(李沐函数简要解析)

环境:练习1的环境 代码详解 0.导入库 import torch from torch import nn from d2l import torch as d2l1.初始化数据 这里初始化出train_iter test_iter 可以查一下之前的获取Fashion数据集后的数据格式与此对应 n_train, n_test, num_inputs, batch_size 20, 100, 200, …...

websocket 中 request-line 中的URI编码问题

首先&#xff0c;request-line组成如下&#xff1a; Request-Line Method SP Request-URI SP HTTP-Version CRLF 在 rfc6455 规范的 5.1.2 Request-URI 中&#xff0c;有这样的描述&#xff1a; The Request-URI is transmitted in the format specified in section 3.2.1. …...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误

HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误&#xff0c;它们的含义、原因和解决方法都有显著区别。以下是详细对比&#xff1a; 1. HTTP 406 (Not Acceptable) 含义&#xff1a; 客户端请求的内容类型与服务器支持的内容类型不匹…...

如何在看板中体现优先级变化

在看板中有效体现优先级变化的关键措施包括&#xff1a;采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中&#xff0c;设置任务排序规则尤其重要&#xff0c;因为它让看板视觉上直观地体…...

Linux简单的操作

ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具

文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...

新能源汽车智慧充电桩管理方案:新能源充电桩散热问题及消防安全监管方案

随着新能源汽车的快速普及&#xff0c;充电桩作为核心配套设施&#xff0c;其安全性与可靠性备受关注。然而&#xff0c;在高温、高负荷运行环境下&#xff0c;充电桩的散热问题与消防安全隐患日益凸显&#xff0c;成为制约行业发展的关键瓶颈。 如何通过智慧化管理手段优化散…...

【AI学习】三、AI算法中的向量

在人工智能&#xff08;AI&#xff09;算法中&#xff0c;向量&#xff08;Vector&#xff09;是一种将现实世界中的数据&#xff08;如图像、文本、音频等&#xff09;转化为计算机可处理的数值型特征表示的工具。它是连接人类认知&#xff08;如语义、视觉特征&#xff09;与…...

OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 在 GPU 上对图像执行 均值漂移滤波&#xff08;Mean Shift Filtering&#xff09;&#xff0c;用于图像分割或平滑处理。 该函数将输入图像中的…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

2023赣州旅游投资集团

单选题 1.“不登高山&#xff0c;不知天之高也&#xff1b;不临深溪&#xff0c;不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...

Pinocchio 库详解及其在足式机器人上的应用

Pinocchio 库详解及其在足式机器人上的应用 Pinocchio (Pinocchio is not only a nose) 是一个开源的 C 库&#xff0c;专门用于快速计算机器人模型的正向运动学、逆向运动学、雅可比矩阵、动力学和动力学导数。它主要关注效率和准确性&#xff0c;并提供了一个通用的框架&…...