题目地址:
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 c
或2 a b c
。
输出格式:
输出每个询问的结果,每个结果占一行。
数据范围:
1
≤
N
,
M
≤
50000
1≤N,M≤50000
1≤N,M≤50000,
1
≤
a
≤
b
≤
N
1≤a≤b≤N
1≤a≤b≤N,
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
1≤c≤231−1。
所有操作保证合法。
先离散化,然后开一个权值线段树,并且想象一下每个权值线段树的节点里有个数组,该数组维护的是在这个权值范围内,每个位置有多少个数。这样即有:
操作
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
c−d大的数。
由于外传节点里的那个数组需要区间加 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)。