区间第 k 大(计数+链表)
NOIP2021 模拟赛,\(\text{tyy}\) 的题,爆大零,打了个水分暴力就跳了。
题目内容
给定一个 \(1\sim n\) 的排列 \(\{p_i\}\) 以及整数 \(k\)。
对于每个 \(i \in [1,n]\),你需要求出该排列中有多少个区间 \([l,r]\) 的第 \(k\) 大恰好是 \(p_i\)。
\(1\leq n\leq 3\times10^5\),\(1\leq k\leq 100\)。
解题思路
对每一个 \(p_i\) 求有多少个区间满足条件。在符合条件的区间 \([l,r]\) 内,如果 \(p_i\) 左边有 \(j\) 个数大于它,那么在右边显然有 \(k-j-1\) 个数大于它。所以枚举 \(j\),设 \(f_j\) 表示 \(p_i\) 左边比 \(p_i\) 大的第 \(j\) 个数的位置,\(g_j\) 表示 \(p_i\) 右边比 \(p_i\) 大的第 \(j\) 个数的位置,则每个 \(j\) 对答案的贡献为 \((f_j-f_{j-1})\times (g_{k-j}-g_{k-j-1})\)。
要预处理 \(f,g\) 两个数组,也就是要找到 \(p_i\) 左右各 \(k\) 个比它大的数。如果你 \(\mathcal{O(n^2)}\) 的做这件事只能拿 \(50\) 分,如果用线段树/平衡树可以带个 \(\log\) 草过去(或者被卡。
\(\mathcal{O(nk)}\) 怎么做?我们需要一个数据结构来替代线段树,去实现 \(\mathcal{O(1)}\) 删除、查询前驱后继——链表!从小到大枚举 \(p_i\)(因为是个排列),跳前驱后继处理 \(f,g\) 记录答案,删除 \(p_i\) 保证当前元素最小即可。
AC 代码
代码基本按照 std 思路写的。
for(int i=1;i<=n;++i){
int at=b[i];//b[]为每个值的编号
for(int j=0;j<=k;++j) f[j]=at,at=L[at];//跳前驱处理f[]
at=b[i]; for(int j=0;j<=k;++j) g[j]=at,at=R[at];//跳后继处理g[]
at=b[i]; for(int j=1;j<=k;++j) ans[at]+=(lint)(f[j]-f[j-1])*(g[k-j]-g[k-j+1]);//记录答案
L[R[at]]=L[at],R[L[at]]=R[at];//删除节点
}