树状数组查询离散化

更好的观看体验

一些概念

  • 在线操作:每读入一个操作方式,就进行一次修改或者输出结果。
  • 离线操作:将所有操作先全部读入存起来,进行处理后再进行修改或者输出结果。
      我们很多时候,对线段树或者树状数组都是进行在线操作的,边读入操作边修改。但是用树状数组来解决一些题目时,得依赖离线操作来限制在树状数组内信息的范围。不理解这句话不要紧,这里有两道很好的例题可以用离线操作解决。

题目一:换个角度思考

题意简述

  题目给出一个长为 n n n 的序列(均小于 1 0 5 10^5 105),无需修改,只要 m m m 次查询:查询时一个三元组 < l , r , n u m > <l,r,num> <l,r,num> ,意在询问 [ l , r ] [l,r] [l,r] 区间内小于或等于 n u m num num 的个数。数据保证 n ≤ 1 0 5 , m ≤ 1 0 5 , n \leq 10^5 ,m \leq 10^5, n105,m105, 1 ≤ l ≤ r ≤ n , n u m ≤ 1 0 5 1 \leq l \leq r \leq n,num \leq 10^5 1lrn,num105

离线分析

  考虑简单情况,对给定序列询问小于等于某个数的数量,可以用树状数组轻松解决。但是现在要限定在某个区间的内的查询,用树状数组在线操作是很难实现的(什么主席树、莫队的我还不会┭┮﹏┭┮)。那我们考虑一下离线操作吧,这里提供两种离线思路和代码。

按查询区间端点离线

  也许你已经想到了,如果查询在 [ l , r ] [l,r] [l,r] 区间上小于等于 n u m num num 的数目,其实就等于 [ 1 , r ] [1,r] [1,r] 区间上小于等于 n u m num num 的数目减去在 [ 1 , l − 1 ] [1,l-1] [1,l1] 区间上小于等于 n u m num num 的数目。那么,我们可以将所有查询的端点 p o s pos pos 从小到大排序,然后在树状数组中依次加入原序列,限定在树状数组中的数一定是在 [ 1 , p o s ] [1,pos] [1,pos] 区间内的。为了区分左右端点,还得做一个标记 k i n d kind kind左端点是 − 1 -1 1 ,右端点是 1 1 1,这样合并答案的时候就可以实现右边减左边的操作。因为是离线查询,要存每一个端点对应哪次询问的答案,和上面的 k i n d kind kind 相乘即可。

C o d e : Code: Code:

#include<bits/stdc++.h>
using namespace std;
#define For(i,sta,en) for(int i = sta;i <= en;i++)
#define lowbit(x) x&(-x)
#define speedUp_cin_cout ios::sync_with_stdio(false);cin.tie(0); cout.tie(0);
const int maxn = 1e5+9;
int n,m,t[maxn],num[maxn],ans[maxn],range;
struct Question{
    //pos表示查询的区间为[1,pos],id 用来定位查询的序号,num是要比较的值,kind=-1是左端点,kind=1为右端点
    int pos,id,num,kind;
    bool operator < (const Question &y)const{
        return pos<y.pos;
    }
}q[maxn<<1];

void update(int now){
    while(now <= range){
        t[now]++;
        now += lowbit(now);
    }
}

int query(int now){
    int  an = 0;
    while(now){
        an+=t[now];
        now -= lowbit(now);
    }return an;
}

int main(){
    speedUp_cin_cout
    cin>>n>>m;
    For(i,1,n) cin>>num[i],range = max(range,num[i]);
    //一次操作根据端点拆分成两项
    int lef,rig;
    For(i,1,m) {
        lef = (i << 1)-1;
        rig = i << 1;
        cin >> q[lef].pos >> q[rig].pos >> q[lef].num;
        q[rig].num = q[lef].num;
        q[lef].pos --;       //注意左端点要减1,因为查询的是[l,r]区间上的数
        q[rig].id = q[lef].id = i;
        q[lef].kind = -1, q[rig].kind = 1;
    }
    sort(q+1,q+2*m+1);
    //i是原数组下标,j是离线的查询数组下标
    for(int i = 1,j = 1;j <= 2*m;j++){
       //当原数组下标小于等于当前查询的位置,就继续往线段树里塞元素
        while(i <= q[j].pos && i <= n) update(num[i++]);
        ans[q[j].id] += query(q[j].num) * q[j].kind;
    }
    For(i,1,m) cout<<ans[i]<<endl;//最后按原来顺序输出查询答案
    return 0;
}

  代码中定义了一个结构体存查询状态。记得左端点要减一。同时循环那里要小心,经常容易 W A WA WA ,写法虽然很多,但是这种写法是最短的而且不容易错的,比较推荐。

按查询数字大小离线

  还有一种方法是按照查询的数字排序,然后把原序列也给排序了。设原序列数字为 a i a_i ai ,查询数字为 n u m j num_j numj 。把比 n u m j num_j numj 小的 a i a_i ai 加入线段树,然后查询在区间 [ l , r ] [l,r] [l,r] 的数有多少个就行了。注意加入线段树的是 a i a_i ai 的下标 i i i ,和上面的做法不一样。这样由于 n u m num num 是升序的,前面比 a i a_i ai 大的数,后面一定也比 a i a_i ai 大,所以正确性可以得到保证。因为还要保存原序列的下标,所以给原序列也定义了一个结构体。

