【题解】LOJ 6029 -「雅礼集训 2017 Day1」市场
题目大意
给定一个长度为 \(n\) 的序列 \(a\) ,元素编号为 \([0, n - 1]\) 。试维护一个支持以下操作的数据结构:
-
区间 \([l, r]\) 内的元素都 \(+ c\)
-
对于 \(l \leq i \leq r\) ,\(a_i = \lfloor \frac{a_i}{d} \rfloor\)
-
询问区间 \([l, r]\) 的最小值
-
询问区间 \([l, r]\) 的元素和
\(n \leq 10^5, -10^4 \leq c \leq 10^4, 2\leq d \leq 10^9\)
解题思路
对区间进行操作,首先考虑线段树和平衡树。观察题目,发现用 线段树 维护给出的操作更加方便。可以注意到操作 \(1, 2, 4\) 都是模板,考虑第 \(3\) 个操作如何实现。
我们考虑增加一个 \(lazy\) 标记。我们给区间整体除以一个数 \(d\),最小值以及元素之和都会缩小到原来的 \(\frac{1}{d}\) 。但是,如果我们尝试叠加两个 \(lazy\) 标记 \(a\) 和 \(b\) ,我们会发现不可以直接将子结点的 \(lazy\) 更新为 \(a \times b\) 。原因是题目中给出的操作还需要下取整,设 \(k = \lfloor \frac{c}{a} \rfloor\) ,可能会出现某个值 \(c\) 满足 \(\lfloor \frac{c}{ab} \neq \lfloor \frac{k}{b} \rfloor\) 。
因此,经过若干尝试,我们发现无法将一个特殊的 \(lazy\) 标记套到线段树上完成操作。此时有两种选择:
-
用其他算法来辅助维护
-
将 \(lazy\) 标记转化成容易维护的标记
显然,这道题目并没有什么显著的算法特征。因此,我们考虑综合几种线段树模型的思路,将除法的操作转化成加法的维护。
我们可以效仿区间开方线段树的做法。容易发现,当一个区间经过了若干次操作以后,该区间包含的所有元素都会逐渐靠近于 \(0\),最终变成相同的数。我们可以发现,当区间中元素的最大值等于 \(10^9\) 时,我们只需要 \(30\) 次除法操作就可以令区间中的所有元素都变成 \(\leq 0\) 。如果我们想要破坏这种结构,修改单点的最坏复杂度也只有 \(O(logn)\) ,因此总复杂度不会变得更坏。
当区间中的元素各不相同时,我们只能进行多次单点修改。但是假设 \(l \leq i \leq r\) 时 \(\max(a_i) = p, \min(a_i) = q\) 。假设 \(i\) 为一自然数,因为当 \(i\) 递增时,\(\lfloor \frac{i}{d} \rfloor\) 严格不降。如果 \(\lfloor \frac{p}{d} \rfloor = \lfloor \frac{q}{d} \rfloor\) ,此时取值范围在 \((q, p)\) 之间的所有元素除以 \(d\) 再下取整的结果都是 \(\lfloor \frac{p}{d} \rfloor\) 。因为 \(\forall l \leq i \leq r, \lfloor \frac{a_i}{d} \rfloor\) 都相等,这个式子等价于 \(\forall l \leq i \leq r, a_i - (a_i - \lfloor \frac{a_i}{d} \rfloor)\) 都相等。
所以我们在维护除法操作时可以判断:如果 \(p - \lfloor \frac{p}{d} \rfloor = q - \lfloor \frac{q}{d} \rfloor\) ,说明该区间中所有的 \(a_i - (a_i - \lfloor \frac{a_i}{d} \rfloor)\) ,即 \(\lfloor \frac{a_i}{d} \rfloor\) 都相等。证明:如果存在某个 \(a_i\) 满足 \(a_i - (a_i - \lfloor \frac{a_i}{d} \rfloor)\) 与其他 \(a_i\) 计算的结果不相等,说明 \(\lfloor \frac{a_i}{d} \rfloor\) 与其他值不等,也就是 \(a_i\) 的取值范围不在 \([p, q]\) 内,与 \(p, q\) 的定义不符。所以满足 \(p - \lfloor \frac{p}{d} \rfloor = q - \lfloor \frac{q}{d} \rfloor\) 时,区间内所有的 \(\lfloor \frac{a_i}{d} \rfloor\) 都相等。因为 \(p - \lfloor \frac{p}{d} \rfloor = q - \lfloor \frac{q}{d} \rfloor\) 等价于 \(p - \lfloor \frac{p}{d} \rfloor = q - \lfloor \frac{p}{d} \rfloor\) ,等式两边同时加上 \(\lfloor \frac{p}{d} \rfloor\) ,得到 \(p = q\) 。由 \(p = q\) 可知区间内所有元素相等,所有元素都减去 \(p - \lfloor \frac{p}{d} \rfloor\) 就可以得到 \(\lfloor \frac{a_i}{d} \rfloor\) 的结果。 上文提到的式子都是等价的,我们直接判断其一即可。
所以这道题的除法操作可以转化成加法操作。我们只需要在线段树中维护最大值、最小值和区间和,对于操作 \(1, 2, 4\),我们直接维护即可。对于操作 \(3\),如果被修改的区间所有元素值都相等,我们就直接给整个区间减去最大值除以 \(d\) 再下取整的结果,否则进行暴力单点修改。时间复杂度约为 \(O(nlogn)\) ,时限 \(2\) 秒可以通过。
参考代码
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
#define int long long
const int maxn = 1e5 + 5;
const int inf = 1e16;
struct node {
int l, r, sum;
int maxv, minv, lazy;
} tree[maxn << 2];
int n, q;
int a[maxn];
inline int read() {
int res = 0, flag = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') {
flag = -1;
}
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
res = res * 10 + ch - '0';
ch = getchar();
}
return res * flag;
}
void push_up(int k) {
tree[k].maxv = max(tree[2 * k].maxv, tree[2 * k + 1].maxv);
tree[k].minv = min(tree[2 * k].minv, tree[2 * k + 1].minv);
tree[k].sum = tree[2 * k].sum + tree[2 * k + 1].sum;
}
void push_down(int k) {
if (tree[k].l == tree[k].r) {
tree[k].lazy = 0;
return;
}
tree[2 * k].lazy += tree[k].lazy;
tree[2 * k + 1].lazy += tree[k].lazy;
tree[2 * k].sum += (tree[k].lazy * (tree[2 * k].r - tree[2 * k].l + 1));
tree[2 * k + 1].sum += (tree[k].lazy * (tree[2 * k + 1].r - tree[2 * k + 1].l + 1));
tree[2 * k].maxv += tree[k].lazy;
tree[2 * k + 1].maxv += tree[k].lazy;
tree[2 * k].minv += tree[k].lazy;
tree[2 * k + 1].minv += tree[k].lazy;
tree[k].lazy = 0;
}
void build(int k, int l, int r) {
tree[k].l = l;
tree[k].r = r;
if (l == r) {
tree[k].sum = tree[k].maxv = tree[k].minv = a[l];
return;
}
int mid = (l + r) / 2;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
push_up(k);
}
void update_add(int k, int l, int r, int w) {
if (tree[k].l >= l && tree[k].r <= r) {
tree[k].sum += (w * (tree[k].r - tree[k].l + 1));
tree[k].lazy += w;
tree[k].maxv += w;
tree[k].minv += w;
return;
}
push_down(k);
int mid = (tree[k].l + tree[k].r) / 2;
if (l <= mid) {
update_add(2 * k, l, r, w);
}
if (r > mid) {
update_add(2 * k + 1, l, r, w);
}
push_up(k);
}
void update_div(int k, int l, int r, int w) {
if (tree[k].l >= l && tree[k].r <= r) {
int a = tree[k].maxv - floor(tree[k].maxv * 1.0 / w);
int b = tree[k].minv - floor(tree[k].minv * 1.0 / w);
if (a == b) {
int cur = a;
tree[k].sum -= (cur * (tree[k].r - tree[k].l + 1));
tree[k].lazy -= cur;
tree[k].maxv -= cur;
tree[k].minv -= cur;
return;
}
}
push_down(k);
int mid = (tree[k].l + tree[k].r) / 2;
if (l <= mid) {
update_div(2 * k, l, r, w);
}
if (r > mid) {
update_div(2 * k + 1, l, r, w);
}
push_up(k);
}
int query_min(int k, int l, int r) {
if (tree[k].l >= l && tree[k].r <= r) {
return tree[k].minv;
}
push_down(k);
int mid = (tree[k].l + tree[k].r) / 2, res = inf;
if (l <= mid) {
res = min(res, query_min(2 * k, l, r));
}
if (r > mid) {
res = min(res, query_min(2 * k + 1, l, r));
}
return res;
}
int query_sum(int k, int l, int r) {
if (tree[k].l >= l && tree[k].r <= r) {
return tree[k].sum;
}
push_down(k);
int mid = (tree[k].l + tree[k].r) / 2, sum = 0;
if (l <= mid) {
sum += query_sum(2 * k, l, r);
}
if (r > mid) {
sum += query_sum(2 * k + 1, l, r);
}
return sum;
}
signed main() {
int opt, l, r, c, d;
n = read();
q = read();
for (int i = 1; i <= n; i++) {
a[i] = read();
}
build(1, 1, n);
for (int i = 1; i <= q; i++) {
opt = read();
l = read();
r = read();
l++, r++;
if (opt == 1) {
c = read();
update_add(1, l, r, c);
} else if (opt == 2) {
d = read();
update_div(1, l, r, d);
} else if (opt == 3) {
printf("%lld\n", query_min(1, l, r));
} else if (opt == 4) {
printf("%lld\n", query_sum(1, l, r));
}
}
return 0;
}