【ACWing】2306. K大数查询

题目地址:

https://www.acwing.com/problem/content/2308/

N N N个位置, M M M个操作。每个位置可以同时存储多个数。操作有两种,每次操作:
如果是1 a b c的形式,表示在第 a a a个位置到第 b b b个位置,每个位置加入一个数 c c c
如果是2 a b c的形式,表示询问从第 a a a个位置到第 b b b个位置,第 c c c大的数是多少。

输入格式:
第一行包含两个整数 N , M N,M N,M
接下来 M M M行,每行包含一条指令,形如1 a b c2 a b c

输出格式:
输出每个询问的结果,每个结果占一行。

数据范围:
1 ≤ N , M ≤ 50000 1≤N,M≤50000 1N,M50000,
1 ≤ a ≤ b ≤ N 1≤a≤b≤N 1abN,
1 1 1操作中的 c c c的绝对值不超过 N N N,
2 2 2操作中 c c c满足 1 ≤ c ≤ 2 31 − 1 1≤c≤2^{31}−1 1c2311
所有操作保证合法。

先离散化,然后开一个权值线段树,并且想象一下每个权值线段树的节点里有个数组,该数组维护的是在这个权值范围内,每个位置有多少个数。这样即有:
操作 1 1 1对于外层的权值线段树而言是个单点操作,而对于内层的数组而言是个区间加 1 1 1的操作;
操作 2 2 2可以用二分答案来做,对于权值线段树的每个节点来说,其维护的权值范围的中点可以作为每次二分的中点 m m m,那么通过对其右儿子节点内部数组做一下位置 [ a , b ] [a,b] [a,b]的区间求和,即知道位置 [ a , b ] [a,b] [a,b]范围内大于 c c c的数有多少个,如果这个个数 d d d大于等于 c c c,说明第 c c c大的数是大于 m m m的,要向右半部分递归;否则向左边部分递归,去找第 c − d c-d cd大的数。

由于外传节点里的那个数组需要区间加 1 1 1和区间求和的操作,所以也可以用线段树来维护,从而整个数据结构即为树套树,外层是权值线段树维护权值区间,内层是普通线段树,维护的是权值范围内的情况下,每个位置的数的个数。

内层线段树需要一个 s s s记录区间和,还有个懒标记 a a a记录区间加 1 1 1这个操作。由于只有一个修改操作,我们可以采用标记持久化的技巧。重新定义 s s s s s s为只考虑当前节点及其孩子的懒标记的情况下,的总和,即 s s s不考虑父亲的懒标记,也即所有懒标记不下传。所以每个节点的真实懒标记,其实是其所有父亲的懒标记的总和,而这个总和是可以在递归的时候向下传参的。而懒标记的定义不变,但是每个懒标记不会pushdown去改变孩子的 s s s值。

代码如下:

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
using ll = long long;

const int N = 5e4 + 10, P = N * 17 * 17;
int n, m;
vector<int> nums;

struct Query {
  int op, a, b, c;
} q[N];

// 求x离散化之后的值
int find(int x) {
  return lower_bound(nums.begin(), nums.end(), x) - nums.begin();
}

namespace inner {

struct Node {
#define lc(u) tr[u].lc
#define rc(u) tr[u].rc
  // [l, r]是本节点维护的位置的范围,即题中的“N个位置”
  int l, r;
  // lc和rc是本节点的左右孩子的下标
  int lc, rc;
  // sum是[l, r]的位置范围内的数的总个数,add是懒标记
  ll sum, add;
} tr[P];
int idx;

// 求[a, b]和[c, d]的交集长度
int intersection(int a, int b, int c, int d) {
  return min(b, d) - max(a, c) + 1;
}

// u是维护位置[l, r]的那个节点,在[ql, qr]这个范围内的位置里每个位置添加一个数
void update(int u, int l, int r, int ql, int qr) {
  // [l, r]这些位置的数字总个数要加上交集长度
  tr[u].sum += intersection(l, r, ql, qr);
  if (ql <= l && r <= qr) {
    tr[u].add++;
    return;
  }
  int mid = l + r >> 1;
  if (ql <= mid) {
    if (!lc(u)) lc(u) = ++idx;
    update(lc(u), l, mid, ql, qr);
  }
  if (qr > mid) {
    if (!rc(u)) rc(u) = ++idx;
    update(rc(u), mid + 1, r, ql, qr);
  }
}

// u是维护位置[l, r]的那个节点,返回在[ql, qr]的范围内的位置里的所有数的总个数,
// add是u的所有父亲的懒标记总和
ll get_cnt(int u, int l, int r, int ql, int qr, int add) {
  // sum已经考虑了当前节点以及所有孩子的懒标记,只需要加上父亲懒标记的影响即可
  if (ql <= l && r <= qr) return tr[u].sum + add * (r - l + 1LL);
  int mid = l + r >> 1;
  ll res = 0;
  add += tr[u].add;
  if (ql <= mid) {
    if (lc(u))
      res += get_cnt(lc(u), l, mid, ql, qr, add);
    else
      res += add * intersection(l, mid, ql, qr);
  }
  if (qr > mid) {
    if (rc(u))
      res += get_cnt(rc(u), mid + 1, r, ql, qr, add);
    else
      res += add * intersection(mid + 1, r, ql, qr);
  }
  return res;
}

}  // namespace inner

// 外层的权值线段树
namespace outer {
struct Node {
  // [l, r]是本节点维护的权值范围
  int l, r;
  // rt是维护该权值范围的下标的线段树的树根的下标
  int rt;
} tr[N << 2];

// 构造外层线段树,[l, r]是每个节点维护的权值范围
void build(int u, int l, int r) {
  tr[u] = {l, r, ++inner::idx};
  if (l == r) return;
  int mid = l + r >> 1;
  build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}

// 在位置[ql, qr]的范围内每个位置加入一个数c
void change(int u, int ql, int qr, int c) {
  inner::update(tr[u].rt, 1, n, ql, qr);
  if (tr[u].l == tr[u].r) return;
  int mid = tr[u].l + tr[u].r >> 1;
  if (c <= mid)
    change(u << 1, ql, qr, c);
  else
    change(u << 1 | 1, ql, qr, c);
}

int query(int u, int ql, int qr, int c) {
  if (tr[u].l == tr[u].r) return tr[u].l;
  int mid = tr[u].l + tr[u].r >> 1;
  ll cnt = inner::get_cnt(tr[u << 1 | 1].rt, 1, n, ql, qr, 0);
  if (cnt >= c)
    return query(u << 1 | 1, ql, qr, c);
  else
    return query(u << 1, ql, qr, c - cnt);
}

}  // namespace outer

int main() {
  scanf("%d%d", &n, &m);
  for (int i = 0; i < m; i++) {
    scanf("%d%d%d%d", &q[i].op, &q[i].a, &q[i].b, &q[i].c);
    if (q[i].op == 1) nums.push_back(q[i].c);
  }
  sort(nums.begin(), nums.end());
  nums.erase(unique(nums.begin(), nums.end()), nums.end());
  outer::build(1, 0, nums.size() - 1);
  for (int i = 0; i < m; i++) {
    int op = q[i].op, a = q[i].a, b = q[i].b, c = q[i].c;
    if (op == 1)
      outer::change(1, a, b, find(c));
    else
      printf("%d\n", nums[outer::query(1, a, b, c)]);
  }
}

预处理时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn),每个操作时间 O ( log ⁡ 2 n ) O(\log ^2n) O(log2n),空间 O ( n log ⁡ n + m log ⁡ 2 n ) O(n\log n+m\log ^2n) O(nlogn+mlog2n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值