【题解】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\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; }
标签:le,return,CF1227D,int,题解,元素,pos,Subsequences,序列 来源: https://www.cnblogs.com/zengpeichen/p/14617869.html