数据结构学习之:BIT(二元索引树、树状数组)

树状数组是一种支持单点修改区间查询的,代码量小的数据结构。

前置知识介绍:

lowbit(x)lowbit(x)lowbit(x)表示的为x的二进制表达中,最低为的1及其后面的0组成的数。

int lowbit(x){
	return (x)&(-x);
}

区间查询

树状数组重要的是用前缀数组c[x]维护一些信息。假设原数组为a,则c[x]维护的就是从a中x−lowbit(x)+1到x的信息。树状数组重要的是用前缀数组c[x]维护一些信息。假设原数组为a,则c[x]维护的就是从a中x-lowbit(x)+1到x的信息。树状数组重要的是用前缀数组c[x]维护一些信息。假设原数组为a,则c[x]维护的就是从axlowbit(x)+1x的信息。
以区间和为例。想要查询a(l,r)的和?那么其实就相当于查询∑i=1rai与∑i=1l−1ai,二者做差即可。以区间和为例。想要查询a(l,r)的和?那么其实就相当于查询\sum_{i=1}^{r}a_i与\sum_{i=1}^{l-1}a_i,二者做差即可。以区间和为例。想要查询a(l,r)的和?那么其实就相当于查询i=1raii=1l1ai,二者做差即可。
如何查询a(1,x)?对于c[x],我们知道其维护的是∑i=x−lowbit(x)+1xai,那么我们可以一直让x往前面跳,直到x为0。每次让x=x−lowbit(x)。如何查询a(1,x)?对于c[x],我们知道其维护的是\sum_{i=x-lowbit(x)+1}^{x}a_i,那么我们可以一直让x往前面跳,直到x为0。每次让x=x-lowbit(x)。如何查询a(1,x)?对于c[x],我们知道其维护的是i=xlowbit(x)+1xai,那么我们可以一直让x往前面跳,直到x0。每次让x=xlowbit(x)

int que(int x){
	int sum=0;
	while(x>0){
		sum+=c[x];
		x=x-lowbit(x);
	}
	return sum;
}


int que_range(int l,int r){
	return que(r)-que(l-1);
}

单点修改

首先需要知道,针对ai进行单点修改,我们只需要修改所有包含ai的cx。此外,在c构成的树状数组中,x的父亲为x+lowbit(x)。且c[x]只会管辖其左下方内容。故我们每次更新x并更新对应的c[x]即可。首先需要知道,针对a_i进行单点修改,我们只需要修改所有包含a_i的c_x。此外,在c构成的树状数组中,x的父亲为x+lowbit(x)。且c[x]只会管辖其左下方内容。故我们每次更新x并更新对应的c[x]即可。首先需要知道,针对ai进行单点修改,我们只需要修改所有包含aicx。此外,在c构成的树状数组中,x的父亲为x+lowbit(x)。且c[x]只会管辖其左下方内容。故我们每次更新x并更新对应的c[x]即可。

void update(int idx,int x){
	a[idx]+=x;
	while(idx<=n){
		c[idx]+=x;
		idx=idx+lowbit(idx));
	}
}

在这里插入图片描述

建树

一般转化为n次单点修改,假如a为[5,1,7],那么可以看作对原始数组c(全为0)进行三次单点修改,对a[1]单点加5,对a[2]单点加1,对a[3]单点加7即可。一般转化为n次单点修改,假如a为[5,1,7],那么可以看作对原始数组c(全为0)进行三次单点修改,对a[1]单点加5,对a[2]单点加1,对a[3]单点加7即可。一般转化为n次单点修改,假如a[5,1,7],那么可以看作对原始数组c(全为0)进行三次单点修改,对a[1]单点加5,对a[2]单点加1,对a[3]单点加7即可。

关于区间修改与查询

