码迷,mamicode.com
首页 > 其他好文 > 详细

【题解】CF1227D Optimal Subsequences

时间:2021-04-06 15:16:46      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:names   clu   query   查询   快速定位   特殊   第一个   using   链表   

题意

给定一个长度为 $n$ 的序列 $a_1,a_2,...,a_n$。

有 $m$ 个询问,每个询问给出两个正整数 $k,pos$。你需要找到一个长度为 $k$ 的子序列,且满足如下要求:

  • 该子序列的元素和是所有子序列中最大的;
  • 该子序列是所有满足上一条件的子序列中字典序最小的一个。

对于每个询问,输出该子序列的第 $pos$ 个元素的值。

$1 \le k \le n$,在同一询问中有 $1 \le pos \le k$。

Easy Version:$1 \le n,m \le 100$。

Hard Version:$1 \le n,m \le 200000$

暴力解法

发现简单版本的 $n,m$ 都很小,因此我们可以考虑暴力做法。

不难想出一个贪心策略:将序列 $a$ 从大到小排序,取前 $k$ 个数,那么选出的这 $k$ 个数组成的子序列一定和最大。证明过于简单这里就不写了。

不过这题的 $a$ 序列中可能存在值相同的元素,如何解决它们在原序列的分布问题十分关键。

一般地,设从大到小排序后的第 $k$ 个元素为 $x$。

那么原序列中所有大于 $x$ 的元素都一定会被取到,所有小于 $x$ 的元素都不会被取到。

而原序列中等于 $x$ 的元素,是子序列中值最小的元素,可能只取到一部分

这么一说,序列中不确定的元素只剩下等于 $x$ 的元素了。

而题目说了第二关键字是字典序,所以 $x$ 作为子序列中最小的数,在子序列中的位置应该越前越好。

因此可以设计策略如下:

  • 对于值大于原序列第 $k$ 大的元素,直接收入子序列;
  • 对于值等于原序列第 $k$ 大的元素,优先挑靠前位置的,直到选满 $k$ 个数为止。

