肥宅快乐主席树
目录
- 静态主席树
- 算法理解
- 例题
- 讲解
- 练习
- 树上第k小
- 毒瘤逆序对
- 求区间种类
- 简单询问
- 简单查询
- 矩阵主席树大暴力!!!
- 算法理解
- 动态主席树
- 讲解
- 例题
- 算法讲解
- 代码
- 练习
- 不是动态主席树的带修题。
- 讲解
- 可持久化主席树
- 例题
- 思路
- 单点修改
- 区间修改
- 代码
- 小结
- 小结
- 查错
静态主席树
算法理解
例题
时间限制: 1 Sec 内存限制: 128 MB 【问题描述】 给n(1<=n<=100000)个数字a[1],a[2],......,a[n](0<=a[i]<=1000000000),m(1<=m<=100000)次询问l到r之间的第k小的值。 【输入文件】 第一行为n和m。 接下来一行输入n个数。 接下来m行,每行三个数l,r和k。 【输出文件】 m行,每行对应一个答案。 【输入样例】 7 3 1 5 2 6 3 7 4 2 5 3 4 4 1 1 7 3 【输出样例】 5 6 3讲解
例题思路
首先,看到这道题目,我们是懵逼的,甚至很迷茫。
但是仔细思考,我们都知道线段树是维护整段的区间第\(k\)大,但是如何找到一段呢?
这是有个想法闪过,就是我们可以将\(l-r\)区间内的数字扔到一个权值线段树内进行查询。
这便是一种较暴力的做法了。
主席树?前缀和?
主席树本质上可以说是缺了许多东西的线段树(不严谨),但是这样子让主席树更加的灵活,具有更多的功能。
但同时伴随的常数大的问题。
主席树主要支持这些操作:
我们要提取\(l-r\)区间内的数字,而主席树又支持加减,这不由让我们想到了前缀和。
想到了个思路我们就要去实现他。
插点link
主席树是棵残缺的线段树,也就是说只有一条链也是可以的,那我们就把每个位置都当成一棵权值主席树,然后把这个点插进去。
如下图(注意是权值线段树):
图好丑博主手绘差就不多吐槽了,好吧。
代码如下:
void link(int &now,int l,int r,int c) {if(!now)now=++len;tr[now].c++;if(l==r)return ;int mid=(l+r)/2;if(c<=mid)link(tr[now].lc,l,mid,c);else link(tr[now].rc,mid+1,r,c); }你可以认为这是一个数组,那么他离前缀和数组还有一个步骤,就是从前加到后,也就是\(merge\)
merge
合并是什么东西?
先说合并的限制:合并的话一般限于两棵主席树维护范围相同、维护值的意义是相同的,且目前两棵主席树里面维护的点不重复(有时候可以重,看是权值还是位置)。
而合并,也是有点讲究的。
如图是一个将第一个主席树\(A\)合并到第二棵主席树\(B\)里面的一个例子,首先,我们需要将\(A\)相同的节点(指维护范围一样的点)的\(c\)值添加到\(B\)的节点里面去,同时给\(B\)补上\(B\)没有\(A\)有的点,补得方法不是新建节点,而是直接把儿子认到\(A\)的节点上面,也就是说上图合并完后树的样子应长这样:
而主席树或者线段树一般都是跳儿子的,这样做貌似也没多大问题哟。
那么我们就可以从前到后合并的。
但是同时也有个问题,不要把三棵树合并在一棵树上,不然总会有个节点会被改变,导致前面的主席树改过了,出现一坨锅,所以主席树最好不要把许多树合并到一棵树上(但可以按顺序合并),当然也存在特殊情况,毕竟总有万一。
那么这些前缀和是可以预处理的,复杂度也是\(O(nlogn)\),因为一条链你再怎么跳也只能跳到\(logn\)个节点,然后合并\(n\)次。
顺便提一提:合并只要你不改变前面树的形态,在不改变时间复杂度的方法,可以有一些其他的方法进行建树(也是前缀和,但是可能每棵树有好几条链)。
void merge(int &x,int y)//y合并到x里面去 {if(x==0){x=y;return ;}if(y==0)return ;tr[x].c+=tr[y].c;merge(tr[x].l,tr[y].l);merge(tr[x].r,tr[y].r); }快乐加减
加法一般要求两棵主席树维护的点没有重合,而减法一般是要求减树维护的点集是被减树的子集,同时一般要求维护的东西相同,范围相同。
当然,这里维护的点的意思一般是位置,不如说一个维护的是1、3、5号点,另外一个是2、4号点,而不是权值重复,即使是权值主席树,一般也是这样的。
利用加减,有时候我们可以进行容斥原理来得到我们想要的点的子集。
这里我们就用类似前缀和的方法:拿\(r\)的主席树减去\(l-1\)的主席树,就得出了区间的主席树,然后查询。
int cale(int x/*减树*/,int y/*被减树*/,int l,int r,int k) {if(l==r)return ls[l].x;int mid=(l+r)/2,kc=tr[tr[x].l].c-tr[tr[y].l].c;//左边区间有多少个数字。if(k<=kc)return cale(tr[x].l,tr[y].l,l,mid,k);else return cale(tr[x].r,tr[y].r,mid+1,r,k-kc); }完整代码
既然询问代码都出来了,那就代码一起贴出来吧,当然你乐意的话加个离散化也是可以的。
#include<cstdio> #include<cstring> #define N 110000 #define M 5100000 #define INF 1000000000//数字的范围 using namespace std; struct node {int lc,rc,c; }tr[M];int len=0,rt[N]/*根节点数组*/; void link(int &now,int l,int r,int c) {if(!now)now=++len;//新建节点tr[now].c++;if(l==r)return ;int mid=(l+r)/2;if(c<=mid)link(tr[now].lc,l,mid,c);else link(tr[now].rc,mid+1,r,c); } void merge(int &x,int y) {if(!x){x=y;return ;}if(!y)return ;tr[x].c+=tr[y].c;merge(tr[x].lc,tr[y].lc);merge(tr[x].rc,tr[y].rc); } int calc(int x,int y,int l,int r,int k) {if(l==r)return l;int zhe=tr[tr[y].lc].c-tr[tr[x].lc].c,mid=(l+r)/2;if(zhe<k)return calc(tr[x].rc,tr[y].rc,mid+1,r,k-zhe);else return calc(tr[x].lc,tr[y].lc,l,mid,k); } int n,m; int main() {scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){int x;scanf("%d",&x);link(rt[i],0,INF,x);merge(rt[i],rt[i-1]);}for(int i=1;i<=m;i++){int l,r,k;scanf("%d%d%d",&l,&r,&k);printf("%d\n",calc(rt[l-1],rt[r],0,INF,k));}return 0; }练习
树上第k小
时间限制: 2 Sec 内存限制: 128 MB 【题目描述】 给定一棵N(1<=N<=100000)个节点的树,每个点有一个权值,对于M(1<=M<=100000)个询问(x,y,k),你需要回答x和y这两个节点间第k小的点权。 【输入数据】 第一行两个整数N,M。 第二行有N个整数,其中第i个整数表示点i的权值。 后面N-1行每行两个整数(x,y),表示点x到点y有一条无向边。 最后M行每行两个整数(x,y,k),表示一组询问。 【输出数据】 M行,表示每个询问的答案。 【输入样例】 8 5 105 2 9 3 8 5 7 7 1 2 1 3 1 4 3 5 3 6 3 7 4 8 2 5 1 1 5 2 7 8 3 1 6 3 3 4 2 【输出样例】 2 9 7 105 9一看就就是一道毒瘤题,我们可以发现\(x->y\)的路径可以分为两部分(设\(z=lca(x,y)\)):\(x->z,y->z\)(\(z\)重合了,在后面的容斥原理中会解决这问题),那么我们仍然要把\(x->y\)提取出来,那么我们就设\(x\)的主席树代表的\(x\)到根节点这些点权值的主席树,那么就可以一个DFS解决主席树预处理的问题了,提取的话我们也可以用容斥原理解决:
(\([x]\)代表\(x\)的主席树,以此类推)\([x]+[y]-[z]-[fa(z)]\)。这样不就好起来了吗?(滑稽)
//这里的LCA是用ST表的 #include<cstdio> #include<cstring> #include<algorithm> #define M 410000 #define NN 5100000 using namespace std; struct node {int y,next; }a[M];int len,last[M]; inline void ins(int x,int y){len++;a[len].y=y;a[len].next=last[x];last[x]=len;} int st[25][M],log2[M],in[M],out[M],dep[M],cnt,fa[M]; void dfs(int x) {st[0][++cnt]=x;in[x]=cnt;for(int k=last[x];k;k=a[k].next){int y=a[k].y;if(fa[x]!=y){fa[y]=x;dep[y]=dep[x]+1;dfs(y);st[0][++cnt]=x;}}out[x]=cnt; }//设1为根节点,处理DFS序 inline int myminz(int x,int y){return dep[x]<dep[y]?x:y;} void first() {dfs(1);for(int i=2;i<=cnt;i++)log2[i]=log2[i>>1]+1;for(int i=1;i<=log2[cnt];i++){for(int j=cnt-(1<<i)+1;j>=1;j--)st[i][j]=myminz(st[i-1][j],st[i-1][j+(1<<(i-1))]);} }//ST表预处理 inline int lca(int x,int y) {int tx=out[x],ty=in[y];tx>ty?tx^=ty^=tx^=ty:0;int k=log2[ty-tx+1];return myminz(st[k][tx],st[k][ty-(1<<k)+1]); } //LCA struct TREE {int lc,rc,c; }tr[NN];int trlen; void link(int &x,int l,int r,int c) {if(!x)x=++trlen;tr[x].c++;if(l==r)return ;int mid=(l+r)/2;if(c<=mid)link(tr[x].lc,l,mid,c);else link(tr[x].rc,mid+1,r,c); } void merge(int &x,int y) {if(!x){x=y;return ;}if(!y)return ;tr[x].c+=tr[y].c;merge(tr[x].lc,tr[y].lc);merge(tr[x].rc,tr[y].rc); } int ask(int x,int y,int z1,int z2,int l,int r,int k)//x+y-z1-z2 {if(l==r)return l;int zhe=tr[tr[x].lc].c+tr[tr[y].lc].c-tr[tr[z1].lc].c-tr[tr[z2].lc].c,mid=(l+r)/2;if(zhe<k)return ask(tr[x].rc,tr[y].rc,tr[z1].rc,tr[z2].rc,mid+1,r,k-zhe);else return ask(tr[x].lc,tr[y].lc,tr[z1].lc,tr[z2].lc,l,mid,k); }//主席树 int n,m,rt[M],zhi[M]/*原数*/; int qwq[M]/*存编号*/,qaq[M]/*离散化后的值*/;//这不是离散化吗 void ddfs(int x) {for(int k=last[x];k;k=a[k].next){int y=a[k].y;if(fa[x]!=y){link(rt[y],1,n,qaq[y]);merge(rt[y],rt[x]);ddfs(y);}} }//处理每个点的主席树 inline bool cmp(int x,int y){return zhi[x]<zhi[y];} int main() {scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){scanf("%d",&zhi[i]);qwq[i]=i;}for(int i=1;i<n;i++){int x,y;scanf("%d%d",&x,&y);ins(x,y);ins(y,x);}sort(qwq+1,qwq+n+1,cmp);for(int i=1;i<=n;i++)qaq[qwq[i]]=i;first();link(rt[1],1,n,qaq[1]);ddfs(1);for(int i=1;i<=m;i++){int l,r,k;scanf("%d%d%d",&l,&r,&k);if(l>r)l^=r^=l^=r;//异或的灵性交换int x=lca(l,r);printf("%d\n",zhi[qwq[ask(rt[l],rt[r],rt[fa[x]],rt[x],1,n,k)]]);}return 0; }毒瘤逆序对
时间限制: 3 Sec 内存限制: 256 MB 【题目描述】 给你N(1<=N<=50000)个数,M(1<=M<=50000)个询问,每个询问(l,r)求l到r的逆序对数。 注:逆序指的是a[i]>a[j]并且i<j 【输入数据】 第一行两个整数N,M。 第二行有N个整数。 最后M行每行一组操作。 【输出数据】 M行,表示每个询问的答案。 【输入样例】 5 3 3 1 2 5 4 1 3 2 1 3 5 【输出样例】 2 1 1 【提示】 如果被卡常,请加上 #pragma GCC optimize("Ofast")你以为这是一道SB题?不,他不是,他是分块加主席树暴力AC的题目。
首先,我们思考一下,如果我们能预处理\(ans[i][j]\)表示\(i\)到\(j\)的逆序对数,不就好起来了吗?
不谈时间,空间都让你崩溃的一批,而\(O(n^{2}logn)\)的预处理时间更是黄(TLE的颜色是黄色,MLE也是,在那个OJ上)的飞起。
那么我们就应该考虑一些毒瘤的优化,这时,我们就想到了\(ans[i][j]\)表示\(i\)到\(i+(1<<j)-1\)的位置上有多少逆序对数。
将询问分成两段,解决前半段,后半段用快乐主席树。
但是如何预处理便成了一大难题。
于是我们想到了减复杂度的好帮手,分块!
\(ans[i][j]\)表示的是\(i\)块的开头到\(j\)有几个逆序对。
用树状数组预处理就可以解决了。
而前半段也是用主席树轻轻松松搞定的事情。
#pragma GCC optimize("Ofast")//毒瘤优化 #include<cstdio> #include<cstring> #include<algorithm> #define sN 310 #define N 51000 #define M 1100000 using namespace std; int n,m; struct node {int lc,rc,c; }tr[M];int len=0,rt[N]; void link(int &now,int l,int r,int c) {if(!now)now=++len;tr[now].c++;if(l==r)return ;int mid=(l+r)/2;if(c<=mid)link(tr[now].lc,l,mid,c);else link(tr[now].rc,mid+1,r,c); } void merge(int &x,int y) {if(!x){x=y;return ;}if(!y)return ;tr[x].c+=tr[y].c;merge(tr[x].lc,tr[y].lc);merge(tr[x].rc,tr[y].rc); } int calc(int x,int y,int k,int l,int r) {if(l==r)return 0;int mid=(l+r)/2;if(k<=mid)return calc(tr[x].lc,tr[y].lc,k,l,mid);else return calc(tr[x].rc,tr[y].rc,k,mid+1,r)+(tr[tr[y].lc].c-tr[tr[x].lc].c); } //快乐主席树 int bst[N]; int lowbit(int x){return x&-x;} void change(int x,int c) {while(x<=n){bst[x]+=c;x+=lowbit(x);} } int getsum(int x) {int ans=0;while(x){ans+=bst[x];x-=lowbit(x);}return ans; } //树状数组 int ans[sN][N],id[N],ll[sN],rr[sN],cnt,a[N],cc; int sor[N],LS[N]; inline bool cmp(int x,int y){return a[x]<a[y];} void fenkuai(int l,int r) {ll[++cnt]=l;rr[cnt]=r;for(int i=l;i<=r;i++)id[i]=cnt;for(int i=1;i<=n;i++)bst[i]=0;for(int i=l;i<=n;i++){change(LS[i],1);ans[cnt][i]=ans[cnt][i-1]+(i-l+1-getsum(LS[i]));} } //处理块的信息 inline int mymin(int x,int y){return x<y?x:y;} int main() {scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){sor[i]=i;scanf("%d",&a[i]);}sort(sor+1,sor+n+1,cmp);for(int i=1;i<=n;i++)a[sor[i]]==a[sor[i-1]]?LS[sor[i]]=LS[sor[i-1]]:LS[sor[i]]=i;//离散化for(int i=1;i<=n;i++){if(i*i>=n){cc=i;break;}}int now=0;for(now=1;now<=n;now+=cc)fenkuai(now,mymin(now+cc-1,n));//分块for(int i=1;i<=n;i++){link(rt[i],1,n,LS[i]);merge(rt[i],rt[i-1]);}for(int i=1;i<=m;i++){int l,r;scanf("%d%d",&l,&r);if(l>r)l^=r^=l^=r; int x=id[l-1]+1;int answer=ans[x][r],ed=mymin(rr[x-1],r);if(l<=ed){for(int i=l;i<=ed;i++)answer+=calc(rt[i-1],rt[r],LS[i],1,n);}printf("%d\n",answer);}return 0; }求区间种类
时间限制: 1 Sec 内存限制: 128 MB 【题目描述】 给n(1<=n<=50000)个数,a[1],a[2],......,a[n](0<=a[i]<=1000000),m(1<=m<=200000)个询问。每个询问包含两个数l和r,求这个区间内不同数字的个数。 【输入数据】 第一行两个数n和m。 接下来一行n个数。 接下来m行,每行两个数l和r。 【输出数据】 m行,对应相应的询问。 【输入样例】 6 1 2 3 4 3 5 3 1 2 3 5 2 6 【输出样例】 2 2 4这道题就是很明显的不同的建树方式。
这道题的思路是什么呢?
这个区间只有\(r\)个数的话,这道题很明显会简单不少,毕竟如果这个数字是最后出现的话,就给他设为\(1\),否则为\(0\),然后前缀和一下。
估计这样的题目大部分人都会,但是放到这里就不会了。
前面的主席树都是帮助我们把区间提出来,这里不一样了,是在确定\(r\)的情况下\(logn\)的时间求前缀和。
也就是说假设这个点的权值出现过,那么我们在这棵主席树不仅要将这个点设为\(1\),还要去前面设为\(0\)。
这不就好起来了吗?
#include<cstdio> #include<cstring> #define N 51000 #define M 1100000 using namespace std; struct node {int lc,rc,c; }tr[M];int len=0,rt[N]; void link(int &now,int l,int r,int c,int k) {if(!now)now=++len;tr[now].c+=k;if(l==r)return ;int mid=(l+r)/2;if(c<=mid)link(tr[now].lc,l,mid,c,k);else link(tr[now].rc,mid+1,r,c,k); } void merge(int &x,int y) {if(!x){x=y;return ;}if(!y)return ;tr[x].c+=tr[y].c;merge(tr[x].lc,tr[y].lc);merge(tr[x].rc,tr[y].rc); } int calc(int now,int x,int l,int r) {if(l==r)return tr[now].c&(l==x);int mid=(l+r)/2;if(x<=mid)return calc(tr[now].lc,x,l,mid);else return calc(tr[now].rc,x,mid+1,r)+tr[tr[now].lc].c; } int mp[M]; int n,m; int main() {scanf("%d",&n);for(int i=1;i<=n;i++){int x;scanf("%d",&x);if(mp[x]){link(rt[i],1,n,mp[x],-1);link(rt[i],1,n,i,1);merge(rt[i],rt[i-1]);//有过这个点}else{link(rt[i],1,n,i,1);merge(rt[i],rt[i-1]);//新点}mp[x]=i;}scanf("%d",&m);for(int i=1;i<=m;i++){int l,r;scanf("%d%d",&l,&r);printf("%d\n",calc(rt[r],r,1,n)-calc(rt[r],l-1,1,n));//1-r的区间}return 0; }简单询问
时间限制: 2 Sec 内存限制: 128 MB 【题目描述】 给你n(1<=n<=100000)个数,并给出m(1<=m<=100000)个询问,每次给出(x,y,k),就是让你求x到y这个区间,有多少个数小于等于k。 【输入数据】 第一行两个数n,m。 第二行有n个数。 接下来一行,每行一个询问。 【输出数据】 m行,每行对应一个询问。 【输入样例】 10 10 0 5 2 7 5 4 3 8 7 7 3 9 6 4 5 0 2 4 1 2 10 4 1 2 0 4 6 5 6 6 1 5 7 3 2 6 7 6 8 3 【输出样例】 4 0 0 3 1 2 0 1 5 1弱智题,直接跳权值主席树不就行了?
#include<cstdio> #include<cstring> #include<algorithm> #define N 110000 #define M 3100000 using namespace std; struct node {int l,r,c; }tr[M];int root[N],cnt; int rt[N]; struct LSnode {int x,y; }ls[N]; int n,m; bool cmp(LSnode x,LSnode y){return x.x<y.x;}//离散化排序 void add(int &x,int l,int r,int k) {if(x==0)x=++cnt;tr[x].c++;if(l==r)return ;int mid=(l+r)/2;if(k<=mid)add(tr[x].l,l,mid,k);else add(tr[x].r,mid+1,r,k); } void merge(int &x,int y) {if(x==0){x=y;return ;}if(y==0)return ;tr[x].c+=tr[y].c;merge(tr[x].l,tr[y].l);merge(tr[x].r,tr[y].r); } int cale(int x,int y,int l,int r,int k) {if(l==r)return (tr[y].c-tr[x].c)&(l<k);//有一种情况是k>n,所以这里相当于一次特判int mid=(l+r)/2;if(k<=mid)return cale(tr[x].l,tr[y].l,l,mid,k);else return cale(tr[x].r,tr[y].r,mid+1,r,k)+(tr[tr[y].l].c-tr[tr[x].l].c); } int erfen(int x)//求在原数列中x的前驱的离散化值 {int l=1,r=n,mid,ans=-1;while(l<=r){mid=(l+r)/2;if(ls[mid].x<=x)ans=mid,l=mid+1;else r=mid-1;}return ans+1; } int main() { // freopen("1.in","r",stdin); // freopen("1.out","w",stdout);scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){scanf("%d",&ls[i].x);ls[i].y=i;}sort(ls+1,ls+n+1,cmp);for(int i=1;i<=n;i++)rt[ls[i].y]=i;//离散化QMQfor(int i=1;i<=n;i++){add(root[i],1,n,rt[i]);merge(root[i],root[i-1]);}for(int i=1;i<=m;i++){int x,y,k;scanf("%d%d%d",&x,&y,&k);if(x>y)x^=y^=x^=y;printf("%d\n",cale(root[x-1],root[y],1,n,erfen(k)));}return 0; }简单查询
时间限制: 2 Sec 内存限制: 1024 MB 【题目描述】 给一个长度为n(1<=n<=100000)的序列a1,a2,a3,......,an(1<=ai<=100000)。 m(1<=m<=100000)组询问,每次询问一个区间[l,r]。 询问是否存在一个数在[l,r]中出现的次数大于(r-l+1)/2。如果存在,输出这个数,否则输出0。 【输入数据】 第一行两个数n,m。 第二行n个数。 接下来m行,每行两个数l,r,表示询问[l,r]这个区间。 【输出数据】 m行,每行对应一个询问的答案。 【输入样例】 7 5 1 1 3 2 3 4 3 1 3 1 4 3 7 1 7 6 6 【输出样例】 1 0 3 0 4一样权值主席树
往下跳看左子树有木有满足条件,满足去左儿子,不满足看右儿子,这种数字在区间内最多只有一个,左右儿子都没有直接输出\(false\)
#include<cstdio> #include<cstring> #define N 110000 #define M 3100000 using namespace std; int xnum=0; inline int mymax(int x,int y){return x>y?x:y;} struct node {int lc,rc,c; }tr[M];int len=0,rt[N]; void link(int &now,int l,int r,int c) {if(!now)now=++len;tr[now].c++;if(l==r)return ;int mid=(l+r)/2;if(c<=mid)link(tr[now].lc,l,mid,c);else link(tr[now].rc,mid+1,r,c); } void merge(int &x,int y) {if(!x){x=y;return ;}if(!y)return ;tr[x].c+=tr[y].c;merge(tr[x].lc,tr[y].lc);merge(tr[x].rc,tr[y].rc); } int calc(int x,int y,int l,int r,int k) {if(l==r)return l;int mid=(l+r)>>1;if(tr[tr[y].lc].c-tr[tr[x].lc].c>k)calc(tr[x].lc,tr[y].lc,l,mid,k);//左儿子符不符合条件?else if(tr[tr[y].rc].c-tr[tr[x].rc].c>k)calc(tr[x].rc,tr[y].rc,mid+1,r,k);//右儿子符不符合条件?else return 0;//都不符合 } int n,m,a[N]; int main() {scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){scanf("%d",&a[i]);xnum=mymax(xnum,a[i]);}for(int i=1;i<=n;i++)link(rt[i],1,xnum,a[i]),merge(rt[i],rt[i-1]);for(int i=1;i<=m;i++){int l,r;scanf("%d%d",&l,&r);printf("%d\n",calc(rt[l-1],rt[r],1,xnum,(r-l+1)>>1));}return 0; }矩阵主席树大暴力!!!
时间限制: 1 Sec 内存限制: 128 MB 【问题描述】 给一个n*n的矩阵,给m个询问,每次询问(x1,y1)到(x2,y2)之间的第k小值。 这几天做题遇到了一道这样的题,原题是国家集训队梁盾的《矩阵乘法》,而原题必须要用整体二分,这对于一个天天写主席树的我非常痛苦,于是强迫症的出了道强制在线弱化版本。【输入文件】 第一行两个数N,Q,表示矩阵大小和询问组数; 接下来N行N列一共N*N个数,表示这个矩阵; 再接下来Q行每行5个数描述一个询问:x1,y1,x2,y2,k表示找到以(x1,y1)为左上角、以(x2,y2)为右下角的子矩形中的第K小数。 这一组的x1,y1,x2,y2,k需异或lastans才可得到,lastans指的是上一组的答案,初始时为0。【输出文件】 对于每组询问输出第K小的数。【输入样例】 2 2 2 1 3 4 1 2 1 2 1 0 0 3 3 2【输出样例】 1 3【数据范围】 100%的数据:N<=100,Q<=40000。 保证异或后的点位置正确提示 强制在线一开始看到这道题,慌张的一批,打算在预处理上做功夫,打了个代码,中途想起来合并了多次会炸掉,重新理清思路,发现\(n\)很小,也就是说我们可以在询问上打暴力!
\((i,j)\)的主席树表示的是在第\(j\)列中,\(1-i\)行的数字集合。(也是权值主席树)
那么我们在查询\((x1,y1),(x2,y2)\)的时候,将\(y1->y2\)列的\(x2\)行主席树全部弄到一个\(list\)里面,表示加法的主席树,而将\(x1\)行的主席树丢到另外一个\(list\)里面,表示的是减法的主席树,然后每一层查询的时候暴力查,加个离散化,时间复杂度:\((Qnlogn)\)(注:\(logn^{2}=2logn\),所以在这里省略了\(2\))。
//在校园网上还能拿次优解,笑死我了 #include<cstdio> #include<cstring> #include<algorithm> #define N 110 #define NN 11000 #define M 1100000 using namespace std; inline int mymax(int x,int y){return x>y?x:y;} struct node {int lc,rc,c; }tr[M];int len=0,rt[N][N]; void link(int &now,int l,int r,int c) {if(!now)now=++len;tr[now].c++;if(l==r)return ;int mid=(l+r)/2;if(c<=mid)link(tr[now].lc,l,mid,c);else link(tr[now].rc,mid+1,r,c); } void merge(int &x,int y) {if(!x){x=y;return ;}if(!y)return ;tr[x].c+=tr[y].c;merge(tr[x].lc,tr[y].lc);merge(tr[x].rc,tr[y].rc); } int li1[N],li2[N],top; inline void turn(int x) {if(x==0)for(int i=1;i<=top;i++)li1[i]=tr[li1[i]].lc,li2[i]=tr[li2[i]].lc;else for(int i=1;i<=top;i++)li1[i]=tr[li1[i]].rc,li2[i]=tr[li2[i]].rc; } int calc(int l,int r,int k) {if(l==r)return l;int mid=(l+r)/2,zz=0;for(int i=1;i<=top;i++)zz+=tr[tr[li1[i]].lc].c-tr[tr[li2[i]].lc].c;//暴力计算if(zz>=k){turn(0);return calc(l,mid,k);}else{turn(1);return calc(mid+1,r,k-zz);} } //主席树 int n,m,nn,a[NN]; int id[NN],zhi[NN],cnt,ys[NN]; inline int num(int x,int y){return (x-1)*n+y-1;} inline bool cmp(int x,int y){return a[x]<a[y];} int main() {scanf("%d%d",&n,&m);nn=n*n-1;for(int i=0;i<=nn;i++){scanf("%d",&a[i]);id[i]=i;}sort(id,id+nn+1,cmp);a[0]=-999999999;for(int i=0;i<=nn;i++)zhi[id[i]]=(a[id[i]]==a[id[i-1]]?cnt:(ys[cnt+1]=a[id[i]],++cnt));for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)link(rt[i][j],1,cnt,zhi[num(i,j)]),merge(rt[i][j],rt[i-1][j]);//向上合并}int lastans=0;for(int i=1;i<=m;i++){int x1,y1,x2,y2,k;scanf("%d%d%d%d%d",&x1,&y1,&x2,&y2,&k);x1^=lastans;x2^=lastans;y1^=lastans;y2^=lastans;k^=lastans;top=0;for(int j=y1;j<=y2;j++)li1[++top]=rt[x2][j],li2[top]=rt[x1-1][j];//将要用到的主席树塞进去lastans=ys[calc(1,cnt,k)];printf("%d\n",lastans);}return 0; }动态主席树
讲解
例题
时间限制: 1 Sec 内存限制: 256 MB 【问题描述】 给n(1<=n<=50000)个数字,进行m(1<=m<=10000)次操作,有两种操作: Q l r k:询问l到r第k小的数。 C x k:改变第x个数的值为k。 【输入文件】 第一行为n和m。 接下来一行n个数。 接下来m行为m个操作。 【输出文件】 遇到Q操作就输出。 【输入样例】 5 3 1 2 3 4 5 Q 2 5 2 C 4 9 Q 3 5 3 【输出样例】 3 9算法讲解
静态主席树就跟前缀和一样,所以我们要修改就会很咕咕。
这是就有大佬瞬间想到,树套树不就行了?
对,就是树套树,树状数组套主席树。
但是静态主席树也要保留,不如说树套树保存的是修改的结果。
保留静态主席树是为了空间更小一点,毕竟树套树的空间一次就是\(log^{2}n\)。
\(rt\)根数组要开两倍,一个是静态的,一个是与树状数组每个节点一一对应的。
而树状数组每个节点的权值主席树维护的则是树状数组维护区域的数字。
当修改了\(i\)的值的时候,我们就在树状数组上查找维护\(i\)的节点,然后进入到主席树中,将原来的数字的\(c\)减去\(1\),将现在的数字增加\(1\)。
同时查询的时候,用前缀和的方法,在树状数组中得出这个区间目前增加或减少了多少个数字。
更多的细节在代码中标记出来了。
代码
#include<cstdio> #include<cstring> #define N 51000 #define NN 110000 #define INF 10000000 #define M 5100000 using namespace std; int n,m; struct node {int lc,rc,c; }tr[M];int len; int a[N],rt[NN]; void link(int &x,int l,int r,int c,int k) {if(!x)x=++len;tr[x].c+=k;if(l==r)return ;int mid=(l+r)/2;if(c<=mid)link(tr[x].lc,l,mid,c,k);else link(tr[x].rc,mid+1,r,c,k); } inline int lowbit(int x){return x&-x;} int list[N],top,hehe[N],ti/*时间戳*/; inline void turn(int id)//细节1:为了在calc中跟着他一起跳设计的函数。 {for(int i=1;i<=top;i++){int x=list[i];if(id==0)a[x]=rt[x+n];else if(id==1)a[x]=tr[a[x]].lc;else if(id==2)a[x]=tr[a[x]].rc;} } inline void tiao(int x) {while(x){if(hehe[x]!=ti)list[++top]=x;hehe[x]=ti;/*这个点有木有跳过?*/x-=lowbit(x);} } void change(int x,int c,int k) {while(x<=n){link(rt[x+n],0,INF,c,k);x+=lowbit(x);} } int getsum(int x) {int ans=0;while(x){ans+=tr[tr[a[x]].lc].c;x-=lowbit(x);}return ans; } int calc(int x,int y,int ll,int rr,int l,int r,int k) {if(l==r)return l;int zhe=tr[tr[x].lc].c-tr[tr[y].lc].c+getsum(rr)-getsum(ll),mid=(l+r)/2;if(zhe<k){turn(2);return calc(tr[x].rc,tr[y].rc,ll,rr,mid+1,r,k-zhe);}else{turn(1);return calc(tr[x].lc,tr[y].lc,ll,rr,l,mid,k);} } int zhi[N]; void merge(int &x,int y) {if(!x){x=y;return ;}if(!y)return ;tr[x].c+=tr[y].c;merge(tr[x].lc,tr[y].lc);merge(tr[x].rc,tr[y].rc); } int main() {scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){scanf("%d",&zhi[i]);link(rt[i],0,INF,zhi[i],1);merge(rt[i],rt[i-1]);}for(int i=1;i<=m;i++){char st[20];int x,y,z;scanf("%s",st+1);if(st[1]=='C'){scanf("%d%d",&x,&y);change(x,zhi[x],-1);zhi[x]=y;change(x,zhi[x],1);}else{scanf("%d%d%d",&x,&y,&z);if(x>y)x^=y^=x^=y;ti++;top=0;tiao(x-1);tiao(y);turn(0);printf("%d\n",calc(rt[y],rt[x-1],x-1,y,0,INF,z));}}return 0; }练习
不是动态主席树的带修题。
时间限制: 3 Sec 内存限制: 256 MB 【问题描述】 最近复习了一下带修主席树,于是出道水题给大家做,问题很简单: 1、询问区间k小值; 2、交换相邻两个数的权值。 【输入文件】 第一行两个数N,Q,表示序列大小和询问组数; 第一行输入n个数,表示原始序列。 接下来Q行, 当opt = 1时输入三个数 l,r,k询问l~r区间第k小的数 当opt = 2时输入一个数p 交换p和p + 1位置上的权值。 对于任何修改或询问都需异或一个lastans, lastans表示上一个答案,初始值为0。 【输出文件】 对于每组询问输出第K小的数。 【输入样例】 7 3 1 5 2 6 3 7 4 1 2 5 3 2 4 1 7 0 6 【输出样例】 5 3 【数据范围】 如果被卡常,请使用#pragma GCC optimize("Ofast") 100%的数据:N<=500000,Q<=500000。千万不要信出题人,我就打了动态主席树。。。(满嘴粗话...)
提一提:不用理\(x=n\)的情况。
我们会发现交换其实对后面的树是没有影响的,有影响的只是\(x\)号点的主席树而已。
所以我们只要修改他就行了。
这里提供三种思路:
可持久化主席树
很多大佬都会说主席树就是用来可持久化的,仔细想想也有道理,毕竟\(link\)的可操作性让他支持了可持久化。
例题
【题目描述】 给出一个长度为n(1<=n<=100000)的序列a[1],a[2],a[3]......,a[n](1<=a[i]<=1000000000),有m个操作(1<=m<=100000)。 操作类型分为4种。 1 l r k:给l到r之间的数都加上k(1<=k<=10000)。 2 l r:询问当前l到r的和。 3 l r h:询问第h次修改(修改只指1操作)时l到r的和。 4 h:回到第h次修改的时候不再返回。(意思:接下来的修改是建立在第h次修改的模型上) 【输入数据】 第一行两个数n和m(n,m<=10^5)。 接下来一行n个数。 接下来m行m个操作。 【输出数据】 每行对应一个询问。 【输入样例】 10 5 1 2 3 4 5 6 7 8 9 10 2 4 4 2 1 10 2 2 4 1 3 6 3 2 2 4 【输出样例】 4 55 9 15某有练习啦,博主太菜啦,做题太少了。
思路
单点修改
首先考虑给一个点加上\(k\)的话要怎么做?
在不修改以前的操作的话要保留现在操作的节点,我们不可能说新建一个主席树。
但是,我们想起来主席树有一种骚操作就是一个儿子可以是许多人的儿子。(绿~~)
那么,也就是说我们只需要把涉及到需要修改\(c\)的节点新建,然后不需要的节点直接指向原来的就行了。
LOOK,图中标着\(1,2\)的代表的就是每个时间代表的主席树的根节点。
区间修改
博主博主,区间修改莫不是一次次单点修改。你怎么这么聪明。
区间修改,你一看到这个你一定要跳出一句代码:
#define 区间修改 lazy我们也是一样的,把会改变的点复制出来,但是如果发现一个点的区间与目前的完全吻合,就打上懒标记,在单点查询的时候,再把查询时需要用到的点建出来,而区间查询也是如此,空间复杂度就是\(O((n+m)logc)\)(\(c\)为值域)。
但是,有更优秀的做法,当我们在打上标记的时候,查询时不建点,而是选择个东西存起来,让下面点知道有标记,但不等同于下传,只是在记录答案的时候用一下,当时我骄傲的跟机房大佬说就叫\(Monkey\) \(Lazy\),然后就被大佬各种石锤:“这TM明明是永久化标记。”,然后就被大佬D爆了,QAQ。
这种方法还有一个更NB的地方,回到第\(h\)次操作我们是可以直接等于到那次的\(len\)实现暴力删点,而前面那种做法就得很难受的打内存池了。
代码
总体思路是这样,但我写的挺丑的,仔细想想会发现有更优美的写法,所以我还是比同机房的大佬差了这么一点点的时间。
//怎么感觉主席树的代码怎么加都是这么短 #include<cstdio> #include<cstring> #define N 110000 #define M 3100000 using namespace std; typedef long long LL; struct node {int lc,rc;LL c,la; }tr[M];int len,rt[N],cnt/*目前有几次操作*/; int new_dian(){++len;tr[len].lc=tr[len].rc=tr[len].c=tr[len].la=0;return len;} LL ch;//你可以认为这是众多函数中的辅助变量 void link(int &now,LL c,int l,int r) {if(!now)now=new_dian();tr[now].c+=ch;if(l==r)return ;LL mid=(l+r)/2;if(c<=mid)link(tr[now].lc,c,l,mid);else link(tr[now].rc,c,mid+1,r); } void change(int &now,int x,int l,int r,int ll,int rr) {if(!now)now=new_dian(),tr[now].c=tr[x].c,tr[now].la=tr[x].la;tr[now].c+=(rr-ll+1)*ch/*要增加多少值*/;if(l==ll && r==rr){tr[now].la+=ch;tr[now].lc=tr[x].lc;tr[now].rc=tr[x].rc;return ;}int mid=(l+r)/2;if(rr<=mid)change(tr[now].lc,tr[x].lc,l,mid,ll,rr),tr[now].rc=tr[x].rc;else if(mid+1<=ll)change(tr[now].rc,tr[x].rc,mid+1,r,ll,rr),tr[now].lc=tr[x].lc;else change(tr[now].lc,tr[x].lc,l,mid,ll,mid),change(tr[now].rc,tr[x].rc,mid+1,r,mid+1,rr); } //记录答案 //这里其实是可以不用存在ch中的,可以直接(rr-ll+1)*lazy LL getsum(int now,int l,int r,int ll,int rr) {if(l==ll && r==rr)return tr[now].c+(r-l+1)*ch/*lazy非下传下传法*/;int mid=(l+r)/2;ch+=tr[now].la;if(rr<=mid)return getsum(tr[now].lc,l,mid,ll,rr);else if(mid+1<=ll)return getsum(tr[now].rc,mid+1,r,ll,rr);else{LL hehe=ch,ans=getsum(tr[now].lc,l,mid,ll,mid);ch=hehe;return getsum(tr[now].rc,mid+1,r,mid+1,rr)+ans;} } int n,m; int main() {scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){scanf("%lld",&ch);link(rt[0],i,1,n);}for(int i=1;i<=m;i++){int type,x,y,z;scanf("%d",&type);if(type==1){scanf("%d%d%lld",&x,&y,&ch);rt[++cnt]=0;change(rt[cnt],rt[cnt-1],1,n,x,y);}else if(type==2){scanf("%d%d",&x,&y);ch=0;printf("%lld\n",getsum(rt[cnt],1,n,x,y));}else if(type==3){scanf("%d%d%d",&x,&y,&z);ch=0;printf("%lld\n",getsum(rt[z],1,n,x,y));}else{scanf("%d",&x);if(x!=cnt)len=rt[x+1]-1;cnt=x;}}return 0; }小结
有人也许会问可持久化带修区间第\(k\)小的\(rt\)为什么要开两维,是不是我学了个假的可持久化,没有。博主很良心的
其实这个问题我也问了下机房的大佬,他是这么回答我的:
主席树支持可持久化原本就是因为他可以支持共用节点,比如\(FHQ\)就是一个活生生的例子。
而区间第\(k\)小原本就是利用了主席树的可持久化特性,用类似前缀和的方法做的,本身就相当于调用了主席树可持久化的性质,再套个可持久化不就是双重可持久化,那二维不也很正常嘛?
所以大佬还是大佬呀。
//怎么感觉主席树的代码怎么加都是这么短 #include<cstdio> #include<cstring> #define N 110000 #define M 3100000 using namespace std; typedef long long LL; struct node {int lc,rc;LL c,la; }tr[M];int len,rt[N],cnt/*目前有几次操作*/; int new_dian(){++len;tr[len].lc=tr[len].rc=tr[len].c=tr[len].la=0;return len;} LL ch;//你可以认为这是众多函数中的辅助变量 void link(int &now,LL c,int l,int r) {if(!now)now=new_dian();tr[now].c+=ch;if(l==r)return ;LL mid=(l+r)/2;if(c<=mid)link(tr[now].lc,c,l,mid);else link(tr[now].rc,c,mid+1,r); } void change(int &now,int x,int l,int r,int ll,int rr) {if(!now)now=new_dian(),tr[now].c=tr[x].c,tr[now].la=tr[x].la;tr[now].c+=(rr-ll+1)*ch/*要增加多少值*/;if(l==ll && r==rr){tr[now].la+=ch;tr[now].lc=tr[x].lc;tr[now].rc=tr[x].rc;return ;}int mid=(l+r)/2;if(rr<=mid)change(tr[now].lc,tr[x].lc,l,mid,ll,rr),tr[now].rc=tr[x].rc;else if(mid+1<=ll)change(tr[now].rc,tr[x].rc,mid+1,r,ll,rr),tr[now].lc=tr[x].lc;else change(tr[now].lc,tr[x].lc,l,mid,ll,mid),change(tr[now].rc,tr[x].rc,mid+1,r,mid+1,rr); } //记录答案 //这里其实是可以不用存在ch中的,可以直接(rr-ll+1)*lazy LL getsum(int now,int l,int r,int ll,int rr) {if(l==ll && r==rr)return tr[now].c+(r-l+1)*ch/*lazy非下传下传法*/;int mid=(l+r)/2;ch+=tr[now].la;if(rr<=mid)return getsum(tr[now].lc,l,mid,ll,rr);else if(mid+1<=ll)return getsum(tr[now].rc,mid+1,r,ll,rr);else{LL hehe=ch,ans=getsum(tr[now].lc,l,mid,ll,mid);ch=hehe;return getsum(tr[now].rc,mid+1,r,mid+1,rr)+ans;} } int n,m; int main() {scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){scanf("%lld",&ch);link(rt[0],i,1,n);}for(int i=1;i<=m;i++){int type,x,y,z;scanf("%d",&type);if(type==1){scanf("%d%d%lld",&x,&y,&ch);rt[++cnt]=0;change(rt[cnt],rt[cnt-1],1,n,x,y);}else if(type==2){scanf("%d%d",&x,&y);ch=0;printf("%lld\n",getsum(rt[cnt],1,n,x,y));}else if(type==3){scanf("%d%d%d",&x,&y,&z);ch=0;printf("%lld\n",getsum(rt[z],1,n,x,y));}else{scanf("%d",&x);if(x!=cnt)len=rt[x+1]-1;cnt=x;}}return 0; }小结
有人也许会问可持久化带修区间第\(k\)小的\(rt\)为什么要开两维,是不是我学了个假的可持久化,没有。博主很良心的
其实这个问题我也问了下机房的大佬,他是这么回答我的:
主席树支持可持久化原本就是因为他可以支持共用节点,比如\(FHQ\)就是一个活生生的例子。
而区间第\(k\)小原本就是利用了主席树的可持久化特性,用类似前缀和的方法做的,本身就相当于调用了主席树可持久化的性质,再套个可持久化不就是双重可持久化,那二维不也很正常嘛?
所以大佬还是大佬呀。
查错
其实之前的话存在一点错误。
不管你用什么方法建树,比如从前往后合并,或者是两个两个合并,只要区间永远不重复就可以了,并且只合并\(n-1\)次,并且每个点只被合并给别人一次,我们就可以保证时间复杂度是\(O(nlogn)\)的。
为什么,我们考虑进入一点的有效情况(两个主席树都有这个点),而我的证明只限于一种情况,就是当集合合并到另外一个大的集合时,以后都只有这个大集合带着这个小集合(或者一些特殊情况),所以以后这个大集合的被合并的话,只要这个点被有效访问,这个集合就会进一步变大,而一个点只要区域内的数字都被添加了,这个点就不会被有效访问,也就是说每层的点只会有效访问\(n\)次,\(log\)层,时间复杂度自然就是\(O(nlogn)\)。
但是,还是有无效访问的,常数比较小的自然还是从左往右合并啦。
转载于:https://www.cnblogs.com/zhangjianjunab/p/11346849.html
总结
- 上一篇: 算法学习:最小圆覆盖
- 下一篇: 【FJOI2015】最小覆盖双圆问题