• AcWing 244 迷一样的牛


    \(AcWing\) \(244\) 迷一样的牛

    一、题目描述

    \(n\) 头奶牛,已知它们的身高为 \(1∼n\)各不相同,但不知道每头奶牛的具体身高。

    现在这 \(n\) 头奶牛站成一列,已知第 \(i\) 头牛前面有 \(A_i\)头牛比它低,求每头奶牛的身高。

    输入格式

    \(1\) 行:输入整数 \(n\)
    \(2..n\) 行:每行输入一个整数 \(A_i\),第 \(i\) 行表示第 \(i\) 头牛前面有 \(A_i\) 头牛比它低。
    (注意:因为第 \(1\) 头牛前面没有牛,所以并没有将它列出)

    输出格式

    输出包含 \(n\) 行,每行输出一个整数表示牛的身高。

    \(i\) 行输出第 \(i\) 头牛的身高。

    数据范围
    \(1≤n≤10^5\)

    二、解题方法

    这个题意不太好理解,我尝试理解一下:
    每头牛都只关心 自己前面比自己短的牛个数
    最直观的办法是看测试样例:

    //输入样例
    5
    1
    2
    1
    0
    //输出样例
    2
    4
    5
    3
    1
    

    \(5\)头牛,第一头牛没有前面的其它牛,没有给出它前面有多少头牛比自己低,有数字也只能是\(0\),题目就没有给出,所以我们看到的1,2,1,0是从第二头牛开发的。

    第二头牛 第三头牛 第四头牛 第五头牛
    向前看 \(1\) \(2\) \(1\) \(0\)

    我认为,凡是这类一开始时,所求答案完全未知的问题,应该在 边界问题 上寻找原问题的 突破口 。因为对于那些靠近问题空间 中部 的子问题,其 左右两边 可以认为和它是本质相同的子问题,没有办法直接解决。因此我们应该 着重 考虑边界处的子问题

    这东西 不能从前向后去尝试,原因很简单,比如以第三头牛为例,它知道自己前面有\(2\)个比自己小的,但是不知道自己后面有几个比自己小的,也就不知道自己在整体中是第几,确定不下来。

    这东西 可以从右向前去尝试,为啥呢?

    在本题中,我们 首先考虑最后一头牛。由于它已经统计了在它之前的所有牛,因此,假如它比\(x\)头牛高,则它的高度一定是\(x+1\)

    我们 采取从后往前考虑的方式,就是因为题目给出了 每头牛已知在自己前面比自己矮的牛的个数 这一条件,从后往前可以少考虑很多问题

    由于每头牛的高度各不相同 且取遍区间\([1,n]\),因此,对于倒数第二头牛而言,它应该在除去上述\(x+1\)的区间\([1,n]\)中,选取\(A_{n−1}+1\)小的数。其他的牛以此类推。

    换句人话 就是直接从后向前跑一下简单的测试用例,就好理解了:

    • 第五头牛知道前面有\(0\)头牛比自己小,自己当然是\(1\)
    • 第四头牛知道前面有\(1\)头牛比自己小,这还不够,因为还需要知道后面是不是有比自己小的啊!这个好办,因为第五头牛高度是\(1\)已经确定,可以不再考虑它,把它从牛群中牵走,它的离开,并不会影响第四头牛的心情,因为第五头本来就是在第四头的后面,它走了,第四头牛的信息"前面有\(1\)个比我小" 并没有受到影响,此时,它还是最后一头牛,可以不用考虑后面有比自己小的,直接用前面比我小的信息就够了~!由于第五头把\(1\)这个高度拿走了,所以,现在剩下的就是2 3 4 5,前面有\(1\)个比自己小,那前面小的肯定是\(2\),自己是\(3\)
    • 同理,再把第四头牛牵走,剩下的子问题是一样一样的~
    • 用循环啥的一个个牵走吧,问题就解决了

    方法① 两重循环枚举

    先从后往前枚举每头牛,当枚举到第\(i\)头牛时,再从\(1\)枚举到\(n\),找到第\(h[i] + 1\)个没有使用过的数,再对该数进行标记,继续枚举下一头牛,时间复杂度\(O(n^2)\)\(n<=1e5\),超时妥妥的

    #include <cstdio>
    #include <string>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    //通过了 7/10个数据
    
    const int N = 1e5 + 10;
    int h[N];
    int st[N];
    int ans[N];
    int n;
    //快读
    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 main() {
        n = read();
        for (int i = 1; i <= n; i++) st[i] = 1; //初始化st数组
    
        for (int i = 2; i <= n; i++) h[i] = read(); //第一个放过,不讨论,输进来也是0,不输也是0
    
        for (int i = n; i >= 1; i--) { //从后向前枚举每头牛
            int cnt = 0;
            for (int j = 1; j <= n; j++) {
                if (st[j]) cnt++;
                if (cnt == h[i] + 1) { //寻找第h[i]+1个未被占据的位置
                    st[j] = 0;         //标识此位置被某个奶牛占据过了
                    ans[i] = j;        //记录i号奶牛占据的是j号位置
                    break;
                }
            }
        }
        for (int i = 1; i <= n; i++) printf("%d\n", ans[i]);
        return 0;
    }
    

    方法②、树状数组 + 二分

    树状数组的应用:倒序多次查找第\(k\)小的数

    既然暴力无法过掉所有的用例,就只能考虑优化一下了

    假如建立一个全部元素为\(1\)的数列,某个位置的数为\(1\)代表这个高度还不知道是哪头牛的,那么就 用树状数组维护该数列的前缀和 ,若某个位置的前缀和等于\(A_{i}+1\),此时的下标就是要找的数。选择这个数后,将相应位置的\(1\)\(0\).可以二分这个位置。

    【单点修改+区间前缀和查询】

    在所有满足 \(sum(x) = k\) 情况中,通过二分找到最小的 \(x\) ,如图所示

    时间复杂度 \(O(nlogn)\)

    #include <cstdio>
    #include <string>
    #include <cstring>
    #include <algorithm>
    #include <iostream>
    
    using namespace std;
    
    const int N = 100010;
    int a[N];
    int ans[N];
    int n;
    
    //树状数组模板
    int tr[N];
    #define lowbit(x) (x & -x)
    void add(int x, int k) {
        for (int i = x; i <= n; i += lowbit(i)) tr[i] += k;
    }
    int sum(int x) {
        int res = 0;
        for (int i = x; i; i -= lowbit(i)) res += tr[i];
        return res;
    }
    
    int main() {
        //加快读入
        ios::sync_with_stdio(false), cin.tie(0);
        cin >> n;
        for (int i = 2; i <= n; i++) cin >> a[i]; //读入每头奶牛前面有几头奶牛比自己矮
        for (int i = 1; i <= n; i++) add(i, 1);   //利用树状数组,把1~n之间每个位置设置为1,方便后面用来求前缀和
        for (int i = n; i >= 1; i--) {
            int l = 1, r = n;
            while (l < r) {           //用二分找出前缀和恰好为a[i] + 1的数
                int mid = l + r >> 1; // mid枚举的是下标
                if (sum(mid) >= a[i] + 1)
                    r = mid; //再小点试试
                else
                    l = mid + 1; //再大点~
            }
            ans[i] = l; //记录答案
            add(l, -1); //找到后,这个数-1,标识为0,方便下次求前缀和
        }
        //输出结果
        for (int i = 1; i <= n; i++) printf("%d\n", ans[i]);
        return 0;
    }
    
  • 相关阅读:
    String.PadLeft()
    数据生成树 新增
    SQL允许你用EXECUTE执行一个变量中定义的SQL语句,并且允许你在被执行的SQL语句中,再次嵌套入一个变量定义的语句,并且再次在其中用EXECUTE执行它
    获取SqlConnection的统计信息
    页面缓存 OutputCache
    将小写金额转换成大写
    判断是否枚举中的匹配项
    js获取系统时间的几种方法<一> 《网摘学习》
    将指定文件夹(路径)下的所有内容copy到目标文件夹(路径)下的方法
    50个优美的句子<摘自网上>
  • 原文地址:https://www.cnblogs.com/littlehb/p/16144865.html
Copyright © 2020-2023  润新知