• [NOI Online 2021 Ⅰ 提高组题解] 岛屿探险


    题意简述

    一个长度为 (n) 的序列,每一个位置 (i) 有两个属性值 (a_i,b_i)

    (q) 次询问,形如 (l,r,c,d) ,查询有多少个 (i) 满足 (iin [l,r],(a_ioplus c)le min{b_i,d}) ,其中 (oplus) 为按位异或运算。

    (1le n,qle 10^5 , 1le lle rle n , 0le a_i,b_i,c,d<2^{24})

    算法分析

    首先暴力特别好写,复杂度 (mathcal O(nq)) (然后就可以用指令集优化了,暴力碾标算)

    先考虑一个部分分,(max{d}le min{b_i}),即我们只需要考虑 (d) 的限制。这是一道原题

    限制是异或,因此考虑按位分析。我们从高位到低位遍历,考虑第 (p) 位,分两种情况:

    1. (d_p=0) ,此时如果在考虑的范围内 ((a_ioplus c)_p=1),则一定不合法,即必须满足 ((a_ioplus c)_p=0) 但仍然不确定,因此需继续遍历低位;
    2. (d_p=1) ,此时如果在考虑的范围内 ((a_ioplus c)_p=0),则一定合法,无需继续遍历,否则不确定,需继续遍历低位。

    这种限制条件很容易用 0-1 trie 表示出,即:

    图片.png

    (a_i) 插入 trie 即可,为了处理 (iin [l,r]) 的限制,要使用可持久化 trie ,时空复杂度均为 (mathcal O(nlog n))

    再考虑第二个部分分,(max{b_i}le min{d}),即我们只需要考虑 (b_i) 的限制。此时插入的有两个限制,询问只有一个限制,因此考虑反过来计算贡献,如下分析:

    继续从高位到低位遍历,考虑第 (p) 位,分两种情况:

    1. (b_p=0) ,此时如果在考虑的范围内询问的 (c) 会使得 ((a_ioplus c)_p=1) 成立,则一定不合法,即必须满足 ((a_ioplus c)_p=0) ,这个元素才有可能给询问 (c) 一个贡献,但仍然不确定,因此需继续遍历低位;
    2. (b_p=1) ,此时如果在考虑的范围内询问的 (c) 会使得 ((a_ioplus c)_p=0),则一定合法,一定有贡献,无需继续遍历,否则不确定是否产生贡献,需继续遍历低位。

    这种限制条件是类似的,也可以用 0-1 trie 表示出,即:

    图片.png

    其中黄色节点表示此处有贡献,最后询问的时候把经过的所有点的贡献相加即为答案。

    同样使用可持久化 trie 维护下标偏序关系,时空复杂度均为 (mathcal O(nlog n))

    两者结合,我们很难同时考虑两个限制。不妨找到一个分界,前面的所有元素满足 (b_i<d) ,后面的所有元素满足 (b_i>d) 。我们按照 (b,d) 升序排序后 two pointers 扫一遍即可求解。

    但是由于排序的存在,我们无法继续沿用可持久化 trie 的方法处理下标偏序关系。由于信息可加可减,因此有如下两种解决方案:

    1. 树状数组套 trie ,时空复杂度均为 (mathcal O(nlog^2 n)),空间卡不过去。
    2. cdq分治trie,时间复杂度 (mathcal O(nlog^2 n)) ,空间复杂度 (mathcal O(nlog n)),可以卡过去。

    树状数组套 trie 直接维护 (n)trie,外层树状数组维护其维护区间的所有元素的 trie 的根,修改直接暴力插入即可,比较直观,也比较无脑。

    cdq分治trie,在外层把下标限制拆开,按照下标排序,内层再按照 (b_i/d) 排序,左边修改对右边询问产生贡献,实现起来有一些细节。

    代码实现

    树状数组套 trie,(luogu民间数据只有20分,沦为暴力……)

    #include<bits/stdc++.h>
    using namespace std;
    #define maxn 1000005
    #define maxm 20000005
    #define inf 0x3f3f3f3f
    #define LL long long
    #define ull unsigned long long
    #define ldb long double
    #define mod 1000000007
    #define eps 1e-9
    #define local
    void file(string s){freopen((s+".in").c_str(),"r",stdin);freopen((s+".out").c_str(),"w",stdout);}
    template <typename Tp> void read(Tp &x){
    	int fh=1;char c=getchar();x=0;
    	while(c>'9'||c<'0'){if(c=='-'){fh=-1;}c=getchar();}
    	while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c&15);c=getchar();}x*=fh;
    }
    int n,m;
    struct node{
    	int id,a,b;
    	bool operator <(node y)const{
    		return b<y.b;
    	}
    };
    struct Quer{
    	int id,l,r,c,d;
    	bool operator <(Quer y)const{
    		return d<y.d;
    	}
    };
    node ai[maxn];
    Quer qi[maxn];
    int ans[maxn];
    
    namespace Sub1{//d<b  chk d
    	int ch[maxm][2],siz[maxm],T[maxn],tot;
    	void upd(int rt,int x,int v){//情况1的插入
    		int p=rt,dir;
    		for(int i=23;~i;--i){
    			dir=((x>>i)&1);
    			if(!ch[p][dir])ch[p][dir]=++tot;
    			p=ch[p][dir];
    			siz[p]+=v;//记录个数
    		}
    	}
    	int query(int rt,int c,int d){//情况1的查询
    		++d;//这里要+1,因为我们上面的分析过程没有计算本身,这样写能减少边界细节
    		int ret=0,p=rt,dc,dd;
    		for(int i=23;~i;--i){
    			dc=((c>>i)&1);dd=((d>>i)&1);
    			if(dd){
    				ret+=siz[ch[p][dc]];//累加确定的个数
    				p=ch[p][dc^1];
    			}
    			else p=ch[p][dc];
    			if(!p)break;
    		}
    		return ret;
    	}
    	void add(int x,int a,int v){//树套树修改
    		for(;x<=n;x+=(x&(-x))){
    			upd(T[x],a,v);
    		}
    	}
    	int ask(int x,int c,int d){//树套树查询
    		int ret=0;
    		for(;x;x-=(x&(-x)))ret+=query(T[x],c,d);
    		return ret;
    	}
    	void prep(){
    		for(int i=1;i<=n;++i)T[i]=++tot;
    		for(int i=1;i<=n;++i)add(ai[i].id,ai[i].a,1);
    	}
    }
    
    namespace Sub2{// d>b chk b
    	int ch[maxm][2],siz[maxm],T[maxn],tot;
    	void upd(int rt,int a,int b,int v){//情况2的插入
    		++b;//这个细节与上面的同理
    		int p=rt,da,db;
    		for(int i=23;~i;--i){
    			da=((a>>i)&1);db=((b>>i)&1);
    			if(db){
    				if(!ch[p][da])ch[p][da]=++tot;
    				if(!ch[p][da^1])ch[p][da^1]=++tot;
    				siz[ch[p][da]]+=v;//记录贡献
    				p=ch[p][da^1];
    			}
    			else{
    				if(!ch[p][da])ch[p][da]=++tot;
    				p=ch[p][da];
    			}
    		}
    	}
    	int query(int rt,int c){//情况2的查询
    		int p=rt,ret=0,dir;
    		for(int i=23;~i;--i){
    			dir=((c>>i)&1);
    			if(!ch[p][dir])break;
    			p=ch[p][dir];ret+=siz[p];//累加贡献
    		}
    		return ret;
    	}
    	void add(int x,int a,int b,int v){
    		for(;x<=n;x+=(x&(-x))){
    			upd(T[x],a,b,v);
    		}
    	}
    	int ask(int x,int c){
    		int ret=0;
    		for(;x;x-=(x&(-x)))ret+=query(T[x],c);
    		return ret;
    	}
    	void prep(){
    		for(int i=1;i<=n;++i)T[i]=++tot;
    	}
    }
    //其实两个情况的代码十分类似,第一个的插入与第二个的询问几乎一样,第一个的询问与第二个的插入几乎一样,
    signed main(){
    	#ifndef local
    		file("island");
    	#endif
    	read(n);read(m);
    	for(int i=1;i<=n;++i){
    		read(ai[i].a);read(ai[i].b);ai[i].id=i;
    	}
    	for(int i=1;i<=m;++i){
    		read(qi[i].l);read(qi[i].r);read(qi[i].c);read(qi[i].d);qi[i].id=i;
    	}
    	sort(ai+1,ai+n+1);
    	sort(qi+1,qi+m+1);
    	
    	Sub1::prep();
    	Sub2::prep();
    	for(int i=1,j=1;i<=m;++i){
    		while(j<=n&&ai[j].b<qi[i].d){//two pointers扫描
    			Sub1::add(ai[j].id,ai[j].a,-1);
    			Sub2::add(ai[j].id,ai[j].a,ai[j].b,1);
    			++j;
    		}
    		int t1=Sub1::ask(qi[i].r,qi[i].c,qi[i].d);
    		int t2=Sub1::ask(qi[i].l-1,qi[i].c,qi[i].d);
    		int t3=Sub2::ask(qi[i].r,qi[i].c);
    		int t4=Sub2::ask(qi[i].l-1,qi[i].c);
    		ans[qi[i].id]=(t1-t2)+(t3-t4);//代价相减,统计总和
    	}
    	for(int i=1;i<=m;++i)printf("%d
    ",ans[i]);
    	
    	fclose(stdin);
    	fclose(stdout);
    	return 0;
    }
    

    cdq分治trie(luogu民间数据能 AC)

    #include<bits/stdc++.h>
    using namespace std;
    #define maxn 1000005
    #define maxm 10000005
    #define inf 0x3f3f3f3f
    #define LL long long
    #define ull unsigned long long
    #define ldb long double
    #define mod 1000000007
    #define eps 1e-9
    #define local
    void file(string s){freopen((s+".in").c_str(),"r",stdin);freopen((s+".out").c_str(),"w",stdout);}
    template <typename Tp> void read(Tp &x){
    	int fh=1;char c=getchar();x=0;
    	while(c>'9'||c<'0'){if(c=='-'){fh=-1;}c=getchar();}
    	while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c&15);c=getchar();}x*=fh;
    }
    int n,m,nm;
    
    struct Oper{
    	int id,ps,typ,a,b,c,d;//为了能够cdq,这里做了比较大的改版
    	bool operator <(Oper y)const{
    		if(ps==y.ps){
    			if(!typ||!y.typ)return !typ;
    			else return typ<y.typ;
    		}
    		else return ps<y.ps;
    	}
    };
    bool cmp1(Oper x,Oper y){
    	return x.b<y.b;
    }
    Oper ai[maxn];
    
    int ans[maxn];
    
    namespace Sub1{//d<b  chk d
    	int ch[maxm][2],siz[maxm],RT,tot;
    	void upd(int x,int v){
    		int p=RT,dir;
    		for(int i=23;~i;--i){
    			dir=((x>>i)&1);
    			if(!ch[p][dir])ch[p][dir]=++tot;
    			p=ch[p][dir];
    			siz[p]+=v;
    		}
    	}
    	int query(int c,int d){
    		++d;
    		int ret=0,p=RT,dc,dd;
    		for(int i=23;~i;--i){
    			dc=((c>>i)&1);dd=((d>>i)&1);
    			if(dd){
    				ret+=siz[ch[p][dc]];
    				p=ch[p][dc^1];
    			}
    			else p=ch[p][dc];
    			if(!p)break;
    		}
    		return ret;
    	}
    	void prep(){RT=tot=1;}
    }
    
    namespace Sub2{// d>b chk b
    	int ch[maxm][2],siz[maxm],RT,tot;
    	void upd(int a,int b,int v){
    		++b;
    		int p=RT,da,db;
    		for(int i=23;~i;--i){
    			da=((a>>i)&1);db=((b>>i)&1);
    			if(db){
    				if(!ch[p][da])ch[p][da]=++tot;
    				if(!ch[p][da^1])ch[p][da^1]=++tot;
    				siz[ch[p][da]]+=v;
    				p=ch[p][da^1];
    			}
    			else{
    				if(!ch[p][da])ch[p][da]=++tot;
    				p=ch[p][da];
    			}
    		}
    	}
    	int query(int c){
    		int p=RT,ret=0,dir;
    		for(int i=23;~i;--i){
    			dir=((c>>i)&1);
    			if(!ch[p][dir])break;
    			p=ch[p][dir];ret+=siz[p];
    		}
    		return ret;
    	}
    	void prep(){RT=tot=1;}
    }
    //两个情况与树套树做法几乎一模一样,唯一的不同是只有一棵树,没有外层的树状数组结构
    void cdq(int l,int r){
    	if(l==r)return;
    	int mid=((l+r)>>1);
    	cdq(l,mid);cdq(mid+1,r);
    	sort(ai+l,ai+mid+1,cmp1);sort(ai+mid+1,ai+r+1,cmp1);//这里可以写归并优化常数
    	for(int i=l;i<=mid;++i){
    		if(!ai[i].typ){
    			Sub1::upd(ai[i].a,1);
    		}
    	}
    	int j=l;
    	for(int i=mid+1;i<=r;++i){
    		while(j<=mid&&ai[j].b<ai[i].d){//同样 two pointers计算贡献
    			if(!ai[j].typ){
    				Sub1::upd(ai[j].a,-1);
    				Sub2::upd(ai[j].a,ai[j].b,1);
    			}
    			++j;
    		}
    		if(ai[i].typ){
    			int t1=Sub1::query(ai[i].c,ai[i].d);
    			int t2=Sub2::query(ai[i].c);
    			ans[ai[i].id]+=ai[i].typ*(t1+t2);//因为询问的[l,r]拆解,这里要乘上系数
    		}
    	}
    	for(int i=l;i<j;++i){//记得 全 部 清 空
    		if(!ai[i].typ){
    			Sub2::upd(ai[i].c,ai[i].d,-1);
    		}
    	}
    	for(int i=j;i<=mid;++i){
    		if(!ai[i].typ){
    			Sub1::upd(ai[i].a,-1);
    		}
    	}
    }
    
    signed main(){
    	#ifndef local
    		file("island");
    	#endif
    	read(n);read(m);
    	for(int i=1,a,b;i<=n;++i){
    		read(a);read(b);
    		ai[++nm]=(Oper){i,i,0,a,b,a,b};
    	}
    	for(int i=1,l,r,c,d;i<=m;++i){
    		read(l);read(r);read(c);read(d);
    		ai[++nm]=(Oper){i,r,1,c,d,c,d};
    		ai[++nm]=(Oper){i,l-1,-1,c,d,c,d};
    	}
    	sort(ai+1,ai+nm+1);
    	Sub1::prep();Sub2::prep();
    	cdq(1,nm);
    	for(int i=1;i<=m;++i)printf("%d
    ",ans[i]);
    	
    	fclose(stdin);
    	fclose(stdout);
    	return 0;
    }
    
    
  • 相关阅读:
    django项目一:基于django2.2可重用登录与注册模块-图片验证码
    django项目一:基于django2.2可重用登录与注册模块-Django表单
    django项目一:基于django2.2可重用登录与注册模块-登录视图函数
    谈谈我的移动端rem适配方案
    微信公众号jssdk自定义分享,二次分享自定义失败解决技巧
    html-webpack-plugin的使用
    formData 无需form异步上传多个图片
    原生javascript跨浏览器常用事件处理
    apache+php+mysql运行环境
    MySQL企业级备份
  • 原文地址:https://www.cnblogs.com/ZigZagKmp/p/14587026.html
Copyright © 2020-2023  润新知