【洛谷7470】[NOI Online 2021 提高组] 岛屿探险(线段树分治+Trie树)
- 有一个长度为\(n\)的序列,每个位置有两个属性\(a_i,b_i\)。
- \(q\)次询问,每次给定一个区间和两个属性\(c_j,d_j\),询问区间中有多少位置满足\((a_i\oplus c_j)\le\min\{b_i,d_j\}\)。
- \(n,q\le10^5,a,b,c,d<2^{24}\)
NOI Online的时候花了近三个小时肝\(T1\)的\(60\)分,导致这道题口胡出来却来不及写了,最后由于服务器爆炸连暴力都没来得及交上去。
结果没想到今天花了半个多小时就把这题写完了,而且过了样例后直接一遍过,如今想来真是血亏。
不过反正我开O2爆零了,似乎没什么区别?
线段树分治
首先由于这是一个区间询问,搞起来就非常麻烦。
显然需要离线转化一下,官方题解给出的是\(CDQ\)分治,但这还需要经过进一步推导。
因此,像我这种暴力选手就直接无脑上线段树分治了,这样一来就能把区间询问变成全局询问了。
但实测线段树分治应该比\(CDQ\)分治效率要低一些吧。
\(Trie\)树乱搞
反正我们已经把询问离线了,不如进一步离线,当枚举到线段树上一个节点的时候,把所有位置按照\(b_i\)排序,所有询问按照\(d_j\)排序。
然后我们双指针去枚举,就能对于每个询问,把位置划分为\(b_i<d_j\)和\(b_i\ge d_j\)的两部分,每个位置只会从第二部分进入第一部分一次,然后对于这两部分的询问分别去处理。
第一部分\((a_i\oplus c_j)\le b_i\),考虑在\(Trie\)树上表示出\(c_j\)可能的取值区间,询问时就可以直接求了。
因为比大小肯定比较最高的不同位,所以对于每一位我们只要假设先前的取值都相同,否则就已经比出结果了。
然后一种情况是它们仍旧取值相同,那么递归处理。
另一种情况是它们取值不同,如果\(b\)这一位是\(1\)则合法,否则反变成了\(a>b\)肯定不合法。因此只要在\(b\)这一位为\(1\)的时候给对应的子树打个永久化标记即可。
则一次询问就是求出从根到\(c_j\)所有点标记之和。
第二部分\((a_i\oplus c_j)\le d_j\),其实刚好和上面相反,我们在\(Trie\)树上插入\(a_i\),询问时去找它的可能区间。
同理考虑这一位的取值,如果相同则递归处理;如果不同且\(d\)这一位是\(1\),给答案加上对应子树的总和即可。
代码:\(O(nlognlogV)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define D 24
using namespace std;
int n,Qt,a[N+5],b[N+5],ans[N+5];I bool cmp(CI x,CI y) {return b[x]<b[y];}
struct Q {int p,c,d;I Q(CI i=0,CI x=0,CI y=0):p(i),c(x),d(y){}I bool operator < (Con Q& o) Con {return d<o.d;}};
namespace FastIO
{
#define FS 100000
#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
#define pc(c) (FC==FE&&(clear(),0),*FC++=c)
int OT;char oc,FI[FS],FO[FS],OS[FS],*FA=FI,*FB=FI,*FC=FO,*FE=FO+FS;
I void clear() {fwrite(FO,1,FC-FO,stdout),FC=FO;}
Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
Tp I void writeln(Ty x) {W(OS[++OT]=x%10+48,x/=10);W(OT) pc(OS[OT--]);pc('\n');}
}using namespace FastIO;
class Trie
{
private:
#define PU(x) (O[x].V2=O[O[x].S[0]].V2+O[O[x].S[1]].V2)
int Nt;struct node {int V1,V2,S[2];}O[N*D*3];
public:
I void Cl() {W(Nt) O[Nt].V1=O[Nt].V2=O[Nt].S[0]=O[Nt].S[1]=0,--Nt;}
I void A1(int& rt,CI a,CI b,CI v,CI p=D-1)//插入a,b表示出c的可能取值
{
if(!~p) return (void)(O[rt].V1+=v);!O[rt].S[0]&&(O[rt].S[0]=++Nt),!O[rt].S[1]&&(O[rt].S[1]=++Nt);//建好子节点
RI t=(a^b)>>p&1;b>>p&1&&(O[O[rt].S[t^1]].V1+=v),A1(O[rt].S[t],a,b,v,p-1),PU(rt);//如果b这位为1可以不同,相同情况递归处理
}
I int Q1(CI rt,CI c,CI p=D-1)//询问根到c的标记总和
{
if(!rt||!~p) return O[rt].V1;return O[rt].V1+Q1(O[rt].S[c>>p&1],c,p-1);
}
I void A2(int& rt,CI a,CI v,CI p=D-1)//单纯地插入a
{
if(!rt&&(rt=++Nt),O[rt].V2+=v,!~p) return;A2(O[rt].S[a>>p&1],a,v,p-1),PU(rt);
}
I int Q2(CI rt,CI c,CI d,CI p=D-1)//询问c,d对应的a的取值
{
if(!rt||!~p) return O[rt].V2;RI t=(c^d)>>p&1;return (d>>p&1?O[O[rt].S[t^1]].V2:0)+Q2(O[rt].S[t],c,d,p-1);//d这位为1可以不同,相同情况递归处理
}
}T;
class SegmentTree
{
private:
#define PT CI l=1,CI r=n,CI rt=1
#define LT l,mid,rt<<1
#define RT mid+1,r,rt<<1|1
int id[N+5];vector<Q> V[N<<2];vector<Q>::iterator it;
public:
I void K(CI L,CI R,Con Q& q,PT)//把询问扔到线段树上
{
if(L<=l&&r<=R) return (void)V[rt].push_back(q);RI mid=l+r>>1;
L<=mid&&(K(L,R,q,LT),0),R>mid&&(K(L,R,q,RT),0);
}
I void Work(PT)//线段树分治,化区间询问为全局询问
{
RI i,Rt=0;for(T.Cl(),i=l;i<=r;++i) T.A2(Rt,a[id[i-l+1]=i],1);sort(id+1,id+r-l+2,cmp);//初始所有b[i]≥d[j]
for(sort(V[rt].begin(),V[rt].end()),i=1,it=V[rt].begin();it!=V[rt].end();++it)//枚举所有询问
{
W(i<=r-l+1&&b[id[i]]<it->d) T.A1(Rt,a[id[i]],b[id[i]],1),T.A2(Rt,a[id[i++]],-1);//双指针
ans[it->p]+=T.Q1(Rt,it->c)+T.Q2(Rt,it->c,it->d);//分两部分各自询问
}
if(l^r) {RI mid=l+r>>1;Work(LT),Work(RT);}//递归
}
}S;
int main()
{
RI i;for(read(n,Qt),i=1;i<=n;++i) read(a[i],b[i]);
RI l,r,x,y;for(i=1;i<=Qt;++i) read(l,r,x,y),S.K(l,r,Q(i,x,y));S.Work();//把询问扔到线段树上离线
for(i=1;i<=Qt;++i) writeln(ans[i]);return clear(),0;
}