首先关于前置知识:差分。差分数组是一种用来记录多次区间修改的内容的数组。对于数组a,定义其差分数组b[1]=a[1],b[i]=a[i]−a[i−1]。对于差分数组,求前缀和即可得到数组a。首先关于前置知识:差分。差分数组是一种用来记录多次区间修改的内容的数组。对于数组a,定义其差分数组b[1]=a[1],b[i]=a[i]-a[i-1]。对于差分数组,求前缀和即可得到数组a。首先关于前置知识:差分。差分数组是一种用来记录多次区间修改的内容的数组。对于数组a,定义其差分数组b[1]=a[1]b[i]=a[i]a[i1]。对于差分数组,求前缀和即可得到数组a
现在假如要区间查询。同样的,我们考虑将a[l,r]转化为a[1,r]−a[1,l−1]。现在单独考虑a[1,r],∑i=1rai=∑i=1r∑j=1ibj。每个bi被计算了(r−i+1)次,故再次转化为∑i=1rbi∗(r−i+1)。这里用图来表示更清晰。现在假如要区间查询。同样的,我们考虑将a[l,r]转化为a[1,r]-a[1,l-1]。现在单独考虑a[1,r],\sum_{i=1}^{r}a_i=\sum_{i=1}^{r}\sum_{j=1}^{i}b_j。每个b_i被计算了(r-i+1)次,故再次转化为\sum_{i=1}^{r}b_i*(r-i+1)。这里用图来表示更清晰。现在假如要区间查询。同样的,我们考虑将a[l,r]转化为a[1,r]a[1,l1]。现在单独考虑a[1,r]i=1rai=i=1rj=1ibj。每个bi被计算了(ri+1)次,故再次转化为i=1rbi(ri+1)。这里用图来表示更清晰。
在这里插入图片描述
在这里插入图片描述
所以我们可以用两个树状数组,一个c1维护差分数组b的值,一个c2维护b[i]∗i的值。所以我们可以用两个树状数组,一个c1维护差分数组b的值,一个c2维护b[i]*i的值。所以我们可以用两个树状数组,一个c1维护差分数组b的值,一个c2维护b[i]i的值。
如何区间操作?假如a[l,r]区间都加上x,则b[l]+x且b[r+1]−x。那么对c1在l和r+1单点修改两次x即可,对c2在l,单点修改x∗l,在r+1,单点修改x∗(r+1)。如何区间操作?假如a[l,r]区间都加上x,则b[l]+x且b[r+1]-x。那么对c1在l和r+1单点修改两次x即可,对c2在l,单点修改x*l,在r+1,单点修改x*(r+1)。如何区间操作?假如a[l,r]区间都加上x,则b[l]+xb[r+1]x。那么对c1lr+1单点修改两次x即可,对c2l,单点修改xl,在r+1,单点修改x(r+1)

void solve() {
    vector<int> c1(500002, 0);//维护差分数组b
    vector<int> c2(500002, 0);//维护b[i]*i
    vector<int> a(500002, 0);
    vector<int> b(500002, 0);
    int n;
    cin >> n;
    int m;
    cin >> m;
    auto update_c1 = [&](int idx, int val) {//单点更新c1
        while (idx <= n) {
            c1[idx] += val;
            idx = idx + lowbit(idx);
        }
    };

    auto update_c2 = [&](int idx, int val) {//单点更新c2
        while (idx <= n) {
            c2[idx] += val;
            idx = idx + lowbit(idx);
        }
    };

    auto ask_c1 = [&](int x) {//询问 sum(b1,bx)
        int sum = 0;
        while (x) {
            sum += c1[x];
            x = x - lowbit(x);
        }
        return sum;
    };

    auto ask_c2 = [&](int idx) {//询问 sum(b1*1,bx*x)
        int sum = 0;
        while (idx) {
            sum += c2[idx];
            idx = idx - lowbit(idx);
        }
        return sum;
    };

    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    for (int i = 1; i <= n; i++) {
        b[i] = a[i] - a[i - 1];
        update_c1(i, b[i]);
        update_c2(i, b[i] * i);
    }

    auto ope_range = [&](int l, int r, int x) {
        update_c1(l, x), update_c1(r + 1, -x);
        update_c2(l, x * l), update_c2(r + 1, -x * (r + 1));
    };

    auto ask_range = [&](int l, int r) {
        return ask_c1(r) * (r + 1) - ask_c2(r) - ask_c1(l - 1) * l + ask_c2(l - 1);
    };

    while (m--) {
        int x;
        cin >> x;
        if (x == 1) {
            int l, r, k;
            cin >> l >> r >> k;
            ope_range(l, r, k);
        } else {
            int idx;
            cin >> idx;
            cout << ask_range(idx, idx) << endl;
        }
    }

}

权值数组

我们将在a数组中每个数出现的次数组成一个新的数组b,则b数组就是a数组的权值数组。比如a=[1,1,5,3,4],则b=[2,0,1,1,1]。我们需要注意一个问题,如果a[i]过大的话可能会出现爆掉的可能,所以需要考虑离散化。我们将在a数组中每个数出现的次数组成一个新的数组b,则b数组就是a数组的权值数组。比如a=[1,1,5,3,4],则b=[2,0,1,1,1]。我们需要注意一个问题,如果a[i]过大的话可能会出现爆掉的可能,所以需要考虑离散化。我们将在a数组中每个数出现的次数组成一个新的数组b,则b数组就是a数组的权值数组。比如a=[1,1,5,3,4],则b=[2,0,1,1,1]。我们需要注意一个问题,如果a[i]过大的话可能会出现爆掉的可能,所以需要考虑离散化。

