zoukankan      html  css  js  c++  java
  • 【题解】CF1227D Optimal Subsequences

    题意

    给定一个长度为 $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
    ", 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 imes 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
    ", ans[i]);
    
        return 0;
    }
  • 相关阅读:
    【板子】博弈论
    【洛谷】P1229快速幂
    【洛谷】P1349广义斐波那契
    2018.11.15 Nginx服务器的使用
    2018.11.14 hibernate中的查询优化---关联级别查询
    2018.11.13 Hibernate 中数据库查询中的Criteria查询实例
    2018.11.12 Spring事务的实现和原理
    2018.11.11 Java的 三大框架:Struts+Hibernate+Spring
    2018.11.10 Mac设置Eclipse的 .m2文件夹是否可见操作&&Mac系统显示当前文件夹的路径设置
    2018.11.9 Dubbo入门学习
  • 原文地址:https://www.cnblogs.com/zengpeichen/p/14617869.html
Copyright © 2011-2022 走看看