树状数组将线性结构转化成树状结构,在单点修改和前缀和(可以O(1) 求得区间和)方面表现优异(log级),相比线段树,应用范围窄,代码短,好写。
基本知识
- 一个数字(非0)与它的相反数与,结果是二进制下这个数最靠右的1的位置,操作称作lowbit。
1
| inline int lowbit(int x) { return x & -x; }
|
- 树状数组的存放规则:第k位存放从k往前数lowbit(k)个数的和
基本操作:单点修改,区间查询
目标
对于一个数组,支持在线单点修改操作与区间和查询操作
单点修改
下标不断+自己的lowbit,更新该下标的值,直到n为止
1 2 3 4 5 6 7 8 9
| void update(int x, int k) { if(x < 0) return; while (x <= n) { tree[x] += k; x += lowbit(x); } }
|
区间查询
查询[1,x] 区间和,x作为下标不断减去自己的lowbit,直到0 ,每次将tree[x]
加入答案中,利用前缀和可以求出任意区间[x,y] 的和query(y)-query(x-1)
1 2 3 4 5 6 7 8 9 10
| int query(int x) { int sum = 0; while (x > 0) { sum += tree[x]; x -= lowbit(x); } return sum; }
|
改进:实现区间修改与区间查询
-
设差分数组di ,则每个点的值ai=∑i=1ndi 。
-
区间修改[l,r] :每个元素+k ,即dl←dl+k,dr+1←dr+1−k ,将差分数组建成树状数组,则区间修改对应修改单点值。
-
单点查询:求[1,x] 区间和即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
for (int i = 1; i <= n; i++) { cin >> val; update(i, val); update(i + 1, -val); }
update(l, k); update(r+1, -k);
cout << query(x) << endl;
for (int i = 1; i <= n; i++) cin >> a[i];
cout << a[x] + query(x) << endl;
|
- 区间查询:∑i=1nai=∑i=1n∑j=1idj=∑i=1n(n−i+1)di=(n+1)∑i=1ndi−∑i=1nidi ,维护两个树状数组,di 和idi ,修改相应单点修改的参数就可以了。
1 2 3 4 5 6 7 8 9 10
| T1.update(l, k); T1.update(r + 1, -k); T2.update(l, l * k); T2.update(r + 1, (r + 1) * (-k));
inline int query(int x) { return (x + 1) * T1.query(x) - T2.query(x); }
|
题目
洛谷P5459
洛谷P6186
洛谷P6186 [NOI Online #1 提高组] 冒泡排序
逆序对与冒泡排序性质的研究+权值树状数组,两种方法
题解首发于CSDN博客2022.1.8 NOI Online #1 提高组] 冒泡排序:【冒泡排序】与【逆序对】问题default111的博客-CSDN博客
题意
n 元排列pi ,两种操作:
1 x
:交换当前排列中x 和x+1 位置的数。
2 x
:输出当前排列经过x 轮冒泡排序交换后的逆序对个数。
输入输出
输入:单组数据,n 排列长度,m 操作数,n 元排列,m 个操作。
输出:每个一行,操作2 的结果。
数据范围
2≤n,m≤2e5 ,操作2 中x 在int 范围,其他输入数保证合法。
分析
开始毫无头绪,我太笨了,看题解都看了好久,把两种方法整理一下TAT
从逆序对和冒泡排序的本质出发:
记原排列为a[1...n],b[i] 表示a[i] 左边比它大的数的个数,则逆序对的个数为∑i=1nb[i] 。
考虑操作二,每一轮冒泡排序,若b[1...n] 不为0 则减1,则第x 轮的逆序对个数为:
i=1∑nb[i]−i=1∑nmin(b[i],x)=b[i]>x∑b[i]−xb[i]>x∑1
-
方法一:右边的式子是很明显的两颗权值线段树!
-
方法二:观察第一个式子,第一项可以在读入数据时建立权值线段树求解得到;第二项f(x)=∑i=1nmin(b[i],x)=f(x−1)+b[i]≥x的个数 得到。因为0≤b[i]<n ,开一个桶d[c] 记录b[1...n]==c 的个数,则f(x)=f(x−1)+∑i=xn−1d[i]=f(x−1)+n−∑i=0x−1d[i],其中f(0)=0。有了f(x)−f(x−1)=n−∑i=0x−1d[i],建一棵线段树/树状数组,每次求区间和就可以得到f(x) 了!而如果我们把线段树/树状数组略作修改,将第一个节点设为∑i=1nb[i],并依次加入∑i=0x−1d[i]−n 节点,就可以通过一个直接的[1...x] 区间求和得到答案!
至此,仅操作2 ,即对于一个序列求任意轮冒泡排序后逆序对个数的问题解决了。
操作1 会产生什么影响呢?只交换相邻两个,只能影响到它们俩有关的量。
- 方法一:直接更新两棵权值线段树就好了!
- 方法二:考虑交换a[i],a[i+1] (同步交换b 数组),初始逆序对个数发生±1 的改变;另外相当于把d 的两个相邻的桶(d[b[i]]和d[b[i]−1],或d[b[i+1]]和d[b[i+1]+1]) ,做了修改(左+1右-1或反过来),那么只需要修改左边那个桶对应的上述树状数组的节点值即可!
至此,分析完了(吐血)。
代码
注意long long
方法一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| int n,m; int a[N],b[N]; struct BIT { ll tree[N]; inline int lowbit(int x) { return x & (-x); } inline void init() { memset(tree,0,sizeof(tree)); } inline void update(int x,int val=1) { if(!x) return; while(x<=n) { tree[x]+=val; x+=lowbit(x); } } inline ll query(int x) { ll sum=0; while(x>0) { sum+=tree[x]; x-=lowbit(x); } return sum; } inline ll query(int l,int r) { return query(r)-query(l-1); } }origin,number; int main() { read(n),read(m); for(int i=1;i<=n;i++) { read(a[i]); b[i]=i-1-origin.query(a[i]-1); origin.update(a[i]); } origin.init(); for(int i=1;i<=n;i++) { origin.update(b[i],b[i]); number.update(b[i]); } int op,x; for(int i=1;i<=m;i++) { read(op),read(x); if(op==2) printf("%lld\n",x>=n?0:origin.query(x+1,n)-x*number.query(x+1,n)); else { if(a[x]>a[x+1]) { origin.update(b[x+1],-b[x+1]); number.update(b[x+1],-1); b[x+1]--; origin.update(b[x+1],b[x+1]); number.update(b[x+1],1); } else { origin.update(b[x],-b[x]); number.update(b[x],-1); b[x]++; origin.update(b[x],b[x]); number.update(b[x],1); } swap(a[x],a[x+1]); swap(b[x],b[x+1]); } } return 0; }
|
方法二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| int n,m; int a[N],b[N],d[N]; ll tree[N]; inline void update(int x,ll val=1) { if(!x) return; while(x<=n) { tree[x]+=val; x+=lowbit(x); } } inline ll query(int x) { ll sum=0; while(x>0) { sum+=tree[x]; x-=lowbit(x); } return sum; } int main() { read(n),read(m); ll cnt=0; for(int i=1;i<=n;i++) { read(a[i]); b[i]=i-1-query(a[i]-1); update(a[i]); d[b[i]]++; cnt+=b[i]; } memset(tree,0,sizeof(tree)); update(1,cnt); cnt=0; for(int i=0;i<n;i++) { cnt+=d[i]; update(i+2,cnt-n); } int op,x; for(int i=1;i<=m;i++) { read(op),read(x); if(op==2) printf("%lld\n",x>=n?0:query(x+1)); else { if(a[x]>a[x+1]) { update(1,-1); b[x+1]--; update(b[x+1]+2); } else { update(1); update(b[x]+2,-1); b[x]++; } swap(a[x],a[x+1]); swap(b[x],b[x+1]); } } return 0; }
|