树状数组求逆序对_树状数组

1,树状数组引入:(转载自链接:https://zhuanlan.zhihu.com/p/25185969

(1)lowbit

int lowbit(int x)
{
	return x&-x;
}

lowbit 它只保留"从低位向高位数,第一个数字1"作为运算结果

比如二进制数00011100,结果就是00000100,也就是4

438c238bff570115c45fb874846bed7b.png

我们每次执行操作(把位置x的值+k),只需要把"能管理到x的所有位置"都+k就行

那么怎样快速找到哪些位置能管理到x呢?

答案还是lowbit

我们先更新x,然后把x赋给一个新值,x+lowbit(x),那么新值依然可以管理到x;

(这样,每个包含x点的树状数组,都会+k)

单点修改

void add(int x,int k)
{
	while(x<=n)
	{
		tree[x]+=k;
		x+=lowbit(x);
	}
}     //维护树状数组

(2)求某区间所有数的和

int sum(int x)
{
	int ans=0;
	while(x!=0)
	{
		ans+=tree[x];
		x-=lowbit(x);
	}
	return ans;
}

//询问区间[L,R]的和sum(L,R)。我们只需要求出sum(1,R)和sum(1,L-1),

//然后sum(1,R)-sum(1,L-1)就是sum(L,R)了

那么对于任意的x,sum(1,x)怎么求呢? (解释函数 int sum)

我们把最终得到的答案存在ans变量中,执行下面的操作:

(1)ans初始化为0

(2)ans加上x位置的值

(3)给x赋予新值 x-lowbit(x)

(4)如果x>0则跳回操作(2),否则结束算法。

举个例子介绍一下:
还记得吗,我们在进行"给原数组第x位置的数增加k"这个操作时,把"能管理到x的所有位置"都增加了k。
那么,对于任意一个位置,树状数组里的值就是"它能管理到的所有位置上,原数组的值之和"。
因此我们给答案加上树状数组第x位置的值,这里就得到了sum(5,6),因为6能管理[5,6]
然后给x减去lowbit(x),得到4。再加上x位置的值,也就是sum(1,4),因为4能管理[1,4]
再让x=x-lowbit(x),得到0,由于不再大于0,算法终止,得到答案。
这时答案恰好是sum(1,6)
依然可以证明,最多只需要进行log级别次数的查询。

(3)将某区间每一个数数加上 x;;求出某一个数的值。

差分:设数组a[]={1,6,8,5,10},那么差分数组b[]={1,5,2,-3,5}

也就是说b[i]=a[i]-a[i-1];(a[0]=0;),那么a[i]=b[1]+....+b[i];

对区间[x,y]进行修改,只用修改b[x]与b[y+1]:[1]

问题解决:用差分的方法,区间[l,r]所有值+k改成"位置l加上k,位置r+1减去k"

高级树状数组用法 (高级树状数组--区间修改区间查询、二维树状数组 - 胡小兔 - 博客园)

(这些足以应对线段树)

1.单点修改 + 区间查询

最简单的树状数组就是这样的:

void add(int p, int x){ //给位置p增加x
    while(p <= n) sum[p] += x, p += lowbit(p);
}
int ask(int p){ //求位置p的前缀和
    int res = 0;
    while(p) res += sum[p], p -= lowbit(p);
    return res;
}
int range_ask(int l, int r){ //区间求和
    return ask(r) - ask(l - 1);
}

2.区间修改 + 单点查询

通过“差分”(就是记录数组中每个元素与前一个元素的差),可以把这个问题转化为问题1。

查询

设原数组为a[i]a[i], 设数组d[i]=a[i]−a[i−1](a[0]=0)d[i]=a[i]−a[i−1](a[0]=0),则 a[i]=∑ij=1d[j]a[i]=∑j=1id[j],可以通过求d[i]d[i]的前缀和查询。

修改

当给区间[l,r][l,r]加上x的时候,a[l]a[l] 与前一个元素 a[l−1]a[l−1] 的差增加了xx,a[r+1]a[r+1] 与 a[r]a[r] 的差减少了xx。根据d[i]d[i]数组的定义,只需给d[l]d[l] 加上 xx, 给d[r+1]d[r+1] 减去 xx 即可。

void add(int p, int x){ //这个函数用来在树状数组中直接修改
    while(p <= n) sum[p] += x, p += p & -p;
}
void range_add(int l, int r, int x){ //给区间[l, r]加上x
    add(l, x), add(r + 1, -x);
}
int ask(int p){ //单点查询
    int res = 0;
    while(p) res += sum[p], p -= p & -p;
    return res;
}

3. 区间修改 + 区间查询

这是最常用的部分,也是用线段树写着最麻烦的部分——但是现在我们有了树状数组!

怎么求呢?我们基于问题2的“差分”思路,考虑一下如何在问题2构建的树状数组中求前缀和:

位置p的前缀和 =

∑i=1pa[i]=∑i=1p∑j=1id[j]∑i=1pa[i]=∑i=1p∑j=1id[j]

在等式最右侧的式子∑pi=1∑ij=1d[j]∑i=1p∑j=1id[j]中,d[1]d[1] 被用了pp次,d[2]d[2]被用了p−1p−1次……那么我们可以写出:

位置p的前缀和 =

∑i=1p∑j=1id[j]=∑i=1pd[i]∗(p−i+1)=(p+1)∗∑i=1pd[i]−∑i=1pd[i]∗i∑i=1p∑j=1id[j]=∑i=1pd[i]∗(p−i+1)=(p+1)∗∑i=1pd[i]−∑i=1pd[i]∗i

那么我们可以维护两个数组的前缀和:
一个数组是 sum1[i]=d[i]sum1[i]=d[i],
另一个数组是 sum2[i]=d[i]∗isum2[i]=d[i]∗i。

查询

位置p的前缀和即: (p + 1) * sum1数组中p的前缀和 - sum2数组中p的前缀和。

区间[l, r]的和即:位置r的前缀和 - 位置l的前缀和。

修改

对于sum1数组的修改同问题2中对d数组的修改。

对于sum2数组的修改也类似,我们给 sum2[l] 加上 l * x,给 sum2[r + 1] 减去 (r + 1) * x。

void add(ll p, ll x){
    for(int i = p; i <= n; i += i & -i)
        sum1[i] += x, sum2[i] += x * p;
}
void range_add(ll l, ll r, ll x){
    add(l, x), add(r + 1, -x);
}
ll ask(ll p){
    ll res = 0;
    for(int i = p; i; i -= i & -i)
        res += (p + 1) * sum1[i] - sum2[i];
    return res;
}
ll range_ask(ll l, ll r){
    return ask(r) - ask(l - 1);
}

4. 二维树状数组

我们已经学会了对于序列的常用操作,那么我们不由得想到(谁会想到啊喂)……能不能把类似的操作应用到矩阵上呢?这时候我们就要写二维树状数组了!

在一维树状数组中,tree[x](树状数组中的那个“数组”)记录的是右端点为x、长度为lowbit(x)的区间的区间和。
那么在二维树状数组中,可以类似地定义tree[x][y]记录的是右下角为(x, y),高为lowbit(x), 宽为 lowbit(y)的区间的区间和。

单点修改 + 区间查询

void add(int x, int y, int z){ //将点(x, y)加上z
    int memo_y = y;
    while(x <= n){
        y = memo_y;
        while(y <= n)
            tree[x][y] += z, y += y & -y;
        x += x & -x;
    }
}
void ask(int x, int y){//求左上角为(1,1)右下角为(x,y) 的矩阵和
    int res = 0, memo_y = y;
    while(x){
        y = memo_y;
        while(y)
            res += tree[x][y], y -= y & -y;
        x -= x & -x;
    }
}

区间修改 + 单点查询

我们对于一维数组进行差分,是为了使差分数组前缀和等于原数组对应位置的元素。

那么如何对二维数组进行差分呢?可以针对二维前缀和的求法来设计方案。

二维前缀和:

sum[i][j]=sum[i−1][j]+sum[i][j−1]−sum[i−1][j−1]+a[i][j]sum[i][j]=sum[i−1][j]+sum[i][j−1]−sum[i−1][j−1]+a[i][j]

那么我们可以令差分数组d[i][j]d[i][j] 表示 a[i][j]a[i][j] 与 a[i−1][j]+a[i][j−1]−a[i−1][j−1]a[i−1][j]+a[i][j−1]−a[i−1][j−1] 的差。

例如下面这个矩阵

1  4  8
 6  7  2
 3  9  5

对应的差分数组就是

1  3  4
 5 -2 -9
-3  5  1

当我们想要将一个矩阵加上x时,怎么做呢?
下面是给最中间的3*3矩阵加上x时,差分数组的变化:

0  0  0  0  0
0 +x  0  0 -x
0  0  0  0  0
0  0  0  0  0
0 -x  0  0 +x

这样给修改差分,造成的效果就是:

0  0  0  0  0
0  x  x  x  0
0  x  x  x  0
0  x  x  x  0
0  0  0  0  0

那么我们开始写代码吧!

void add(int x, int y, int z){ 
    int memo_y = y;
    while(x <= n){
        y = memo_y;
        while(y <= n)
            tree[x][y] += z, y += y & -y;
        x += x & -x;
    }
}
void range_add(int xa, int ya, int xb, int yb, int z){
    add(xa, ya, z);
    add(xa, yb + 1, -z);
    add(xb + 1, ya, -z);
    add(xb + 1, yb + 1, z);
}
void ask(int x, int y){
    int res = 0, memo_y = y;
    while(x){
        y = memo_y;
        while(y)
            res += tree[x][y], y -= y & -y;
        x -= x & -x;
    }
}

区间修改 + 区间查询

类比之前一维数组的区间修改区间查询,下面这个式子表示的是点(x, y)的二维前缀和:

77a1cd8c156dfb8bc25e66dc9b69fba6.png

那么我们要开四个树状数组,分别维护:

d[i][j],d[i][j]∗i,d[i][j]∗j,d[i][j]∗i∗jd[i][j],d[i][j]∗i,d[i][j]∗j,d[i][j]∗i∗j

这样就完成了!

#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
typedef long long ll;
ll read(){
    char c; bool op = 0;
    while((c = getchar()) < '0' || c > '9')
        if(c == '-') op = 1;
    ll res = c - '0';
    while((c = getchar()) >= '0' && c <= '9')
        res = res * 10 + c - '0';
    return op ? -res : res;
}
const int N = 205;
ll n, m, Q;
ll t1[N][N], t2[N][N], t3[N][N], t4[N][N];
void add(ll x, ll y, ll z){
    for(int X = x; X <= n; X += X & -X)
        for(int Y = y; Y <= m; Y += Y & -Y){
            t1[X][Y] += z;
            t2[X][Y] += z * x;
            t3[X][Y] += z * y;
            t4[X][Y] += z * x * y;
        }
}
void range_add(ll xa, ll ya, ll xb, ll yb, ll z){ //(xa, ya) 到 (xb, yb) 的矩形
    add(xa, ya, z);
    add(xa, yb + 1, -z);
    add(xb + 1, ya, -z);
    add(xb + 1, yb + 1, z);
}
ll ask(ll x, ll y){
    ll res = 0;
    for(int i = x; i; i -= i & -i)
        for(int j = y; j; j -= j & -j)
            res += (x + 1) * (y + 1) * t1[i][j]
                - (y + 1) * t2[i][j]
                - (x + 1) * t3[i][j]
                + t4[i][j];
    return res;
}
ll range_ask(ll xa, ll ya, ll xb, ll yb){
    return ask(xb, yb) - ask(xb, ya - 1) - ask(xa - 1, yb) + ask(xa - 1, ya - 1);
}
int main(){
    n = read(), m = read(), Q = read();
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            ll z = read();
            range_add(i, j, i, j, z);
        }
    }
    while(Q--){
        ll ya = read(), xa = read(), yb = read(), xb = read(), z = read(), a = read();
        if(range_ask(xa, ya, xb, yb) < z * (xb - xa + 1) * (yb - ya + 1))
            range_add(xa, ya, xb, yb, a);
    }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++)
            printf("%lld ", range_ask(i, j, i, j));
        putchar('n');
    }
    return 0;
}

参考

  1. ^概念引入
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值