C o d e : Code: Code:

#include<bits/stdc++.h>
using namespace std;
#define For(i,sta,en) for(int i = sta;i <= en;i++)
#define lowbit(x) x&(-x)
#define speedUp_cin_cout ios::sync_with_stdio(false);cin.tie(0); cout.tie(0);
const int maxn = 1e5+9;
int n,m,t[maxn],ans[maxn];
struct Question{
    //[l,r]是查询区间,num是要比较的值
    int l,r,num,id;
    bool operator < (const Question &y)const{
        return num<y.num;
    }
}q[maxn];

struct Data{
    //数据,位置
    int num, pos;
    bool operator < (const Data &y)const{
        return num<y.num;
    }
}a[maxn];

void update(int now){
    while(now <= n){
        t[now]++;
        now += lowbit(now);
    }
}

int query(int now){
    int  an = 0;
    while(now){
        an+=t[now];
        now -= lowbit(now);
    }return an;
}

int main(){
    speedUp_cin_cout
    cin>>n>>m;
    For(i,1,n) {
        cin>>a[i].num;
        a[i].pos = i;
    }
    For(i,1,m) {
        cin>>q[i].l>>q[i].r>>q[i].num;
        q[i].id = i;
    }
    sort(q+1,q+1+m);
    sort(a+1,a+1+n);
    //i是原数组下标,j是查询下标
    for(int i = 1,j = 1; j <= m;j++){
        while(a[i].num <= q[j].num && i <= n) update(a[i++].pos);
        ans[q[j].id] = query(q[j].r) - query(q[j].l - 1);
    }
    For(i,1,m) cout<<ans[i]<<endl;
    return 0;
}

  两种方法都是可以的,随心选择吧,如果有更好的离线算法也可以和我讨论。我其实只想出了第一种按端点的离线。如这道题的题目一样,我们换个角度思考,树状数组模板又可以轻松套上了。

题目二:配对统计

题意简述

  给定 n n n 个数 a 1 , ⋯   , a n a_{1}, \cdots, a_{n} a1,,an,定义好的配对 ( x , y ) (x, y) (x,y):若对于所有的 $i=1,2, \cdots, n, $ 满足 ∣ a x − a y ∣ ≤ \left|a_{x}-a_{y}\right| \leq axay ∣ a x − a i ∣ ( i ≠ x ) , \left|a_{x}-a_{i}\right|(i \neq x), axai(i=x), 则称 ( x , y ) (x, y) (x,y) 为一组好的配对。然后询问 m m m 次,每次询问区间 [ l , r ] [l, r] [l,r] 中含有多少组好的配对,要求 l ≤ x , y ≤ r  且  x ≠ y l \leq x, y \leq r \text { 且 } x \neq y lx,yr  x=y。数据保证 a i a_i ai 互不相等,且 1 ≤ a i ≤ 1 0 9 1\leq a_{i} \leq 10^{9} 1ai109 , n ≤ 3 × 1 0 5 n \leq 3 \times 10^5 n3×105 。最后输出一个值 ∑ i = 1 m A n s i × i , A n s i \sum_{i=1}^{m} A n s_{i} \times i,Ans_i i=1mAnsi×iAnsi 为第 i i i 次查询结果。

离线分析

  这道题和上一道题有类似的地方,都是没有区间修改,但是有比较难在线处理的区间查询。我们考虑进行离线操作。首先由定义可知,对于好的配对 ( x , y ) , (x, y), (x,y), 一定在排成有序时 a x a_{x} ax a y a_{y} ay 相邻,我们可以 O ( n l o g n ) O(nlogn) O(nlogn) 预处理出所有配对,并放入一个结构体数组中。然后我们为了让加入树状数组里的数据来源限定在一定的范围内,就得对查询的区间进行一通排序,将区间右端点小的放在前面。对配对结构体数组也是这样排序(排序的方式并不唯一,写法改一下而已,但是配对数组的排序要和查询数组排序一致)。
  这样,对于某个查询 [ l , r ] [l,r] [l,r] ,把所有右端点小于等于 r r r 的配对的左端点都放进树状数组里头,然后查询大于 l − 1 l-1 l1 的端点有多少个,就说明有几个配对被包含在区间 [ l , r ] [l,r] [l,r] 了。以此类推,因为后面查询的 r r r 一定大于或等于前面的 r r r ,所以树状数组里的数据始终在 [ 1 , r ] [1,r] [1,r] 这个范围里,查询就能保证正确性了。

C o d e : Code: Code:

#include<bits/stdc++.h>
using namespace std;
#define For(i,sta,en) for(int i = sta;i <= en;i++)
#define lowbit(x) x&(-x)
#define speedUp_cin_cout ios::sync_with_stdio(false);cin.tie(0); cout.tie(0);
typedef long long ll;
const int maxn = 3e5+9;