单点修改+全局第k问题

我们考虑在权值数组上建立树状数组,那么当需要查询全局第k小的时候,我们只需要二分查询权值数组的前缀和,找到一个索引位置x满足∑i=1xbi<k并且∑i=1x+1bi>=k,那么此时x+1对应的数即为第k小的数。我们考虑在权值数组上建立树状数组,那么当需要查询全局第k小的时候,我们只需要二分查询权值数组的前缀和,找到一个索引位置x满足\sum_{i=1}^{x}b_i<k并且\sum_{i=1}^{x+1}b_i>=k,那么此时x+1对应的数即为第k小的数。我们考虑在权值数组上建立树状数组,那么当需要查询全局第k小的时候,我们只需要二分查询权值数组的前缀和,找到一个索引位置x满足i=1xbi<k并且i=1x+1bi>=k,那么此时x+1对应的数即为第k小的数。
单点修改的时候,我们只需要在val对应的索引位置+1或者−1即可。单点修改的时候,我们只需要在val对应的索引位置+1或者-1即可。单点修改的时候,我们只需要在val对应的索引位置+1或者1即可。

 	map<int, int> a;//数值对索引
    map<int, int> b;//索引对数值

    vector<int> bit(MAXN, 0);
    int index = 0;
    int types = 0;
    int n;
    cin >> n;
    vector<int> arr(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> arr[i];
    }

    auto update = [&](int idx, int val) {
        while (idx < MAXN) {
            bit[idx] += val;
            idx = idx + lowbit(idx);
        }
    };

    auto que = [&](int idx) {
        int sum = 0;
        while (idx > 0) {
            sum += bit[idx];
            idx = idx - lowbit(idx);
        }
        return sum;
    };

    auto insert = [&](int x) {
        if (a.find(x) == a.end()) {
            index++;
            types++;
            a[x] = index;
            b[index] = x;
        }
        update(a[x], 1);

    };

    auto remove = [&](int val) {

        if (a.find(val) != a.end()) {

            int re = que(a[val]) - que(a[val] - 1);
            if (re == 1) {
                types--;
                update(a[val], -1);
            } else if (re != 0) {
                update(a[val], -1);
            } else {
                cout << "已经删完了" << endl;
            }

        } else {
            cout << "不存在" << endl;
        }
    };

    auto que_k = [&](int k) {
        int l = 1, r = index;
        int ans = -1;
        while (l <= r) {
            int mid = (l + r) >> 1;

            if (que(mid) >= k) {
                ans = mid;
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        if(ans==-1) {
        	cout<<"找不到"<<endl;
        	return -1ll;
        }
        return b[ans];
    };

    sort(arr.begin() + 1, arr.begin() + 1 + n);//因为要保证离散化前后的偏序关系,所以需要先排序。

    for (int i = 1; i <= n; i++) {
        insert(arr[i]);
    }


    cout << que_k(7) << endl;
    remove(9);
    cout << que_k(6) << endl;
    remove(9);
    //input:
    //7
	//1 2 7 8 9 4 5
	//output:
	//9
	//8
	//已经删完了

求逆序对

我们还可以在权值数组上建立树状数组来统计逆序对的数量。给定一个数组a,对于位置为i的数,需要找到有多少个j>i使得a[j]小于a[i]。考虑倒着遍历数组a,设dis[x]为x离散化后的值,对于a[i],我们只需要找到其后面比a[i]小的数。由于离散化保持偏序关系,那么我们可以直接查询(dis[a[i]]−1)的前缀和,即为与a[i]构成逆序对的数量,之后再让bit中dis[a[i]]加1。我们还可以在权值数组上建立树状数组来统计逆序对的数量。给定一个数组a,对于位置为i的数,需要找到有多少个j>i使得a[j]小于a[i]。考虑倒着遍历数组a,设dis[x]为x离散化后的值,对于a[i],我们只需要找到其后面比a[i]小的数。由于离散化保持偏序关系,那么我们可以直接查询(dis[a[i]]-1)的前缀和,即为与a[i]构成逆序对的数量,之后再让bit中dis[a[i]]加1。我们还可以在权值数组上建立树状数组来统计逆序对的数量。给定一个数组a,对于位置为i的数,需要找到有多少个j>i使得a[j]小于a[i]。考虑倒着遍历数组a,设dis[x]x离散化后的值,对于a[i],我们只需要找到其后面比a[i]小的数。由于离散化保持偏序关系,那么我们可以直接查询(dis[a[i]]1)的前缀和,即为与a[i]构成逆序对的数量,之后再让bitdis[a[i]]1

	map<int, int> a;//数值对索引

    int index = 0;
    int n;
    cin >> n;
    vector<int> bit(n+1, 0);

    vector<int> arr(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> arr[i];
    }

    auto update = [&](int idx, int val) {
        while (idx < n+1) {
            bit[idx] += val;
            idx = idx + lowbit(idx);
        }
    };

    auto que = [&](int idx) {
        int sum = 0;
        while (idx > 0) {
            sum += bit[idx];
            idx = idx - lowbit(idx);
        }
        return sum;
    };

    auto temp=arr;
    sort(arr.begin() + 1, arr.begin() + 1 + n);
    for(int i=1;i<=n;i++){//离散化
        if(a.find(arr[i])==a.end()){
            index++;
            a[arr[i]]=index;
        }
    }
    int ans=0;
    for(int i=n;i>=1;i--){
        ans+=que(a[temp[i]]-1);
        update(a[temp[i]],1);
    }
    cout<<ans<<endl;

例题

在这里插入图片描述
我们观察可以发现,题目要求可转化为求在子区间[l,r]中有多少个数比ac小。考虑当前样例:我们观察可以发现,题目要求可转化为求在子区间[l,r]中有多少个数比a_c小。考虑当前样例:我们观察可以发现,题目要求可转化为求在子区间[l,r]中有多少个数比ac小。考虑当前样例:
a[1,4,2,3,5],l=3,r=5,c=4。因为a4等于3,而在[3,5]中有一个2比3小,那么经过排序后原来4位置的数就会a[1,4,2,3,5],l=3,r=5,c=4。因为a_4等于3,而在[3,5]中有一个2比3小,那么经过排序后原来4位置的数就会a[1,4,2,3,5]l=3r=5c=4。因为a4等于3,而在[3,5]中有一个23小,那么经过排序后原来4位置的数就会
变到3+1=4的位置。变到3+1=4的位置。变到3+1=4的位置。
因为给定数组一定构成一个排列,那么我们在经过离散化后从小开始遍历并更新每个数,1−>1,2−>3,3−>4,4−>2,5−>5。因为给定数组一定构成一个排列,那么我们在经过离散化后从小开始遍历并更新每个数,1->1,2->3,3->4,4->2,5->5。因为给定数组一定构成一个排列,那么我们在经过离散化后从小开始遍历并更新每个数,1>1,2>3,3>4,4>2,5>5

for (int i = 1; i <= n; i++) cin >> arr[i], ys[arr[i]] = i;//记录一下每个数映射到的位置

我们从小开始查询,每次查询一个数,因为前面的数一定都是比他小的,比他小的数所在的映射被更新为1,我们只需要查询在[l,r]这个区间有多少个1就可以了。我们从小开始查询,每次查询一个数,因为前面的数一定都是比他小的,比他小的数所在的映射被更新为1,我们只需要查询在[l,r]这个区间有多少个1就可以了。我们从小开始查询,每次查询一个数,因为前面的数一定都是比他小的,比他小的数所在的映射被更新为1,我们只需要查询在[l,r]这个区间有多少个1就可以了。

typedef struct ask {
        int l;
        int r;
        int ans;
        int id;
    } ask;
    int n, m;
    cin >> n >> m;
    vector<int> ys(n + 1);

    vector<int> arr(n + 1);
    for (int i = 1; i <= n; i++) cin >> arr[i], ys[arr[i]] = i;

    vector<vector<ask>> que(n + 1);
    for (int i = 1; i <= m; i++) {
        int l, r, x;
        cin >> l >> r >> x;
        que[arr[x]].push_back({l, r, 0, i});

    }

    vector<int> bit(100010, 0);

    auto updt = [&](int x, int val) {
        while (x <= n) {
            bit[x] += val;
            x += lowbit(x);
        }
    };


    auto qu = [&](int idx) {
        int sum = 0;
        while (idx > 0) {
            sum += bit[idx];
            idx -= lowbit(idx);
        }
        return sum;
    };

    auto sum = [&](int l, int r) {
        return qu(r) - qu(l - 1);
    };

    vector<ask> to;

    for (int i = 1; i <= n; i++) {
        if (que[i].size()) {
            for (auto it: que[i]) {
                it.ans = it.l + sum(it.l, it.r);
                to.push_back(it);
            }
        }
        updt(ys[i], 1);

    }

    sort(to.begin(), to.end(), [&](ask a, ask b) {
        return a.id < b.id;
    });
    for (int i = 1; i <= m; i++) {
        cout << to[i - 1].ans << endl;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值