题意简述
给你一个长为 \(n\) 的序列 \(A_i\),再给你 \(m\) 次操作,操作有两种类型:
- 把区间 \([l,r]\) 之间 \(>x\) 的数减去 \(x\)。
- 询问区间 \([l,r]\) 的和,最大值,最小值。
数据范围:\(n\le 5e5,m\le 5e5,A_i\le 1e9\) ,强制在线。
题目分析
大致做法:倍增分块 \(+\) 序列线段树 \(+\) 底层分块。
稍加考虑就会发现序列上和值域上都必须要用某种数据结构来维护(就是某个数据结构套某个数据结构而且我不会)。
目前蒟蒻只会这种做法(而且还是看了题解,这个倍增分块让蒟蒻大受震撼)。
upd:问了 julao ,其实分块套 splay 也可以做,代码难度应该会低一些如果你没有常数。
因为值域 \(10^9\) 非常不好做,考虑特殊的倍增分块,也就是把值为 \([B^i,B^{i+1})\) 的数分在一个值域块里。
为什么会考虑这种分块呢?
首先考虑比较暴力的做法,建一棵序列线段树,直接维护序列上区间的信息。
-
如果一个区间最大值 $ \ge x$ 直接跳过,
-
如果一个区间被操作区间完全包含并且最小值 $ > x$,直接打上懒标记,
-
如果到了叶子节点,必然在上两种情况之中,直接返回。
这样子时间必然爆炸,因为一个操作可能会被无限细分到叶子。考虑上述做法的复杂度瓶颈,显然是无限细分,考虑对值域进行处理,将值域分成多个块,尽可能降低每个块的细分复杂度。
假如我们已经把值域划分成了 \(cnt\) 个块,每个块里都有这么一个序列线段树,但是这个序列线段树只接受值域在该块之内的数的贡献。
对于每一块的线段树我们可以就像上面的 \(3\) 个操作一样处理,同样的,考虑细分复杂度,因为此时有多棵线段树,所以细分变成了两个部分:区间打懒标记和从这个块中剔除一个减去 \(x\) 就小于值域下界的叶子节点并跌落加入另一个块中。
首先向线段树加入一个数显然是 \(\log{n}\) 的。又因为每个数最多跌落 \(cnt\) 次(在每一个块依次跌落一遍),所以整体来看跌落的最坏复杂度是 \(n\times cnt\times \log{n}\) 的。
那么打懒标记呢?
-
如果一个块上界 \(\le x\),直接就被跳过了。
-
如果一个块下界 $ > x $,其中一部分会重构,刚才我们算过,剩下的每次可以 \(\log{n}\) 算,这一部分是 \(m \times cnt \times \log{n}\) 的。
-
如果一个块下界 \(\le x\),上界 \(>x\),这一块就是等于开始的纯线段树暴力做法,所以比较麻烦。
我们会发现,我们前两种情况复杂度只和分的块数有关,但我们怎么分块并不确定,考虑保证 \(cnt\) 较小时让 \(3\) 最省时间。
设第三种情况的所在块长为 \(len\),那么这种情况一个数要被减去 \(x\),就最多只能够被减去 \(cnt\times len/x\) 次。
我们考虑比较刁钻的情况,就是 \(x\) 尽量小,它就是那个块的下界 \(l\),所以我们就要让复杂度 \(n\times \log{n}\times cnt\times len/l\) 尽量小,同时 \(cnt\) 也尽量小。考虑固定 \(len/l\),设它为 \(B\),那么 \(cnt\) 等于 \(\log_{B}{10^9}\),\(B\) 在 \(16\) 时最优。
那么根据以上柿子,我们发现其实我们可以采用倍增分块的方式,得到优秀的时间。
因为此时 \(cnt=\log_{B}{10^9}\),所以最终时间复杂度为:
\(O((n+m)\times \log_B{10^9}\times \log{n}+n\times B\times \log{n}\times \log_B{10^9})\)
于是我们可以按照上述写法开心地~MLE 了。
那怎么办呢?考虑空间复杂度瓶颈,就是线段树。考虑优化。
我们发现对于一棵线段树,最占空间的是比较底层的节点,尤其是叶子。
于是我们可以设置一个常数 \(K\),让线段树 \(\le K\) 的区间的处理直接变成下传标记在序列上暴力,之后再上传。我们可以这样很可观地以大常数的时间代价换取空间。
所以所谓的底层分块不就是在底层直接暴力吗。
\(K=4\) 是理论最优的(不过还是以实际卡常效果为准)。那么现在总空间大概就是线性的样子,就可以通过本题了。
代码及细节
-
注意 long long 和 int。
-
注意及时在底层分块与线段树间和线段树内部下传标记,上传信息。
-
最好加上快读快输。
#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = (l); i <= (r); ++i)
#define per(i, r, l) for (int i = (r); i >= (l); --i)
#define ls (num << 1)
#define rs (num << 1 | 1)
const int inf = INT_MAX, df = 2e4 + 7, N = 5e5 + 7, B = 16, K = 32 ;/*相对较好的参数*/
int i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z;
long long ma[9][df << 1];
long long tag[9][df << 1];
int mx[9][df << 1], mn[9][df << 1];
int a[N];
long long sum[9][df << 1];
long long lb[9], rb[9]; /*这里是倍增分块的上界和下界*/
/*
ma:区间有几个数
sum:区间和
mx:区间最大值
mn:区间最小值
tag:懒标记
*/
void pushup(int blk, int num)
{
ma[blk][num] = ma[blk][ls] + ma[blk][rs];
sum[blk][num] = sum[blk][ls] + sum[blk][rs];
mx[blk][num] = max(mx[blk][ls], mx[blk][rs]);
mn[blk][num] = min(mn[blk][ls], mn[blk][rs]);
return;
}
/*线段树上传信息*/
void pushdown(int blk, int num)
{
if (!tag[blk][num])
return;
tag[blk][ls] += tag[blk][num] * (ma[blk][ls] != 0), tag[blk][rs] += tag[blk][num] * (ma[blk][rs] != 0);
if (ma[blk][ls])
sum[blk][ls] -= tag[blk][num] * ma[blk][ls], mx[blk][ls] -= tag[blk][num], mn[blk][ls] -= tag[blk][num];
if (ma[blk][rs])
sum[blk][rs] -= tag[blk][num] * ma[blk][rs], mx[blk][rs] -= tag[blk][num], mn[blk][rs] -= tag[blk][num];
tag[blk][num] = 0;
return;
}
/*线段树下传标记*/
int getblk(int x)
{
int now = 1;
while (x > rb[now])
now++;
return now;
}
/*x在第几个值域块*/
void blk_pushdown(int blk, int le, int rig, long long &x)
{
if (!x)
return;
rep(i, le, rig) if (a[i] >= lb[blk] && a[i] <= rb[blk]) a[i] -= x;
x = 0;
return;
}
/*线段树的叶子节点向序列下传标记*/
void blk_pushup(int blk, int le, int rig, int num)
{
ma[blk][num] = 0, sum[blk][num] = 0, mx[blk][num] = 0, mn[blk][num] = inf;
rep(i, le, rig)
{
if (a[i] >= lb[blk] && a[i] <= rb[blk])
{
ma[blk][num]++;
sum[blk][num] += a[i];
mx[blk][num] = max(mx[blk][num], a[i]);
mn[blk][num] = min(mn[blk][num], a[i]);
}
}
return;
}
/*把序列的信息上传到线段树的叶子节点上*/
void insert(int blk, int num, int le, int rig, int p, int x)
{
if (rig - le + 1 <= K)
{
blk_pushdown(blk, le, rig, tag[blk][num]);
a[p] = x;
blk_pushup(blk, le, rig, num);
return;
}
int mid = (le + rig) >> 1;
pushdown(blk, num);
if (p <= mid)
insert(blk, ls, le, mid, p, x);
else
insert(blk, rs, mid + 1, rig, p, x);
pushup(blk, num);
return;
}
/*线段树插入节点*/
void blk_upd(int blk, int le, int rig, int x)
{
rep(i, le, rig) if (a[i] >= lb[blk] && a[i] <= rb[blk] && a[i] > x)
{
a[i] -= x;
if (a[i] < lb[blk])
{
insert(getblk(a[i]), 1, 1, n, i, a[i]);
} /*插入另一个块的线段树*/
}
return;
}
/*块内暴力执行1号操作*/
void blk_qry(int blk, long long &ans, int &maxn, int &minn, int le, int rig, int l, int r)
{
rep(i, max(le, l), min(rig, r))
{
if (a[i] >= lb[blk] && a[i] <= rb[blk])
{
ans += a[i], maxn = max(maxn, a[i]), minn = min(minn, a[i]);
}
}
return;
}
/*块内暴力执行查询*/
void upd(int blk, int num, int le, int rig, int l, int r, int x)
{
if (mx[blk][num] <= x)
return; /*小于就跳过*/
if (rig - le + 1 <= K)
{
blk_pushdown(blk, le, rig, tag[blk][num]);
l = max(l, le), r = min(r, rig);
blk_upd(blk, l, r, x);
blk_pushup(blk, le, rig, num);
return;
} /*到达叶子暴力修改*/
if (le >= l && rig <= r && mn[blk][num] - lb[blk] >= x)
{
sum[blk][num] -= x * ma[blk][num], mn[blk][num] -= x, mx[blk][num] -= x; /*直接打懒标记*/
tag[blk][num] += x;
return;
}
int mid = (le + rig) >> 1;
pushdown(blk, num);
if (l <= mid)
upd(blk, ls, le, mid, l, r, x);
if (r > mid)
upd(blk, rs, mid + 1, rig, l, r, x);
pushup(blk, num);
return;
}
/*线段树上执行操作*/
void qry(long long &ans, int &maxn, int &minn, int blk, int num, int le, int rig, int l, int r)
{
if (le >= l && rig <= r)
{
ans += sum[blk][num], maxn = max(maxn, mx[blk][num]), minn = min(minn, mn[blk][num]);
return;
} /*叶子节点暴力查询*/
if (rig - le + 1 <= K)
{ /*完全包含,直接统计*/
blk_pushdown(blk, le, rig, tag[blk][num]);
blk_qry(blk, ans, maxn, minn, le, rig, l, r);
blk_pushup(blk, le, rig, num);
return;
}
int mid = (le + rig) >> 1;
pushdown(blk, num); /*注意及时下传和上传*/
if (l <= mid)
qry(ans, maxn, minn, blk, ls, le, mid, l, r);
if (r > mid)
qry(ans, maxn, minn, blk, rs, mid + 1, rig, l, r);
pushup(blk, num);
return;
}
inline int read()
{
int x = 0, y = 1;
char ch = getchar();
while (ch > '9' || ch < '0')
y = (ch == '-') ? -1 : 1, ch = getchar();
while (ch >= '0' && ch <= '9')
x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * y;
}
void fwrite(int x)
{
if (x > 9)
fwrite(x / 10);
putchar(x % 10 + '0');
return;
} /*最好加上快读快输*/
void llfwrite(long long x)
{
if (x > 9)
llfwrite(x / 10);
putchar(x % 10 + '0');
return;
}
int main()
{
rep(j, 1, 8) rep(i, 0, df * 2 - 2) mn[j][i] = inf, mx[j][i] = 0;/*初始化*/
n = read(), q = read();
rep(i, 1, 8) lb[i] = rb[i - 1] + 1, rb[i] = lb[i] * B - 1;/*预处理*/
rep(i, 1, n) a[i] = x = read(), insert(getblk(x), 1, 1, n, i, x);
int op = 0, l = 0, r = 0, x = 0, lastans = 0, maxn = 0, minn = 0;
long long ans = 0;
while (q--)
{
op = read(), l = read() ^ lastans, r = read() ^ lastans;
if (op == 1)
{
x = read() ^ lastans;/*强制在线*/
rep(i, 1, 8) if (rb[i] >= x) upd(i, 1, 1, n, l, r, x);
}
else
{
ans = 0, maxn = 0, minn = inf;
rep(i, 1, 8) qry(ans, maxn, minn, i, 1, 1, n, l, r);
llfwrite(ans), putchar(' '), fwrite(minn), putchar(' '), fwrite(maxn), putchar('\n');
lastans = ans % 1048576;
}
}
}
写在后面
这道题主要妙在它的倍增分块,底层分块,以及对时间复杂度的分析的优(du)秀(liu)技巧,是一道练习数据结构和代码能力的好题。