• AcWing 241 楼兰图腾


    \(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\)

    \[\large small = sum(a[i] - 1) \]

    ②左边比我大

    \(i\)的左边一共有\(i - 1\)个元素,其中\(small\)个比\(x=a[i]\)小,则剩下的\(i - 1 - small\)个元素就都比它大:

    \[\large big = i - 1 - small \]

    ③右边比我小

    \(a\)数组里一共\(n\)个元素,范围都不超过\(n\),则比\(a[i]\)小的元素一共\(a[i] - 1\)个,
    既然第\(i\)个元素左边有\(small\)个比它小的元素,那么右边比它小的元素个数就是

    \[\large rsmall = a[i] - 1 - small \]

    ④右边比我大

    同理可求出右边比它大的元素是

    \[\large rbig=n - a[i] - big \]

    这样一来一次查询操作就求出了第\(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\)
  • 相关阅读:
    spring cloud图形化dashboard是如何实现指标的收集展示的
    浮躁的我们
    c/c++学习系列之内存对齐
    c/c++学习系列之取整函数,数据宽度与对齐
    c/c++学习系列之memset()函数
    c/c++学习系列之putchar、getchar、puts、gets的运用
    c#学习系列之静态类,静态构造函数,静态成员,静态方法(总之各种静态)
    c#学习系列之字段(静态,常量,只读)
    C#中MessageBox用法大全(附效果图)<转>
    c#学习系列之Application.StartupPath的用法(美女时钟的做法)
  • 原文地址:https://www.cnblogs.com/littlehb/p/16139711.html
Copyright © 2020-2023  润新知