算法竞赛进阶指南 0X40数据结构进阶——谜一样的牛

题目链接

AcWing 244. 谜一样的牛 easy

题目描述

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

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

输入格式
  • 1 1 1 行:输入整数 n n n

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

输出格式
  • 输出包含 n n n 行,每行输出一个整数表示牛的身高。

  • i i i 行输出第 i i i 头牛的身高。

数据范围
  • 1 ≤ n ≤ 1 0 5 1≤n≤10^5 1n105
输入样例:

5
1
2
1
0

输出样例:

2
4
5
3
1

解法一:树状数组 + 二分

n n n 头牛,并且它们的身高分别是 1 ∼ n 1 \sim n 1n,所以说它们的身高是互不重复的。

由于从中间开始考虑,难以解决。所以我们可以选择从边界开始考虑,即从最后一头牛开始考虑。假设最后一头牛之前有 A n A_n An 头牛比它矮,那么就可以确定最后一头牛的高度肯定是 A n + 1 A_n + 1 An+1。也就是区间 [ 1 , n ] [1,n] [1,n] 中第 A n + 1 A_n + 1 An+1 小的数。

继续考虑倒数第二头牛,假设倒数第二头牛前面有 A n − 1 A_{n - 1} An1 头牛比它矮,那么倒数第二头牛的高度就应该是剩余区间(区间 [ l , r ] [l,r] [l,r] 去掉了位置 A n + 1 A_n + 1 An+1)中 第 A n − 1 + 1 A_{n-1} + 1 An1+1 小的数。

在实现上我们可以用一个树状数组 c c c 来维护一个初始化全为 1 1 1 的数组 b b b 的前缀和。

对于最后一头牛,我们要先求出它在区间中的位置 A n + 1 A_n + 1 An+1,这个位置也就是它的高度。由于前缀和数组是有序的,所以我们可以利用 二分 的方式快速求出。求出位置 p o s pos pos 之后,记录答案。记录之后,将 c [ p o s ] c[pos] c[pos] 的值加上 − 1 -1 1,并维护它的前缀和,表示这个数已经被选了,剩下的 n − 1 n - 1 n1头牛只能从 剩下的 n − 1 n - 1 n1 个数中选择高度了。

对于每一头牛我们都重复这样的操作,直到结束,最后就能求出每一头牛的高度。

时间复杂度: O ( n × l o g 2 n ) O(n \times log^2n) O(n×log2n)

C++代码:

#include<iostream>
#include<vector>

using namespace std;

const int N = 1e5+10;

int c[N] , a[N];

int n;

inline int lowbit(int x){
    return x & -x;
}

inline int query(int x){
    int ans = 0;
    for(;x;x -= lowbit(x)) ans += c[x];
    return ans;
}

void add(int x,int k){
    for(;x <= n;x += lowbit(x)) c[x] += k;
}



int main(){
    cin>>n;
    
    for(int i = 2;i <= n;++i) scanf("%d",&a[i]);
    //等价于每次都 add(i,1) , 即构建一个初始化全为 1 的数组 的树状数组c
    for(int i = 1;i <= n;++i) c[i] = lowbit(i);
    
    vector<int> ans;
    
    for(int i = n;i >= 1;--i){
        int l = 1 , r = n;
        while(l < r){
            int mid = (l + r) >> 1;
            
            int t = query(mid);
            //求当前的第 i 头牛在剩余区间中第 a[i] + 1 小的位置 , 也就是它对应的高度
            if(t >= a[i] + 1) r = mid;
            else l = mid + 1;
        }
        //l 就是当前牛的高度
        ans.push_back(l);
        //表示这个高度已经被占了 , 剩下的牛只能从剩下的数中选择高度
        add(l , -1);
    }
    
    for(int i = n - 1;i >= 0;--i) cout<<ans[i]<<'\n';
    return 0;
}

解法二:树状数组 + 倍增

我们还是用 c c c 维护初始化全为 1 1 1 数组的前缀和。

对于每一头牛(倒序开始考虑):

  • 初始化两个变量 p o s = 0 , s u m = 0 pos = 0 , sum = 0 pos=0,sum=0
  • 对于 s s s ,倒序枚举这样的一个区间 [ 0 , ⌊ l o g ( n ) ⌋ ] [0 , \lfloor log(n) \rfloor] [0,log(n)⌋]
  • 对于每一个 s s s,如果 p o s + 2 s ≤ n pos + 2^s \leq n pos+2sn 并且 s u m + c [ p o s + 2 s ] < k sum + c[pos + 2^s] < k sum+c[pos+2s]<k,则让 p o s = p o s + 2 s pos = pos + 2^s pos=pos+2s s u m = s u m + c [ p o s + 2 n ] sum = sum + c[pos + 2^n] sum=sum+c[pos+2n]
  • 最后 p o s + 1 pos + 1 pos+1就是我们要求的该头牛的高度。记录答案之后,调用 a d d ( p o s + 1 , − 1 ) add(pos + 1 , -1) add(pos+1,1) 表示该高度已经被选了。

时间复杂度: O ( n × l o g n ) O(n \times logn) O(n×logn)

C++代码:

#include <iostream>
#include <cmath>
#include<vector>
using namespace std;

const int N = 1e5+10;

int n;
int a[N], c[N];

int lowbit(int x)
{
    return x & -x;
}

int query(int x)
{
    int ans = 0;
    for(;x;x -= lowbit(x)) ans += c[x];
    return ans;
}

void add(int x,int k)
{
    for(;x <= n;x += lowbit(x)) c[x] += k;
}


int main()
{
    cin>>n;
    
    for (int i = 2;i <= n;++i) scanf("%d", &a[i]);
    for (int i = 1;i <= n;++i) c[i] = lowbit(i);
    
    int k = log2(n);
    vector<int> ans;
    
    for (int i = n;i >= 1;--i) {
        int pos = 0, sum = 0;
        
        for (int s = k;s >= 0;--s) {
            if (pos + (1 << s) <= n && sum + c[pos + (1 << s)] < a[i] + 1) {
                sum += c[pos + (1 << s)];
                pos += (1 << s);
            }
        }
        
        ans.push_back(pos + 1);
        add(pos + 1 , -1);
    }
    
    for (int i = n - 1;i >= 0;--i) {
        printf("%d\n",ans[i]);
    }
    
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值