树状数组能够高效处理【对一个数组不断修改并求其前缀和】的问题,其修改与查询操作的复杂度都是 O ( log n ) O(\log{n}) O(logn)
对于已经维护好的前缀和,如果需要修改,则需要付出 O ( n ) O(n) O(n) 的代价
树状数组是一种维护前缀和的数据结构,可以实现 O ( log n ) O(\log{n}) O(logn)查询一个前缀的和, O ( log n ) O(\log{n}) O(logn) 对原数列的一个位置进行修改。
与前缀和相同的是,树状数组使用与原数列大小相同的数组即可维护
与前缀和不同的是,树状数组的一个位置 i 存储的是从 i 开始,(包括 i)向前 t i t_i ti 个元素的和
t i t_i ti 就是最大的可以整除 i 的2的幂( 2 0 , 2 1 , 2 2 2^0, 2^1,2^2 20,21,22等)
将这些数转化成 2 进制更容易看出规律
因为所有的整数都可以写成二进制形式,比如写成 x = 2 9 + 2 5 + 2 3 x = 2^9 + 2^5 + 2^3 x=29+25+23。我们找最大的可以整除 x 的2的幂,需要保证该数能够整除 2 9 , 2 5 , 2 3 2^9, 2^5, 2^3 29,25,23 中的每一个,因此,这个数一定是这些相加的2的幂中最小的一个。 t i t_i ti 其实就是其二进制最低位的1,与其后的 0 组成的二进制数
从名字上,可以看出是用来找最低位的,也就是二进制最低位的 1 及其后面的 0 组成的数的十进制大小。
我们发现,找一个数二进制的最低位的1及其后的 0,可以通过如下方式来寻找
lowbit(i) = i & i 取反再 + 1
在计算机中,我们将负数,用其正数的补码(取反再加 1)形式来存储。这样的优点是利于计算机实现运算操作。
因此上面的式子可以写成 lowbit(i) = i & (-i)
对于指定位置 x,要查询 [1, x] 的前缀和。我们发现,区间 [1, x] 可以使用一些现有的区间来表示
我们定义树状数组为 s
。
对于一个 x
, S [ x ] = ∑ i ∈ ( x − t x , x ] d [ i ] S[x]=\sum_{i \in\left(x-t_{x}, x\right]} d[i] S[x]=∑i∈(x−tx,x]d[i],若令 y = x − t x y = x - t_x y=x−tx,那么前一个紧邻的区间就可以使用 ( y − t y , y ] (y-t_y, y] (y−ty,y] 来表示。就是 S [ y ] S[y] S[y]。这样不断迭代,就能完整的表示 [1, x] 区间
由于每次执行 x − t x x - t_x x−tx 的操作都会使现在数字 2 进制中的 1 的数量减少 1(从低位向高维减少),因此,最坏情况下需要执行此操作 log 2 x \log_2{x} log2x,所以时间复杂度为 O ( log n ) O(\log{n}) O(logn)。
代码实现,查询的结果就是前缀和
int s[MAXN]
#define lb(x) (x & (-x))
int ask(int x){
int res = 0;
for(int i = x; i >= 1; i -= lb(i))
res += s[i];
return res;
}
如果需要查询一段区间 [l, r] 的和,使用 ask(r) - ask(l - 1)
对于指定位置 x,要将 d[x] 的值增加 v,只需要关心树状数组中哪些位置包含 d[x] 的值,依次进行修改即可
例如:把 d[5] 的值增加 v,则受到影响的点为 s[5], s[6], s[8] 等
树状数组的性质:
若当前节点为 x,且令 x + t x x + t_x x+tx 为其父节点,则树状数组将形成一个树形结构
节点 x 记录区间 ( x − t x , x ] (x - t_x, x] (x−tx,x] 的信息,其子节点记录的区间是 x 记录区间的子集,且子集之间不会相互覆盖
节点 x 记录的区间为节点 y 记录区间的子集,当且仅当 y 是 节点 x 的祖先节点
我们只需要访问 x 所有祖先节点即可
int s[MAXN], n;
#define lb(x) (x & (-x))
void upd(int x, int v){
for(int i = x; i <= n; i += lb(i))
s[i] += v;
}
找逆序对( a i > a j a_i > a_j ai>aj 且 i < j i < j i<j 的有序对)的个数,注意序列中可能有重复的数字
第一行,一个数 n,表示序列中有 n 个数。
第二行 n 个数,表示给定的序列。序列中每个数字不超过 1 0 9 10^9 109。
输出序列中逆序对的数目。
对于 25 % 25\% 25% 的数据, n ≤ 2500 n \leq 2500 n≤2500
对于 50 % 50\% 50% 的数据, n ≤ 4 × 1 0 4 。 n \leq 4 \times 10^4。 n≤4×104。
对于所有数据, n ≤ 5 × 1 0 5 n \leq 5 \times 10^5 n≤5×105
核心思想:假定问题数组为 a
,我们从左向右遍历整个数组,当遍历到 a[i]
时
upd(a[i], 1)
的操作。a[i]
作为逆序对中较大的那个数时,产生的逆序对的数量 i - ask(a[i])
ask(a[i])
得到的值就是索引在前面,且小于等于 a[i]
的数的个数(每次加入比a[i]
小的,都会使树状数组中a[i]
对应的值也增加 1)。i - ask(a[i])
就是索引在前面,且比 a[i]
大的数的个数n < 5000
,a[i] < 10^9
a[i]
排序,再对索引排序来实现#include
using namespace std;
#define lb(x) (x & -x)
#define ll long long
int n;
int t[500005]; // 树状数组
int ranks[500005]; // 记录当前数是第几大的,相同的数会对应不同rank,只要保证先出现的 rank 小于后出现的就可以
ll tot = 0; // 逆序对数目
struct point{
int val; // 记录值
int index; // 记录在原数组中的索引
bool operator < (point a) const{ // 先通过值排序,再通过索引排序
if(a.val == val)
return index < a.index;
return val < a.val;
}
};
point a[500005];
void upd(int x, int v){
for(int i = x; i <= n; i += lb(i))
t[i] += v;
}
ll ask(int x){
ll sum = 0;
for(int i = x; i >= 1; i -= lb(i))
sum += t[i];
return sum;
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++){
scanf("%d", &a[i].val);
a[i].index = i;
}
sort(a + 1, a + 1 + n);
for(int i = 1; i <= n; i++) // 遍历新数组
ranks[a[i].index] = i; // 第 i 大的数是原数组中第 a[i].index 个数
for(int i = 1; i <= n; i++){ // 遍历原数组
upd(ranks[i], 1);
tot += (i - ask(ranks[i]));
}
printf("%lld", tot);
}