算法竞赛进阶指南-第五章-树状数组
- 8 mins树状数组
下面这段代码可以计算出区间[1,x]分成的O(logx)个小区间
while(x > 0)
{
printf("[%d,%d]\n",x - (x & -x) + 1, x);
x -= x & -x;
}
树状数组支持的基本操作有两个
查询前缀和,即序列a第1~x个数的和(O(logN))
int ask(int x)
{
int ans = 0;
for(; x; x -= x & -x) ans += c[x];
return ans;
}
单点增加,即给序列中的一个数a[x]加上y,同时正确维护序列的前缀和(O(logN))
void add(int x, int y)
{
for(; x <= N; x += x & -x) c[x] += y;
}
在执行所有操作之前,我们需要对树状数组进行初始化,针对原始序列a构造一个树状数组.
为了简便,一般的初始化方法是:直接建立一个全为0的数组c,然后对每个位置x执行add(x,a[x])时间复杂度为(O(NlogN))
更高效的初始化方法是:从小到大依次考虑每个节点x,借助lowbit运算扫描它的子节点并求和时间复杂度为(O(N))
树状数组与逆序对
刚开始初始化为全零,然后倒序扫描序列a,查询前缀和,即比这个数小的且在它后面的数量,相加后,再将其本身数值对应出现次数加1.然后继续查找
楼兰图腾
以横坐标为标准排序,得到序列a
倒叙扫描序列a,通过树状数组得到每个a[i]后面有几个数比它大,即为right[i].
正序扫描序列a,通过树状数组得到每个a[i]前面有几个数比它大,即为left[i]
依次枚举每个点作为中心点,以该点为中心的v图腾个数为left[i]*right[i]
因为横纵坐标都不相同,所以a[i]后面比它小的数为n-i-right[i]
同理,a[i]前面比它小的数为i-1-left[i]
依次枚举每个点为中心点,以该点为中心的^图腾个数也可得到
#include <bits/stdc++.h>
using namespace std;
int n;
int y[200005], c[200005];
int ask(int x)
{
int ans = 0;
for (; x; x -= x & -x)
ans += c[x];
return ans;
}
void add(int x, int y)
{
for (; x <= n; x += x & -x)
c[x] += y;
}
long long left_v[200005], leftt[200005], right_v[200005], rightt[200005];
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> y[i];
}
for (int i = n; i > 0; i--)
{
right_v[i] = ask(y[i] - 1);
rightt[i] = n - i - right_v[i];
add(y[i], 1);
}
memset(c, 0, sizeof(y));
for (int i = 1; i <= n; i++)
{
left_v[i] = ask(y[i] - 1);
leftt[i] = i - 1 - left_v[i];
add(y[i], 1);
}
int64_t ans1 = 0, ans2 = 0;
for (int i = 1; i <= n; i++)
{
ans1 += left_v[i] * right_v[i];
ans2 += leftt[i] * rightt[i];
}
cout << ans2 << ' ' << ans1;
return 0;
}
思路呢,就是上面说的那么个思路,不过要注意的是,它数据范围里强调了int64_t,也就是相当于long long,所以要把前缀和和答案的数据类型都改为long long
树状数组的扩展应用
A Tiny Problem with Integers
利用差分的思想,将区间加和转化为两点加和,查询单点数值则转化为求前缀和
#include <bits/stdc++.h>
using namespace std;
int n, m;
char c;
int l, r, d, x;
int dd[100005];
int a[100005];
int ask(int z)
{
int ans = 0;
for (; z; z -= z & -z)
ans += dd[z];
return ans;
}
void add(int z, int y)
{
for (; z <= n; z += z & -z)
dd[z] += y;
}
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
while (m--)
{
cin >> c;
if (c == 'C')
{
cin >> l >> r >> d;
add(l, d);
add(r + 1, -d);
}
else
{
cin >> x;
cout << ask(x) + a[x] << '\n';
}
}
}
A Simple Problem with Integers
因此,我们增加一个树状数组,用于维护i*b[i]的前缀和(c0用来存储b[i],c1用来存储b的前缀和)
用sum存储a的原始前缀和
#include <bits/stdc++.h>
using namespace std;
const int SIZE = 100010;
int a[SIZE], n, m;
long long c[2][SIZE], sum[SIZE];//这里是定义c0和c1以及sum三个数组
long long ask(int k, int x)//这里是把两个ask合并后的函数
{
long long ans = 0;
for (; x; x -= x & -x)
ans += c[k][x];
return ans;
}
void add(int k, int x, int y)//同上
{
for (; x <= n; x += x & -x)
c[k][x] += y;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
sum[i] = sum[i - 1] + a[i];
}
while (m--)
{
char op[2];
int l, r, d;
cin >> op >> l >> r;
if (op[0] == 'C')//判断是C还是Q
{
cin >> d;
add(0, l, d);
add(0, r + 1, -d);
add(1, l, l * d);
add(1, r + 1, -(r + 1) * d);
}
else
{
long long ans = sum[r] + (r + 1) * ask(0, r) - ask(1, r);
ans -= sum[l - 1] + l * ask(0, l - 1) - ask(1, l - 1);
cout << ans << '\n';
}
}
}
Lost Cows
如果第k头牛前有Ak头比它低,那么它的身高Hk是数值1~n中第Ak+1小的没有在后面中出现过的数
因此,我们可以倒序扫描序列a,从最后一个开始,给该牛选择可用的第Ak+1矮的高度
具体操作,先建立一个01序列b,令其初值全为1,并用树状数组c维护b,以方便得到b的前缀和
然后倒序扫描a,对每个A做出下列操作
查询第Ai+1个1在什么位置,令该牛的高度等于该位置
把该位置的”1”改为0
关于具体的查询方法,可以使用树状数组+倍增
开始时,初始化两个变量ans和sum,令其均为0
从logn向下取整到0倒序考虑每个整数:
这里的原因是
结合树状数组的图,我们可以发现,16->8->4->2->1这条线是随2次幂指数依次降低的结果
第一步,假设p为4,则sum+c[ans+2^p]为整个b数组的前缀和,不会小于k
假设p为3,则sum+c[ans+2^p]为b数组前八位的前缀和,如果小于,则相加,然后看9~12,如果大于,则考虑1~4
也就是说,不论大于还是小于,都会考虑下一层,所以是倒序考虑每个整数,当p为0时,也就意味着考虑到了单个元素,那么对应的ans也就是第Ai+1个1的前一个(因为如果权值为0,那么也会相加,所以最后返回的ans一定是Ai+1个1的前一个),即Hi = ans + 1