[题解]CF386B Fly, freebies, fly!

题目链接

#1.0 题目大意

给出一个整数 \(n\) 和一个长度为 \(n\) 的数列 \(\{a_i\}\) 以及一个整数 \(t\),求数列 \(\{a_i\}\) 中最多有几个元素 \(\in[x,x+t]\),其中 \(x\in\{a_i\}.\)

#2.0 朴素做法

打眼一看数据范围很小,可以使用 \(O(n^2)\) 的朴素算法。

我们可以枚举 \(x\) 的取值,之后遍历整个序列,记录答案即可。

const int N = 100010;
const int INF = 0x3fffffff;

int n,t,a[N],cnt,ans;

int main(){
    scanf("%d",&n);
    for (int i = 1;i <= n;i ++)
      scanf("%d",&a[i]);
    scanf("%d",&t);
    for (int i = 1;i <= n;i ++){ //枚举每一个 a[i]
        int r = a[i] + t;cnt = 0;
        for (int j = 1;j <= n;j ++) //遍历数组
          if (a[i] <= a[j] && a[j] <= r)
            cnt ++;
        ans = max(ans,cnt);
    }
    cout << ans;
    return 0;
}

#3.0 线段树优化

我们对 \(O(n^2)\) 的时间复杂度不是很满意,虽然它能过这道题。

朴素算法的时间复杂度瓶颈是 查询有多少数在区间 \([a_i,a_i+T]\),不由得联想到 权值线段树,它可以将单次查询的时间复杂度由 \(O(n)\) 将至 \(O(\log n).\)

不过要注意,我们在建树的时候,权值线段树的维护范围应当为 \([1,M+T]\),其中 \(M=\max\{a_i\}\),这是因为查询时的区间为 \([a_i,a_i + T]\),我们应当避免越界,防止出现未知的错误。

const int N = 100010;
const int MAX = 5000;
const int INF = 0x3fffffff;

struct Node{
    int l,r;
    int ls,rs;
    int sum;
};
Node p[N];

int cnt,n,T,root,a[N],mx,ans;

inline int Max(const int &a,const int &b){
    return a > b ? a : b;
}

inline int create(const int &l,const int &r){ //动态开点
    p[++ cnt].l = l,p[cnt].r = r;
    p[cnt].ls = p[cnt].rs = p[cnt].sum = 0;
    return cnt;
}

inline void insert(int k,int x){ //单点修改
    p[k].sum ++;
    /*因为是统计出现的数的个数,所以所经过的路径上
    的数的总数必然是 +1,之后便不需要 pushup()*/
    if (p[k].l == p[k].r)
      return; //到了叶节点
    int mid = (p[k].l + p[k].r) >> 1;
    if (x <= mid){
        if (!p[k].ls) p[k].ls = create(p[k].l,mid);
        /*如果该点未创建,应当先建点*/
        insert(p[k].ls,x);
    }
    else{
        if (!p[k].rs) p[k].rs = create(mid + 1,p[k].r);
        //同上
        insert(p[k].rs,x);
    }
}

inline int query(int k,int x,int y){//区间查询
    if (x <= p[k].l && p[k].r <= y)
      return p[k].sum; //当前区间被包含,一定是解的一部分
    int mid = (p[k].l + p[k].r) >> 1,res = 0;
    /*下面要将当前区间分为左右两段*/
    /*如果左边的一段与待查询区间有交集,递归查询*/
    if (x <= mid) if (p[k].ls)
      res += query(p[k].ls,x,y);
    /*如果右边的一段与待查询区间有交集,递归查询*/
    if (y > mid) if (p[k].rs)
      res += query(p[k].rs,x,y);
    return res;
}

int main(){
    scanf("%d",&n);
    for (int i = 1;i <= n;i ++){
        scanf("%d",&a[i]);
        mx = Max(mx,a[i]);
    }
    scanf("%d",&T);
    root = create(1,mx + T + 10); 
    //防止超出界限,开大一点
    for (int i = 1;i <= n;i ++)
      insert(root,a[i]); //将点插入
    for (int i = 1;i <= n;i ++) //查询答案
      ans = Max(ans,query(root,a[i],a[i] + T));
    printf("%d",ans);
    return 0;
}

时间复杂度实际为 \(O(n\log (M+T))\),其中 \(M=\max\{a_i\}.\)

#4.0 继续优化

但是我仍旧不满意,单次查询的时间复杂度可不可以优化至 \(O(1)\) 呢?

我们可以对每个数出现的次数进行统计,得到桶数组 b[i],再对 b[i] 做前缀和,得到前缀和数组 sum[i],之后,数列 \(\{a_i\}\) 在区间 \([a_i,a_i+T]\) 内出现的数的个数便是 sum[a[i] + T] - sum[a[i] - 1],注意是 a[i] - 1,因为我们所求的数组是闭区间。

const int N = 100010;
const int INF = 0x3fffffff;

int n,a[N],b[N],sum[N],T,mx,ans;

inline int Max(const int &a,const int &b){
    return a > b ? a : b;
}

int main(){
    scanf("%d",&n);
    for (int i = 1;i <= n;i ++){
        scanf("%d",&a[i]);
        b[a[i]] ++; //记录数量
        mx = Max(mx,a[i]);
    }
    scanf("%d",&T);
    for (int i = 1;i <= mx + T + 1;i ++)
      sum[i] = sum[i - 1] + b[i];//前缀和
    for (int i = 1;i <= n;i ++)
      ans = Max(ans,sum[a[i] + T] - sum[a[i] - 1]);
    printf("%d",ans);
    return 0;
}

时间复杂度应为 \(O(n+M+T)\),其中 \(M=\max\{a_i\}.\)

不过要注意,因为用到了桶,所以如果 \(a_i\) 的值可以特别大,那么这样做便不行了。

亿些有意思的事

\(O(n+M+T)\) 程序跑出的结果:

我:???这个用时???

令人难以理解的题面:

我:???神奇的免费赠品会飞……

End

希望能给您带来帮助。

posted @ 2021-06-04 09:54  Dfkuaid  阅读(73)  评论(1编辑  收藏  举报