树状数组模板题:楼兰图腾

题目链接:https://www.acwing.com/problem/content/description/243/

题目:

在完成了分配任务之后,西部 314 来到了楼兰古城的西部。

相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(V),一个部落崇拜铁锹(),他们分别用 V 和  的形状来代表各自部落的图腾。

西部 314 在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了 n 个点,经测量发现这 n 个点的水平位置和竖直位置是两两不同的。

西部 314 认为这幅壁画所包含的信息与这 n 个点的相对位置有关,因此不妨设坐标分别为 (1,y1),(2,y2),…,(n,yn),其中 y1∼yn 是 1 到 n 的一个排列。

西部 314 打算研究这幅壁画中包含着多少个图腾。

如果三个点 (i,yi),(j,yj),(k,yk) 满足 1≤i<j<k≤n 且 yi>yj,yj<yk,则称这三个点构成 V 图腾;

如果三个点 (i,yi),(j,yj),(k,yk)满足 1≤i<j<k≤n 且 yi<yj,yj>yk,则称这三个点构成  图腾;

西部 314 想知道,这 n 个点中两个部落图腾的数目。

因此,你需要编写一个程序来求出 V 的个数和  的个数。

输入格式

第一行一个数 n。

第二行是 n个数,分别代表 y1,y2,…,yn。

输出格式

两个数,中间用空格隔开,依次为 V 的个数和  的个数。

数据范围

对于所有数据,n≤200000,且输出答案不会超过 int64。
y1∼yn 是 1 到 n 的一个排列。

输入样例:

5
1 5 3 2 4

输出样例:

3 4

思路:

暴力的方式:

求 ∧的个数

求i左边比a[i]小的值,以及i右边,比a[i]小的值。 然后乘起来,

同样 i + 1的也是如此, i + 2的也是如此。

然后每个对应i上能够组成的总和加起来就是答案。  

V 的个数也是一样。

暴力求的话,时间复杂度为O(n ^ 2).

举例来说明树状数组的用途:将原先O(n)求某段区间的和,变为了O(logn)求这个区间的和。但本质没变,都是求a[1] + a[2] + ....+ a[n]的和。

如存入3.则c[3] += 1,c[4] += 1, c[8] += 1.

而当我们求解1~4的和时, 输出的就是c[4]. c[4]的含义就是以4为终点,长度为lowbit(4)也就是4 的和。(即4,3,2,1出现的总次数)。

而当我们求解2~4的和时,就使用c[4] - c[1]即可。所以依然为1, 因为只有3出现了并只出现了1次。

画图举例:

所以求某个i左边区间中比a[i]小的数个数总和,其实就是求左边这个区间在1~a[i] - 1中数值出现的次数和。 所以将a[1~i-1]的值出现的次数存入到树状数组中, 

如i == 3 , a[1] = 5, a[2] = 6, a[3] = 10,

由 a[1] == 5, 所以对应的5出现次数 + 1,从而树状数组改变,c[5] += 1, c[6] += 1,c[8] += 1.

a[2] == 6,所以对应的6出现次数 + 1, 从而树状数组改变, c[6] += 1, c[8] += 1.

到了a[3]的时候,所以求 1~10 - 1这些数值出现的次数。 于是sum(9) - sum(1 - 1)

c[9] + c[8] - c[0] = 2.

所以可以看出实际上还是求1~9出现次数的和,但是从原先的暴力(1出现次数 + 2出现次数+...+9出现的次数)   优化为了 c[9] + c[8] - c[0]. 

这就是树状树组的优化,由原先的O(n)求区间和, 变为了O(logn)求区间和。

而每修改一次 某个a[]出现的次数 就修改一次c[],主要是因为后面需要通过c[]求区间和,所以修改。

所以是否可以用树状数组对我们每次的暴力求左边区间小于a[i]的次数和进行优化呢?实际上是可以的。

以求 ∧的个数为例,

求i左边的1~i - 1的区间中,小于 a[i]这个值的出现次数和。 

每一次遍历i,都将对应的a[i]值加入到对应树状数组中,也就是a[i]这个值出现次数 + 1,从而使得c[]改变, 当到达了a[i]时,就通过树状数组在O(logn)的时间内求出 1~a[i - 1]出现的总次数。使用sun(a[i - 1]) - sum(1 - 1).

求i左边的1 ~ i - 1的区间中,大于a[i]这个值的出现次数的和。也同上,每次将a[i]的值存入到树状数组中,也就是a[i]值出现次数 + 1, 从而使得c[]改变,当到达a[i]时, 就通过树状数组求

a[i] + 1 ~ n出现的总次数。 sum(n) - sum(a[i] + 1).

从而用树状数组求解所有 i左边 比a[i]小的次数 t1,比a[i]大的次数t2, 右边比a[i]小的次数t3,比a[i]大的次数t4.

每一个i的t1 * t3 的总和。

每一个i的 t2 * t4的总和。

总结:

当求某个区间的总和 并且 需要 单点修改某个点的值时,看是否可以使用树状数组进行优化。

代码实现:

/*
c[i]的含义就是 以i为终点,长度为lowbit(i)的前缀和
*/
# include <iostream>
# include <cstring>
using namespace std;

const int N = 200010;

int a[N];
int c[N];

int lower[N]; //表示左边比第i个位置小的数的个数
int up[N];  //表示左边比第i个位置大的数的个数

int n;

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

void add(int x, int k)
{
    for(int i = x; i <= n; i += lowbit(i)) c[i] += k;
}


int sum(int x)
{
    int res = 0;
    for(int i = x ; i ; i -= lowbit(i))
    {
        res += c[i];
    }
    return res;
}

int main()
{
    scanf("%d",&n);
    for(int i = 1; i <= n ; i++)
    {
        scanf("%d",&a[i]);
    }
    
    for(int i = 1; i <= n ; i++)
    {
        int y = a[i];
        lower[i] = sum(y - 1);  // 找i的左边, 所有 1 ~ y - 1的总个数
        up[i] = sum(n) - sum(y);  // 找到i的左边 所有 y + 1 ~ n的总个数
    
        add(y,1);
    }
    
    memset(c,0,sizeof c); // 将c清空
    
    long long v = 0;
    long long d = 0;
    
    for(int i = n ; i >= 1 ; i--)
    {
        int y = a[i];
        d += (long long)sum(y - 1) * lower[i];
        v += (long long)( sum(n) - sum(y) ) * up[i];
        add(y,1);
    }
    printf("%lld %lld\n",v,d);
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值