树状数组的基本应用

树状数组的基本应用

观前提示:本文不打算介绍树状数组的原理,只会列举一些简单的应用。

注意:树状数组只能支持单点修改,区间查询。也许有人说将可以将数组变形成差分然后支持区间修改,单点查询,甚至区间修改,区间查询。但本质上对直接影响的数组其实还是进行单点修改,区间查询的。

单点修改操作

//在数组中为下标为id的数加上add
void update(int tree[], int id, int add)
{
    for(int i = id; i<=n;i += lowbit(i))
        tree[i] += add;
}

查询操作

//求前m个数之和
int query(int tree[], int m)
{
    int res = 0;
    for(int i=m;i;i -= lowbit(i))
        res += tree[i];
    return res;
}

还有一些其他操作

//O(n)建树
for (int i = 1; i <= n; i++)
{
    tree[i] += a[i];
    int j = i + lowbit(i);
    if (j <= n) tree[j] += tree[i];
}
//即每次确定完儿子的值后,用自己的值更新自己的直接父亲

首先最基本的应用:单点修改,区间查询

P3374 【模板】树状数组 1

//在数组中为下标为id的数加上add
update(tree, id, add);
//输出[l, r]区间和
cout << query(tree, r) - query(tree, l-1) << endl;

然后我们做一些改变能够试着应用:区间修改,单点查询

P3368 【模板】树状数组 2

首先我们树状数组里存的是差分

for (int i = 1; i <= n; i++)
{
    tree[i] += a[i] - a[i-1];
    int j = i + lowbit(i);
    if (j <= n) tree[j] += tree[i];
}

区间修改

//将区间 [l,r]内每个数加上d
update(tree, l, d);
update(tree, r+1, -d);

单点查询

// 输出第 x 个数的值
printf("%d\n", query(tree, x));

然后我们试着应用:区间修改,区间查询

一个简单的整数问题2

首先我们要明确,我们本质上只能做的操作是单点修改,区间查询,所以面对区间修改我们要转化为差分。

不妨令原数组为a[n], 差分数组为b[n],即b[i] = a[i] - a[i-1]。

对于b[n],我们能做的只有单点修改,区间查询。 但我们要区间查询a[n],所以我们必须想办法用b[n]的前缀和来构造a[n]的前缀和。

于是,我们想到用b[n]来构建一个新数组c[n],对于c[n],我们可以通过单点修改b[x]来单点修改c[x],从而区间查询c[n]。

好了,我们的任务是找到一个与b[n]有关的新数组c[n],用b[n]的前缀和和c[n]的前缀和来构造a[n]的前缀和

我们要求的a[1] + a[2]] + ... a[m] = b[1]
                                 + b[1] + b[2]
                                 + b[1] + b[2] + b[3]
                                 + ...
                                 + b[1] + b[2] + b[3] + ... + b[m]     
                                 = m*b[1] + (n-1)*b[2] + ... + b[m]
                                 = m*(b[1]+...+b[m]) - (0*b[1]+1*b[2]+2*b[3]+...+(m-1)*b[m])

这样我们只需要求出sum(b[m]) 和 sum((m-1)*b[m]),但一般而言我们会化成这种形式

a[1] + a[2]] + ... a[m] = (m+1)*(b[1]+...+b[m]) - (1*b[1]+2*b[2]+3*b[3]+...+m*b[m])
b[i] = a[i] - a[i-1]
c[i] = b[i] * i

于是,我们的任务为单点修改b[x]来单点修改c[x],从而查询b[n]和c[n]的前缀和来查询a[n]的前缀和。(真拗口)

注意long long

LL a[N], tree1[N], tree2[N];
LL sum(int x);
void update(LL tr[], int x, LL c);

首先我们要建两棵树来维护b[n], c[n]

for (int i = 1; i <= n; i++)
{
    tree1[i] += a[i] - a[i-1];
    tree2[i] += i * (a[i] - a[i-1])
    int j = i + lowbit(i);
    if (j <= n)
    {
        tree1[j] += tree1[i];
        tree2[j] += tree2[j];
    }
}

区间修改

//将区间 [l,r]内每个数加上d
update(tree1, l, d);
update(tree1, r+1, -d);
update(tree2, l, (LL)l*d);//注意long long
update(tree2, r+1, (LL)(r+1)*(-d));

区间查询

LL sumr = (r+1) * query(tree1, r) - query(tree2, r);
LL suml = l * query(tree1, l-1) - query(tree2, l-1);
printf("%lld\n", sumr - suml);

通过权值树状数组查询区间内第k大值

一个数组a[n],每个元素都在[l,r]之间,如果我们进行如下操作

for (int i = 1; i <= n; i++)
    update(tree, a[i], 1);

那k = query(tree, x)是什么?答:a[n]中小于等于x的数的个数

那如果a[n]中数各不相同,而a[i] = x, 那a[i]岂不就是a[n]中的第k大值!

所以,问:对于一个每个数都在[l,r]之间且每个数各不相同的数列,它的第k大值是什么?

答:我们只需要找到第一个query(tree, x)>=k的x就好了

//找第k小的数,即找到第一个query(x)>=k的数
int find(int k, int l, int r)
{
    while(l < r)
    {
        int m = l + r>> 1;
        if(query(tree, m) >= k)
            r = m;
        else
            l = m+1;
    }
    return r;
}
//假如r不在数组中,那必不会是第一个query(tree, x)>=k的x,证明从略。所以r必然在数组中。

假如我们要动态维护也很简单

update(tree, x, 1);//加入x
update(tree, x, -1);//删除x
posted @ 2021-08-29 19:32  DarkLights  阅读(19)  评论(0编辑  收藏  举报