楼兰图腾【按拓扑序讲解】

题目:

在完成了分配任务之后,西部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, 分别统计i位置左边比a[i]小的数的个数m、右边比a[i]小的数的个数n,运用乘法原理:

  1. 第一步从左边 m m m个数中任选一个,有 m m m种选法
  2. 第二步从右边 n n n个数中任选一个,有 n n n种选法
    那么在i位置组成图腾∧的方案数一共是 m ∗ n m * n mn
    累加每个点的方案数,即为所有组成图腾∧的方案总数。

时间复杂度 O ( n 2 ) O(n^2) O(n2)

#include <iostream>
#include <cstdio>

using namespace std;

const int N = 2000010;

typedef long long LL;

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;
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);

    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 = n; j > i; 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;
}

树状数组优化:

优化的地方在于:统计某个点左右两边有特性的点的数量的时候,时间复杂度为:
O ( n 2 ) O(n^2) O(n2)。所以我们要将时间控制在 O ( n ) O(n) O(n) 以内,即统计点的数量的算法控制在 O ( n ) O(n) O(n)以内。这就是我们要优化的地方!

而这里,树状数组支持:单点更新,区间查询,特别动态地维护区间查询。

我们可以采取一种统计顺序,从左往右统计,一边统计,一边插入,即插入后,元素区间和改变,但是下一次统计仍然能有效统计!

  1. 从左向右依次遍历每个数a[i],使用树状数组统计在i位置之前所有比a[i]大的数的个数、以及比a[i]小的数的个数。统计完成后,将a[i]加入到树状数组。

  2. 从右向左依次遍历每个数a[i],使用树状数组统计在 i i i 位置之后所有比a[i]大的数的个数、以及比a[i]小的数的个数。统计完成后,将a[i]加入到树状数组。

  3. 举例解释该循环为什么能统计它左边比小的数的个数和比它大的数的个数:
    因为当 i = 4的时候,说明前面的 1, 2, 3,这三个下标索引的元素都已经通过 add(y,1);插入到树状数组里面去了。而树状数组只需要对他们求一次前缀和即可统计。她们是在 i = 4之前出现的。

总之,通过for循环的从左往右枚举,使之能够正确统计某个数左边比他大的或者比他小的数的个数!

同理:统计某个数右边比他大的数的个数:从右往左循环枚举,使得枚举的数都在它之前出现,由于是从右往左枚举的,所以先它之前出现的数,必然是在它右边的序列中的,比如,枚举到n-2了,说明n-1,和n已经枚举过了,并且出现了,就看是比它大的还是比他小的了!无论你是比n-2大,还是比n-2小,都一定会把你统计,因为在n-2的右边。

核心还是:树状数组维护的底层数组:是1~n;并且取值是0和1,表示不存在和存在!

 for (int i=1; i <= n; i ++)
    {
        int y = a[i];
        great[y] = sum(n) - sum(y);
        lower[y] = sum(y-1);
        add (y, 1);
    }
    
#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

const int N = 2000010;

typedef long long LL;

int n;
//t[i]表示树状数组i结点覆盖的范围和
int a[N], t[N];
//Lower[i]表示左边比第i个位置小的数的个数
//Greater[i]表示左边比第i个位置大的数的个数
int Lower[N], Greater[N];

//返回非负整数x在二进制表示下最低位1及其后面的0构成的数值
int lowbit(int x)
{
    return x & -x;
}

//将序列中第x个数加上k。
void add(int x, int k)
{
    for(int i = x; i <= n; i += lowbit(i)) t[i] += k;
}
//查询序列前x个数的和
int sum(int x)
{
    int res = 0;
    for(int i = x; i; i -= lowbit(i)) res += t[i];
    return res;
}

int main()
{

    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);

    //从左向右,依次统计每个位置左边比第i个数y小的数的个数、以及大的数的个数
    for(int i = 1; i <= n; i++)
    {
        int y = a[i]; //第i个数

        //在前面已加入树状数组的所有数中统计在区间[1, y - 1]的数字的出现次数
        Lower[i] = sum(y - 1); 

        //在前面已加入树状数组的所有数中统计在区间[y + 1, n]的数字的出现次数
        Greater[i] = sum(n) - sum(y);

        //将y加入树状数组,即数字y出现1次
        add(y, 1);
    }

    //清空树状数组,从右往左统计每个位置右边比第i个数y小的数的个数、以及大的数的个数
    memset(t, 0, sizeof t);

    LL res1 = 0, res2 = 0;
    //从右往左统计
    for(int i = n; i >= 1; i--)
    {
        int y = a[i];
        res1 += (LL)Lower[i] * sum(y - 1);
        res2 += (LL)Greater[i] * (sum(n) - sum(y));

        //将y加入树状数组,即数字y出现1次
        add(y, 1);
    }

    printf("%lld %lld\n", res2, res1);

    return 0;
}

思路:(草稿)

题目性质分析:
即给定你一个y序列,这里的x坐标是干扰项,所以着重是y序列坐标!
既然已经知道了尖刀和铁铲的定义,那么我们就应该思考如何去统计它们的个数

1、选中某个点y2,若y1在y2的左边,并且y1>y2, 同时 y3在y2的右边,并且大于y2,则构成
一个向下的尖刀图案!这就是一个尖刀!假若y2左边有很多的y1,y2右边有很多的y3,那么
如何快速统计这些 y1, y2, y3构成的尖刀个数呢?
    乘法原理,左边符合要求的y1的个数 * 右边符合要求的y3的个数,即为尖刀的个数!
2、那么如何求解y1,y3的个数呢?
    
    暴力:
    这就需要分别统计每个数左边比它大(小)的数,每个数右边比它大(小)的数个数了
    若用一个循环每次去固定每个数,然后一个内层循环去枚举这个固定的数的左边,或者
    右边的符合要求的数,则是O(n^2)的时间复杂度,容易超时!
    
    树状数组:
    将y序列作为基层,即被管理层,树状数组维护每个y出现的次数!
        
    那么如何判断当前这个数是否在某个固定的数的左边或者右边呢?
        假如求某个固定的数左边比它大的数,其实这里换句话说,是求某个数之前出现
        的数里比它大的数的个数,那么我们只需要从左往右枚举,即可以保证当前
        比固定的数大的数,都是在它之前出现的!
        举例:假设当前求y3左边比它大的数,从左往右枚举,那么y1,y2必然已经枚举了
        并且通过树状数组记录了y1,y2出现的次数。然后对于查询y3左边比它大的数
        那么我们就可以通过树状数组的区间查询即可获知左边比y3大的数个数了,既然是
        区间查询,那么这个区间肯定有限制啊,比y3大的,那么区间就是 [y3+1,n];
        即查询左边出现的数里,有无处于[y3+1, n]的数,同时树状数组维护的是次数
        不是数值,所以求个前缀和,即可统计比y3之前的比y3大的数的个数!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值