代码如下(这里为了简单就搞了个 std::map,复杂度 $O(nm \log \max{a_i}$):

#include <bits/stdc++.h>
#define INF 1e9
#define eps 1e-6
typedef long long ll;
using namespace std;

map <int, int> M;
int n, m, a[110], b[110], seq[110], ss;
bool bb[110];

int main(){

    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
        scanf("%d", &a[i]), b[i] = a[i];
    sort(b + 1, b + n + 1);
    scanf("%d", &m);
    for(int i = 1, k, pos; i <= m; i++){
        scanf("%d%d", &k, &pos);
        for(int j = n; j >= n - k + 1; j--)        // 挑前 k 大
            M[b[j]]++;
        ss = 0;
        for(int j = 1; j <= n; j++)        // 放到原序列找
            if(M[a[j]] > 0)
                M[a[j]]--, seq[++ss] = a[j];
        printf("%d\n", seq[pos]);
    }

    return 0;
}

将问题简单化

假设一开始令一序列 $b$ 与序列 $a$ 完全相同,且之后将序列 $b$ 从大到小排序。

当查询子序列长度为 $k$ 时,序列 $a$ 中大于 $b_k$ 的数一定会被选中,等于 $b_k$ 的数在原序列排得越前,越优先被选入子序列。

这里就不细说了,如果不理解的可以去看上文的暴力解法。

算法的分析

  • 对于每个 $i,j$,快速定位序列 $a$ 中第 $i$ 个等于 $a_j$ 的数的位置

这个不难,因为 $a_i \le 10^9$ 而数字只有 $2 \times 10^5$ 个,所以可以再创建一个序列 $c$ 作为序列 $a$ 的离散化数组。

开 $n$ 个 vector(命名为 v),第 $i$ 个 vector 记录离散化后的值等于 $i$ 的数,在序列 $a$ 的下标。

对于同一个 vector,里面的元素应满足单调递增。

查询序列 $a$ 中第 $i$ 个等于 $a_j$ 的数的位置,只要找 v[c[j]][i - 1] 就行了

  • 关于询问的顺序

如果对每个询问独立进行回答,这个问题会变得困难。

不难看出,询问的 $k_i$ 长度如果递增,一共只需要插入 $n$ 次数字,避免了巨量的增删。

因此,我们可以离线解决这个问题,将操作以 $k$ 从小到大排序。

  • 将长度为 $s$ 的序列扩展到 $s+1$ 的解决办法

如果 b[s] 与 b[s + 1] 相等,那么要在子序列中插入一个之前没被插入过,且在序列 $a$ 中最靠前且等于 b[s] 的数。

否则,只需要插入序列 $a$ 中第一个等于 b[s + 1] 的数的位置

这步的实现见上文「快速定位」的步骤。

  • 将一个数插入序列的某个位置,同时能询问当前序列中第 $pos$ 个数的值

由于我很菜,没往平衡树和块状链表等算法思考,就在这里提供一个较好理解的做法吧。

首先这题特殊的地方在于,我们知道所有即将插入的数值,以及它在最终序列的下标

那换个表示方法,每插入一个数字,就在序列 $a$ 中该数字对应的位置上打一个标记。

如果我们不想让这些没标记的位置添麻烦,我们可以将序列 $a$ 记录标记数量的前缀和。

最后,如果询问到序列第 $pos$ 个位置的话,我们就找第一个前缀和等于 $pos$ 的位置就可以了。这可以二分解决。

至于前缀和的维护,我们可以操作一个树状数组实现。

该算法总复杂度:$O(m \log^2 n)$,瓶颈在于二分 + 树状数组。

代码如下:

#include <algorithm>
#include <cstdio>
#include <vector>
#define INF 1e9
#define eps 1e-6
#define N 200010
typedef long long ll;
using namespace std;

struct query{
    int k, pos, id;
}q[N];
struct S{
    int id, v;
}s[N];
int n, m, a[N], b[N], cnt;
int ss[N], ans[N], t[N];
vector <int> v[N];

bool cmp(S x, S y){
    if(x.v != y.v) return x.v > y.v;
    return x.id < y.id;
}

bool cmpp(query x, query y){
    if(x.k != y.k) return x.k < y.k;
    return x.pos > y.pos;
}

// 树状数组模板

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

void modify(int x){
    while(x <= n)
        t[x]++, x += lowbit(x);
}

int sum(int x){
    int ss = 0;
    while(x >= 1)
        ss += t[x], x -= lowbit(x);
    return ss;
}

int main(){

    scanf("%d", &n);
    for(int i = 1; i <= n; i++)
        scanf("%d", &a[i]), b[i] = a[i];
    scanf("%d", &m);
    // 离散化
    sort(b + 1, b + n + 1);
    cnt = unique(b + 1, b + n + 1) - b - 1;
    for(int i = 1; i <= n; i++){
        a[i] = lower_bound(b + 1, b + cnt + 1, a[i]) - b;
        s[i].id = i, s[i].v = a[i];
        v[a[i]].push_back(i);
    }
    // 排序操作
    sort(s + 1, s + n + 1, cmp);
    for(int i = 1, k, pos; i <= m; i++)
        scanf("%d%d", &q[i].k, &q[i].pos), q[i].id = i;
    sort(q + 1, q + m + 1, cmpp);    // 离线解决
    int nowk = 0;
    for(int i = 1, L, R; i <= m; i++){
        while(nowk < q[i].k)    // 树状数组维护
            nowk++, modify(v[s[nowk].v][ss[s[nowk].v]]), ss[s[nowk].v]++;
        L = 1, R = n;
        while(L < R){        // 二分找答案
            int mid = (L + R) >> 1;
            if(sum(mid) < q[i].pos) L = mid + 1;
            else R = mid;
        }
        ans[q[i].id] = b[a[L]];
    }
    for(int i = 1; i <= m; i++)
        printf("%d\n", ans[i]);

    return 0;
}

【题解】CF1227D Optimal Subsequences

标签:names   clu   query   查询   快速定位   特殊   第一个   using   链表   

原文地址:https://www.cnblogs.com/zengpeichen/p/14617869.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!