\(AcWing\) \(241\) 楼兰图腾
一、题目描述
在完成了分配任务之后,西部 \(314\) 来到了楼兰古城的西部。
相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(\(V\)),一个部落崇拜铁锹(\(∧\)),他们分别用 \(V\) 和 \(∧\) 的形状来代表各自部落的图腾。
西部 \(314\) 在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了 \(n\) 个点,经测量发现这 \(n\) 个点的水平位置和竖直位置是两两不同的。
西部 \(314\) 认为这幅壁画所包含的信息与这 \(n\) 个点的相对位置有关,因此不妨设坐标分别为 \((1,y_1),(2,y_2),…,(n,y_n)\),其中 \(y_1∼y_n\) 是 \(1\) 到 \(n\) 的一个排列。
西部 \(314\) 打算研究这幅壁画中包含着多少个图腾。
如果三个点 \((i,y_i),(j,y_j),(k,y_k)\) 满足 \(1≤i<j<k≤n\) 且 \(y_i>y_j,y_j<y_k\),则称这三个点构成 \(V\) 图腾;
如果三个点 \((i,y_i),(j,y_j),(k,y_k)\) 满足 \(1≤i<j<k≤n\) 且 \(y_i<y_j,y_j>y_k\),则称这三个点构成 \(∧\) 图腾;
西部 \(314\) 想知道,这 \(n\) 个点中两个部落图腾的数目。
因此,你需要编写一个程序来求出 \(V\) 的个数和 \(∧\) 的个数。
输入格式
第一行一个数 \(n\)。
第二行是 \(n\) 个数,分别代表 \(y_1,y_2,…,y_n\)。
输出格式
两个数,中间用空格隔开,依次为 \(V\) 的个数和 \(∧\) 的个数。
数据范围
对于所有数据,\(n≤200000\),且输出答案不会超过 \(int64\)。
\(y_1∼y_n\) 是 \(1\) 到 \(n\) 的一个排列。
二、暴力做法
这题的思路比较容易想到,想要知道某个点为底产生的正(倒)三角形有多少,只要知道该点左右两边比他大(小)的数的数量即可。如果某个点左边比他大的数有\(a\)个,右边比他大的有\(b\)个,则该点为底的倒三角就有\(a * b\)个。如果直接遍历的话每个数字都要找一遍,复杂度为\(O(n^2)\)。 \(n≤200000\),\(2e5*2e5=4e10\),\(c++\)一秒能计算\(1e9\),肯定会超时。
#include <bits/stdc++.h>
//暴力大法好!
// 过掉4/10个数据
using namespace std;
const int N = 2000010;
typedef long long LL;
//快读
inline int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
int a[N];
// ll[i]表示i的左边比第i个数小的数的个数
// rl[i]表示i的右边比第i个数小的数的个数
// lg[i]表示i的左边比第i个数大的数的个数
// rg[i]表示i的右边比第i个数大的数的个数
int ll[N], rl[N], lg[N], rg[N];
int main() {
int n = read();
for (int i = 1; i <= n; i++) a[i] = read(); //纵坐标
//双重循环,暴力求每个坐标左边比自己小,比自己大的个数
for (int i = 1; i <= n; i++)
for (int j = 1; j < i; j++) {
// a[]保存的是1 ~ n的一个排列,不可能相等(题意)
if (a[j] < a[i])
ll[i]++;
else
lg[i]++;
}
//双重循环,暴力求每个坐标右边比自己小的,比自己大的个数
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++) {
if (a[j] < a[i])
rl[i]++;
else
rg[i]++;
}
//利用乘法原理,计算左侧比自己小,右侧比自己小的数量乘积(或比自己大)
LL resV = 0, resA = 0;
for (int i = 1; i <= n; i++) {
resV += (LL)lg[i] * rg[i];
resA += (LL)ll[i] * rl[i];
}
printf("%lld %lld\n", resV, resA);
return 0;
}
三、优化思路
核心思想 : 桶计数+树状数组+前缀和
首先,我们可以发现数的范围不大仅是只有\(1\)到\(n\),最大不超过\(2e5\),那么我们考虑是不是可以在处理每个数的时候,把这个数直接放进对应下标的数组中,然后直接求\(a_i\)到\(n\)有多少个数。那么我们就需要一个方法去达到快速修改数组中的一个数,并且能够快速求出前缀和。
那么,我们不难想到 树状数组 和 线段树 可以用来处理这个问题。
-
对于\(∧\)我们只需要先从\(1\)到\(n\)求一遍比\(a_i\)小的值个数,然后再从\(n\)到\(1\)求一遍比\(a_i\)小的值个数,两者通过乘法原理乘在一起,就是 \(∧\)的个数。
-
题目还要求求一下\(V\)的个数,这个可以通过上面的求解过程中,采用逆向思维来一并求出:
我现在在\(i\)这个位置,值是\(x=a[i]\),比我小的用树状数组求出来了前缀和,计为\(sum(x-1)\),那么比我大的呢?就是\(sum(n)-sum(x)\)个。
四、实现代码
#include <cstdio>
#include <string>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 200010;
int n, a[N]; // n个元素,a[i]代表原始值
int big[N], small[N]; // big[i]:比a[i]大的元素个数,small[i]:比a[i]小的元素个数
LL res1, res2; // V的个数,∧的个数
//快读快写
inline int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
//树状数组模板
int tr[N];
int lowbit(int x) {
return x & -x;
}
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int sum(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
//桶计数+前缀和
int main() {
n = read();
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= n; i++) {
int x = a[i];
//左侧比我小的元素个数 ∧
small[i] = sum(x - 1);
/*
左侧比我大的元素个数 V
(注意:是左侧不是右侧,不是右侧!!!!!!!)
我第一次就是没想明白,以为是求的右侧比我大的,白白浪费了两个小时也没有进展,后来一看,
是左侧比我大的,逆向思维减一下就完事了
*/
// 方法① 【推荐】
big[i] = sum(n) - sum(x); //目前枚举完的总个数-我的个数-左侧比我小的个数=左侧比我大的个数
// 方法② 【不推荐】
// big[i] = i - 1 - small[i];//总数i = 比我小的 + 比我大的 + 我自己,同时由于本题规定各个数字各不相同,
// 所以,不用考虑存在x值是一样的情况,也就是把我扣除外,i-1=比我小的+比我大的
// 但是,这个是本题的要求,不是通用的解决方案,不建议采用此方法,还是上面的前缀和计算差的方法保准。
add(x, 1); //把位置为x的桶内个数+1
}
//重新统计,倒着统计,找出右侧比我小的,比我大的
memset(tr, 0, sizeof tr);
for (int i = n; i; i--) {
int x = a[i];
res1 += (LL)small[i] * sum(x - 1); //右侧比我小+乘法原理 ∧
res2 += (LL)big[i] * (sum(n) - sum(x)); //右侧比我大+乘法原理 V
add(x, 1); //把位置为x的桶内个数+1
}
//输出LL时要注意输出格式%lld
printf("%lld %lld\n", res2, res1);
return 0;
}
五、持续优化
其实,因为本题的题意特殊性,也不用非得循环两次:
本题有个比较好的条件: \(a_1∼a_n\) 是 \(1\) 到 \(n\) 的一个排列, 这就保证了\(a\)的 取值各不相同 且 不重不漏 。
①左边比我小
用树状数组查询出了第\(i\)元素左边比它小的元素个数\(small\)
②左边比我大
\(i\)的左边一共有\(i - 1\)个元素,其中\(small\)个比\(x=a[i]\)小,则剩下的\(i - 1 - small\)个元素就都比它大:
③右边比我小
\(a\)数组里一共\(n\)个元素,范围都不超过\(n\),则比\(a[i]\)小的元素一共\(a[i] - 1\)个,
既然第\(i\)个元素左边有\(small\)个比它小的元素,那么右边比它小的元素个数就是
④右边比我大
同理可求出右边比它大的元素是
这样一来一次查询操作就求出了第\(i\)个元素左右两边比它大和比它小的元素个数了
#include <cstdio>
using namespace std;
const int N = 200010;
//快读
inline int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
typedef long long LL;
int a[N], tr[N];
int n;
int lowbit(int x) {
return -x & x;
}
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int sum(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
LL res1, res2;
int main() {
n = read();
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= n; i++) {
int x = a[i];
int small = sum(x - 1), big = i - 1 - small;
res1 += (LL)big * (n - x - big);
res2 += (LL)small * (x - 1 - small);
add(x, 1);
}
//输出
printf("%lld %lld\n", res1, res2);
return 0;
}
最后这种方法纯粹是本题目的特殊性造成的,并不通用。而\(yxc\)大佬的两次循环,却是一种常用、通用的手段,还是建议同学们学习\(yxc\)大佬的代码思想,不要拘泥于最后这种方法。
办法 | 执行时间 | 运行空间 |
---|---|---|
推公式 | \(142 ms\) | \(2912\) \(KB\) |
两次循环 | \(371 ms\) | \(3292\) \(KB\) |