struct Num{
    int num,pos;
    bool operator < (const Num &y) const{
        return num < y.num;
    }
}num[maxn];

//配对和查询结构体数组,实际上配对用不到id变量,id记录的是查询的次序
struct node{
    int l,r,id;
    bool operator < (const node &y)const{
        return r  < y.r;  //注意是按右端点小的排前面
    }
}p[maxn<<1],q[maxn]; //p中存在原序列中所有好的配对,q存每条查询

ll t[maxn],n,m,ans,cnt;  //注意答案可能会爆int,cnt统计总配对数

void update(int now){
    while(now <= n){
        t[now]++;
        now += lowbit(now);
    }
}

ll query(int now){
    ll an = 0;
    while(now){
        an += t[now];
        now -= lowbit(now);
    }return an;
}

//增添配对函数
void Add(int now){
    ll d1 = abs(num[now].num - num[now-1].num); //与左边的差距
    ll d2 = abs(num[now].num - num[now+1].num); //与右边的差距
    //如果左右相等,说明x=now时有两个配对
    if(d1 == d2) {
        p[++cnt].l = min(num[now].pos, num[now - 1].pos); //左端点取小的
        p[cnt].r = max(num[now].pos, num[now - 1].pos);  //右端点取大的
        p[++cnt].l = min(num[now].pos, num[now + 1].pos);
        p[cnt].r = max(num[now].pos, num[now + 1].pos);
    }else if(d1 < d2){
        //配对在左边
        p[++cnt].l = min(num[now].pos, num[now - 1].pos);
        p[cnt].r = max(num[now].pos, num[now - 1].pos);
    }else{
        //配对在右边
        p[++cnt].l = min(num[now].pos, num[now + 1].pos);
        p[cnt].r = max(num[now].pos, num[now + 1].pos);
    }
}

int main(){
    speedUp_cin_cout  //读写优化
    cin>>n>>m;
    num[0].num = num[n+1].num = -0x3f3f3f3f;//这里是为了不用特判端点,因为配对肯定取不到0 和 n+1
    For(i,1,n) cin>>num[i].num,num[i].pos = i;
    sort(num+1,num+1+n);  //先排序
    For(i,1,n) Add(i);  //后找配对,加入好的配对数组p中
    sort(p+1,p+1+cnt);

    For(i,1,m) cin >> q[i].l >> q[i].r, q[i].id = i;//读入查询
    sort(q + 1, q + 1 + m);

    //i是已配对数组p的下标,j是查询数组q的下标,注意不要写错
    for(int i = 1,j = 1;j <= m;j++){

        while(p[i].r <= q[j].r && i <= cnt) update(p[i++].l);
        //如果配对的区间仍在[1 , q[j].r]范围内,就把它的左端点下标加进树状数组

        ans += (query(q[j].r) - query(q[j].l - 1)) * q[j].id;
        //查询[q[j].l , q[j].r]所有配对的数目
        //也可以这样写 ans += (query(n) - query(q[j].l - 1)) * q[j].id;
        //因为树状数组里的数据肯定是在[1 , q[j].r]范围内的,这样写没错也证明了这一点。
    }
    cout<<ans;
    return 0;
}

  这样做的实质其实是为了让每一个查询 [ l , r ] [l,r] [l,r],都在所有包含在 [ 1 , r ] [1,r] [1,r] 内的配对全部加进树状数组的前提下进行(断句困难)。这也是离线操作比较妙的地方。还有不清楚的欢迎来讨论(o゜▽゜)o☆

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Vue树状数组扁平是一个操作,用于将树状数组(也称为B树或二叉搜索树)中的数据结构扁平为线性数据结构。这个操作通常用于数据展示和性能优。 在Vue树状数组扁平过程中,需要遍历树状数组,并将每个节点转换为对应的线性数据结构元素。这通常可以通过递归实现。 下面是一个简单的Vue树状数组扁平的实现步骤: 1. 定义一个数据结构来表示树状数组中的节点。每个节点包含一个数据值和指向其子节点的指针。 2. 定义一个递归函数,用于遍历树状数组并转换每个节点。该函数需要接受当前节点的值和指针,以及一个空列表(用于存储扁平的结果)。 3. 在函数中,检查当前节点是否有子节点。如果有,递归调用函数自己处理子节点。否则,将当前节点的值添加到结果列表中。 4. 最后,遍历完成后,将结果列表转换为一个普通的数组或其他适当的线性数据结构,并将其传递给Vue组件进行展示。 需要注意的是,Vue树状数组扁平操作可能会对性能产生一定的影响,特别是在处理大型数据集时。因此,在实现过程中需要考虑性能优措施,例如使用分页或懒加载等技术来减少不必要的计算和渲染。 此外,为了更好地使用Vue树状数组扁平,你可能需要使用Vue的数据绑定机制和组件系统来动态地呈现扁平的数据结构。你可以创建一个组件来显示扁平的数据,并使用Vue的数据绑定语法将其与数据源进行绑定。 总之,Vue树状数组扁平是一个常用的操作,可以帮助你更有效地处理树状数组数据,并提高性能和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ailanxier

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值