树状数组的基本应用
树状数组的基本应用
观前提示:本文不打算介绍树状数组的原理,只会列举一些简单的应用。
注意:树状数组只能支持
单点修改,区间查询
。也许有人说将可以将数组变形成差分然后支持区间修改,单点查询,甚至区间修改,区间查询。但本质上对直接影响的数组
其实还是进行单点修改,区间查询
的。
单点修改操作
//在数组中为下标为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];
}
//即每次确定完儿子的值后,用自己的值更新自己的直接父亲
首先最基本的应用:单点修改,区间查询
//在数组中为下标为id的数加上add
update(tree, id, add);
//输出[l, r]区间和
cout << query(tree, r) - query(tree, l-1) << endl;
然后我们做一些改变能够试着应用:区间修改,单点查询
首先我们树状数组里存的是差分
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));
然后我们试着应用:区间修改,区间查询
首先我们要明确,我们本质上只能做的操作是单点修改,区间查询
,所以面对区间修改我们要转化为差分。
不妨令原数组为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