1:当急需一个数据结构,可以在无序的数列中查询第一个大于等于(或其他)一个数值(或其他)的元素,可以用线段树实现(^-^)。假如把序列分成两个区域P1和P2,那么如果存在一个大于等于x的元素,则这个元素一定会出现在P1或者P2中,线段树的工作就是查询P1和P2的最大值,如果P1的最大值大于等于x,那么就可以缩小搜索的范围,就在P1中找就行了,同理,如果P1的最大值小于x,P2的最大值大于等于x,那么就在P2中找。以此类推,不断地缩小搜索范围,就可以找出元素的下标。查询复杂度O(log(n)),建树复杂度O(n)。mx[p]存的是区间p的最大值。
void build(int p=1,int l=1,int r=n){
if(l==r){mx[p]=A[l];return;}
int mid=l+r>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
mx[p]=max(mx[p<<1],mx[p<<1|1]);
}
void query(int x,int p=1,int l=1,int r=n){
if(l==r)return l;
int mid=l+r>>1,ans=-1;
if(mx[p<<1]>=x)ans=query(x,p<<1,l,mid);
else if(mx[p<<1|1]>=x)ans=query(x,p<<1|1,mid+1,r);
return ans;
}
上面的线段树只能在[1,n]的范围内寻找第一个满足某条件的下标,当如果要在一段区间[a,b]内寻找第一个满足条件的下标时,那么就要升级一下线段树,mx[p]存的p区间最大值的下标。
void build(int p=1,int l=1,int r=n){
if(l==r){mx[p]=l;return;}
int mid=l+r>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
mx[p]=(A[mx[p<<1]]>=A[mx[p<<1|1]])?mx[p<<1]:mx[p<<1|1];
}
int query(int a,int b,int x,int p=1,int l=1,int r=n){
if(l>b||r<a)return 0;
if(l==r)return l;
int mid=l+r>>1,ans=0;
if(mx[p<<1]>=x)ans=query(a,b,x,p<<1,l,mid);
if(!ans&&mx[p<<1|1]>=x)ans=query(a,b,x,p<<1|1,mid+1,r);
return ans;
}
2:如果一个问题的点,满足在某种条件下进行跳跃,每个点跳跃一次后都能转换成一个固定的点,并且当问题需要知道一个点跳跃多次后的点时,可以用倍增法进行点的快速跳跃。用F[i][j]来表示j这个点跳2^i步后的点,所以只需求出每个点的F[0][j],就可以预处理出每个点跳2^i后的点。
for(int i=1;i<S;i++)for(int j=1;j<=n;j++)F[i][j]=F[i-1][F[i-1][j]];//预处理
当我们要跳step步时,可以用二进制判断step&(1<<i)是否为真,若为真则让这个点跳跃2^i步,就令x=F[i][x]。
for(int i=0;i<S;i++)if(step&(1<<i))x=F[i][x];//运用
也可以用位运算更快速地求出跳step步后的点。
while(step){//效率更高地运用
int op=step&-step;
op=mp[op];
x=F[op][x];
step^=op;
}
3:当一个问题中需要用到一种需要修改的前缀和时,可以用树状数组来维护。可以说树状数组是可以单点修改的前缀和,那么当遇到需要修改的前缀和时,就能用树状数组实现log(n)的修改,log(n)的查询。树状数组上的每个节点存的不是一个点的信息而是一段区间的信息,把数组的下标转换为二进制数来表示,一个下标管理的区间长度是2^(二进制数的末尾0的个数),例如1000管理的长度是8,10110管理的长度是2,对于一个下标i,i管理的区间是i-2^(二进制数i末尾0的个数)+1到i,这样就可以实现高效的修改与查询,修改前缀和时每次让i+=i&-i,查询时让i-=i&-i,因此查询与修改都最多在log(n)的时间内完成,这样树状数组的速度就会比线段树更快(但是树状数组能够实现的线段树一定能够实现...)。
void update(int *A,int i,int x){//将数组下标从i开始的数组下标都加上x
while(i<=n){
A[i]+=x;
i+=i&-i;
}
}
int query(int *A,int i){//查询下标为[1,i]区间内的数组权值之和
int res=0;
while(i){
res+=A[i];
i-=i&-i;
}return res;
}
同时,树状数组也可以实现区间修改以及区间查询。与线段树不同的是,线段树的区间查询需要稍微修改数据结构,而树状数组的区间查询只需换一种使用的方法,就可以实现区间的修改。当我们需修改[L,R]区间内的前缀和数组时,分别对[1,L-1],[L,R],[R+1,n]区间进行分析:
(1)在[1,L-1]区间内的前缀和不变。
(2)在[L,R]区间内下标为i的数组,前缀和的增加量=(i-l+1)*x=i*x-(l-1)*x
(3)在[R+1,n]区间内的所有数组,前缀和的增加量=(r-l+1)*x=r*x-(l-1)*x;
在(2)中,由于[L,R]中的i不断在变,所以不能够处理出所有的i的前缀和变化量。于是可以用bit0和bit1分别表示前缀和变化量的随i变化而变化的量和不变的量。由(2)和(3)推出的公式说明查询i时只要query(bit1,i)*i+query(bit0,i)就行了。
/*查询*/
ll res=0;
res+=query(bit0,R)+query(bit1,R)*R;
res-=query(bit0,L-1)+query(bit1,L-1)*(L-1);
cout<<res<<endl;
/*修改*/
add(bit0,L,-x*(L-1));
add(bit0,R+1,x*R);
add(bit1,L,x);
add(bit1,R+1,-x);
4:如果一个查询类的问题,对于每次查询,都要二分一个权值,然后check()一下该值是否可行,如果这样就可以考虑一下是否能将查询进行离线,然后把二分写在外面(比如循环16次),让check和查询数组的更新同时进行,就可以大大地缩小算法的复杂度。对于每次的循环,都将查询数组的mid进行排序,对于每次check(i),都将mid[j]==i的询问更新一下L,R,mid和ans,然后j++,如果L已经大于R就直接跳过当前询问。这样check(i)的i和查询数组的j同时在变,一次循环的复杂度就为O(n+m)。