本文主要是介绍树状数组模板题:楼兰图腾,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
题目链接: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;
}
这篇关于树状数组模板题:楼兰图腾的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!