扫描线历史和问题
4-side 矩形加 & 求和
给定一个 \(n \times n\) 的二维平面,先进行 \(n\) 次 4-side 矩形加,再查询 \(n\) 次 4-side 矩形内部的整点和。
\(n \le 5 \times 10^5\)。
我们将修改矩形和查询矩形都差分成如图所示的 3-side 矩形:

对 \(x\) 轴扫描线,并在 \(y\) 轴上维护序列数据结构。考察这个数据结构要维护的事情:
-
在扫描到修改矩形的左边界时,我们要做一个区间加:

-
在扫描到查询矩形的右边界时,我们要做一个区间求和:

将 \(x\) 轴视作时间轴。感性理解一下,我们希望每个区间求和随时间推移不断产生贡献。将问题抽象为数据结构语言:
维护两个长为 \(n\) 的序列 \(a_i,b_i\),支持如下操作:
- 给定 \(l,r,v\),对 \(i \in [l, r]\) 令 \(a_i \gets a_{i}+v\)。
- 对 \(i \in [1,n]\) 令 \(b_i \gets b_i +a_i\)。
- 给定 \(l,r\),求 \(\sum \limits_{i=l}^r b_i\)。
上述定义中,\(a_i\) 是 \(y\) 轴信息的原序列,\(b_i\) 是 \(y\) 轴信息的历史和。
对于历史和问题,通用的做法是将信息设计为向量,标记设计为矩阵,从而信息合并是向量加法,标记合并是矩阵乘法。对于本题,我们考虑将信息设计为 $$\begin{bmatrix} \sum a_i \ \sum b_i \ \text{len} \end{bmatrix}$$,其中 \(\text{len}\) 表示区间长度。对操作进行讨论:
- 区间加 \(v\):\(\begin{bmatrix} 1 & 0 & v \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}\begin{bmatrix} \sum a_i \\ \sum b_i \\ \text{len} \end{bmatrix}=\begin{bmatrix} \sum (a_i+v) \\ \sum b_i \\ \text{len} \end{bmatrix}\)。
- 更新历史和:\(\begin{bmatrix} 1 & 0 & 0 \\ 1 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}\begin{bmatrix} \sum a_i \\ \sum b_i \\ \text{len} \end{bmatrix}=\begin{bmatrix} \sum a_i \\ \sum (a_i+b_i) \\ \text{len} \end{bmatrix}\)。
直接使用矩阵会获得巨大常数。事实上,可以只维护形如 \(\begin{bmatrix} 1 & 0 & x \\ y & 1 & z \\ 0 & 0 & 1 \end{bmatrix}\) 的标记,手动模拟矩阵乘法,称这一优化为半群优化。
复杂度 \(O(n \log n)\)。
BZOJ4262 Sum
给定一个长为 \(n\) 的序列 \(a_i\),\(q\) 次询问给定 \(l_1,r_1,l_2,r_2\),查询
\[\sum_{l \in [l_1,r_1]} \sum_{r \in [l_2,r_2]} \left(\max_{i \in [l,r]} a_i - \min_{i \in [l,r]} a_i\right) \]\(n=10^5\),\(q \le 4 \times 10^4\),1 秒,512 MB。
显然一次询问的结果可以拆成 \(\max\) 和 \(\min\) 相减。下面以 \(\max\) 的处理为例,\(\min\) 的处理同理。
考虑每个数 \(a_i\) 的贡献。用单调栈预处理出 \(a_i\) 为最大值的极长区间 \([x_i,y_i]\),则 \(a_i\) 对 \(l \in [x_i,i], r\in [i, y_i]\) 的 \((l,r)\) 都能产生一次贡献。将 \(l,r\) 刻画到二维平面上,此时每个数 \(a_i\) 相当于一个 4-side 矩形 \([x_i,i] \times [i,y_i]\),询问则是查询 \([l_1,r_1] \times [l_2,r_2]\) 这个 4-side 矩形内部整点的点权和。
这里有一个细节:\(x_i\) 的定义是左侧第一个 \(\textcolor{red}{\ge} a_i\) 的下标 \(+1\),\(y_i\) 的定义则是右侧第一个 \(\textcolor{red}{>} a_i\) 的下标 \(-1\)。也就是,我们将数视作 \((a_i,i)\) 的有序二元组,防止一个数被算重。
现在问题被转化成先做 4-side 矩形加,再做 4-side 矩形求和的问题,扫描线历史和解决。视 \(n,q\) 同阶复杂度 \(O(n \log n)\)。
:::::info[代码]
const int N = 1e5 + 5;
int n, m, a[N], x[N], y[N]; i64 ans[N];
vector<tuple<int, int, i64>> U[N]; // (y1, y2, v)
vector<tuple<int, int, int, int>> Q[N]; // (y1, y2, type, id)
void add(int x1, int y1, int x2, int y2, i64 v) {
U[x1].emplace_back(y1, y2, v);
U[x2+1].emplace_back(y1, y2, -v);
}
void prework() {
stack<int> st;
for (int i = 1; i <= n; i++) {
while (!st.empty() && a[st.top()] <= a[i]) st.pop();
x[i] = st.empty() ? 1 : st.top()+1, st.emplace(i);
}
while (!st.empty()) st.pop();
for (int i = n; i >= 1; i--) {
while (!st.empty() && a[st.top()] < a[i]) st.pop();
y[i] = st.empty() ? n : st.top()-1, st.emplace(i);
}
for (int i = 1; i <= n; i++) add(x[i], i, i, y[i], a[i]);
while (!st.empty()) st.pop();
for (int i = 1; i <= n; i++) {
while (!st.empty() && a[st.top()] >= a[i]) st.pop();
x[i] = st.empty() ? 1 : st.top()+1, st.emplace(i);
}
while (!st.empty()) st.pop();
for (int i = n; i >= 1; i--) {
while (!st.empty() && a[st.top()] > a[i]) st.pop();
y[i] = st.empty() ? n : st.top()-1, st.emplace(i);
}
for (int i = 1; i <= n; i++) add(x[i], i, i, y[i], -a[i]);
}
using info_t = tuple<i64, i64, int>;
using tag_t = tuple<i64, i64, i64>;
const tag_t I{};
info_t operator+ (const info_t& u, const info_t& v) {
auto [a1, b1, l1] = u; auto [a2, b2, l2] = v;
return {a1 + a2, b1 + b2, l1 + l2};
}
tag_t operator* (const tag_t& u, const tag_t& v) {
auto [x1, y1, z1] = u; auto [x2, y2, z2] = v;
return {x1 + x2, y1 + y2, y1 * x2 + z1 + z2};
}
info_t operator* (const tag_t& u, const info_t& v) {
auto [x, y, z] = u; auto [a, b, l] = v;
return {a + x * l, y * a + b + z * l, l};
}
struct segtree {
#define ls (rt << 1)
#define rs (rt << 1 | 1)
info_t val[N << 2];
tag_t tag[N << 2];
void pushup(int rt) {val[rt] = val[ls] + val[rs];}
void apply(int rt, const tag_t& c) {val[rt] = c * val[rt], tag[rt] = c * tag[rt];}
void pushdown(int rt) {
if (tag[rt] == I) return;
apply(ls, tag[rt]), apply(rs, tag[rt]), tag[rt] = I;
}
void build(int l = 1, int r = n, int rt = 1) {
if (l == r) return val[rt] = info_t(0, 0, 1), void();
int mid = (l + r) >> 1;
build(l, mid, ls), build(mid + 1, r, rs), pushup(rt);
}
void update(int tl, int tr, const tag_t& c, int l = 1, int r = n, int rt = 1) {
if (tl <= l && r <= tr) return apply(rt, c), void();
int mid = (l + r) >> 1; pushdown(rt);
if (tl <= mid) update(tl, tr, c, l, mid, ls);
if (tr > mid) update(tl, tr, c, mid + 1, r, rs);
pushup(rt);
}
info_t ask(int tl, int tr, int l = 1, int r = n, int rt = 1) {
if (tl <= l && r <= tr) return val[rt];
int mid = (l + r) >> 1; pushdown(rt);
if (tl <= mid && mid < tr) return ask(tl, tr, l, mid, ls) + ask(tl, tr, mid + 1, r, rs);
return tl <= mid ? ask(tl, tr, l, mid, ls) : ask(tl, tr, mid + 1, r, rs);
}
} T;
void _main() {
prework(); cin >> m;
for (int i = 1, l1, r1, l2, r2; i <= m; i++) {
cin >> l1 >> r1 >> l2 >> r2;
Q[r1].emplace_back(l2, r2, 1, i);
if (l1 > 1) Q[l1-1].emplace_back(l2, r2, -1, i);
}
T.build();
for (int x = 1; x <= n; x++) {
for (auto [l, r, v] : U[x]) T.update(l, r, info_t(v, 0, 0)); // op 1 += v
T.update(1, n, info_t(0, 1, 0)); // op 2
for (auto [l, r, t, id] : Q[x]) ans[id] += $1(T.ask(l, r)) * t;
}
for (int i = 1; i <= m; i++) cout << ans[i] << '\n';
}
:::::
P8868 [NOIP2022] 比赛
给定长为 \(n\) 的序列 \(a_i,b_i\),\(q\) 次询问给定 \(x,y\),查询
\[\left(\sum_{[l,r] \subseteq [x,y]} \left(\max_{i \in [l, r]} a_i\right) \times \left(\max_{i \in [l, r]} b_i\right)\right) \bmod 2^{64} \]\(n,q \le 2.5 \times 10^5\),2 秒,512 MB。
对 \(a\) 单调栈求出 \(a_i\) 为最大值的极长区间 \([u_i,v_i]\),同理对 \(b\) 求得 \(b_i\) 为最大值的极长区间 \([\alpha_i,\beta_i]\)。
考察每对 \(a_i \times b_j\) 的贡献。可以发现 \(a_i, b_j\) 同时成为区间 \(\max\) 当且仅当 \(l \in [u_i,i] \cap [\alpha_i,i], r \in [i,v_i] \cap [i,\beta_i]\)。转 \((l,r)\) 二维平面分析,则 \((i,j)\) 的贡献区域是 \([u_i,i] \times [i,v_i]\) 和 \([\alpha_i,i] \times [i,\beta_i]\) 两个 4-side 矩形的交集。询问是对 \([x,y] \times [x,y]\) 的直角三角形求和。
对 \(r\) 扫描线,在时刻 \(r\) 动态维护两个数组 \(f_l= \max \limits_{i=l}^r a_i\),\(g_l=\max \limits_{i=l}^r b_i\),点 \((l,r)\) 的权值即为 \(f_l \times g_l\)。考察 \(r-1\to r\) 时 \(f\) 的变化:
- 若 \([l, r] \subseteq [u_r, r]\),\(f_l\) 变为 \(a_r\)。
- 若 \(l<u_r\),\(f_l\) 不变。
对数组 \(g\) 同理,故修改是两次区间覆盖。
考虑在位置 \(l\) 处维护 \(f_l \times g_l\) 的历史和 \(h_l\),表示时刻 \(1 \sim r\) 每行点权的累加,则 \(y=r\) 的询问答案即为 \(\sum \limits_{i=x}^y h_i\)。

上图展示了这个做法的几何意义,我们用若干垂直线拼出询问的三角形,这些垂直线段就是历史和。
至此转化为如下数据结构问题:
维护三个长为 \(n\) 的序列 \(a_i,b_i,c_i\),支持如下操作:
- 给定 \(l,r,v\),对 \(i \in [l,r]\) 令 \(a_i \gets v\)。
- 给定 \(l,r,v\),对 \(i \in [l,r]\) 令 \(b_i \gets v\)。
- 对 \(i \in [1,n]\) 令 \(c_i \gets c_i+a_i\times b_i\)。
- 给定 \(l,r\) 查询 \(\sum \limits_{i=l}^r c_i\)。
容易发现 \(c_i\) 是 \(a_ib_i\) 的历史和。考虑构造半群信息。
将信息构造为向量 \(\begin{bmatrix} \sum a_i \\ \sum b_i \\ \sum a_ib_i \\ \sum c_i \\ \text{len} \end{bmatrix}\),其中 \(\text{len}\) 为区间长度。对操作进行讨论:
- 序列 \(a_i\) 赋值 \(v\):\(\begin{bmatrix} 0 & 0 & 0 & 0 & v \\ 0 & 1 & 0 & 0 & 0 \\ 0 & v & 0 & 0 & 0\\ 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} \sum a_i \\ \sum b_i \\ \sum a_ib_i \\ \sum c_i \\ \text{len} \end{bmatrix}=\begin{bmatrix} \sum v \\ \sum b_i \\ \sum vb_i \\ \sum c_i \\ \text{len} \end{bmatrix}\)。
- 序列 \(b_i\) 赋值 \(v\):\(\begin{bmatrix} 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & v \\ v & 0 & 0 & 0 & 0\\ 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} \sum a_i \\ \sum b_i \\ \sum a_ib_i \\ \sum c_i \\ \text{len} \end{bmatrix}=\begin{bmatrix} \sum a_i \\ \sum v \\ \sum va_i \\ \sum c_i \\ \text{len} \end{bmatrix}\)。
- 更新历史和:\(\begin{bmatrix} 1 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0\\ 0 & 0 & 1 & 1 & 0 \\ 0 & 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} \sum a_i \\ \sum b_i \\ \sum a_ib_i \\ \sum c_i \\ \text{len} \end{bmatrix}=\begin{bmatrix} \sum a_i \\ \sum b_i \\ \sum a_ib_i \\ \sum c_i+\sum a_ib_i \\ \text{len} \end{bmatrix}\)。
进行半群优化。观察复合后的矩阵结构,考虑只维护形如 \(\begin{bmatrix} a_{0,0} & 0 & 0 & 0 & a_{0,4} \\ 0 & a_{1,1} & 0 & 0 & a_{1,4} \\ a_{2,0} & a_{2,1} & a_{2,2} & 0 & a_{2,4} \\ a_{3,0} & a_{3,1} & a_{3,2} & 1 & a_{3,4}\\ 0 & 0 & 0 & 0 &1 \end{bmatrix}\) 的标记,这样只用维护 \(12\) 个自变量。
视 \(n,q\) 同阶复杂度 \(O(n \log n)\)。
:::::info[代码]
const int N = 2.5e5 + 5;
int n, m, a[N], b[N], la[N], lb[N]; u64 ans[N];
vector<pair<int, int>> Q[N]; // (l, id)
void prework() {
stack<int> st;
for (int i = 1; i <= n; i++) {
while (!st.empty() && a[st.top()] < a[i]) st.pop();
la[i] = st.empty() ? 1 : st.top()+1, st.emplace(i);
}
while (!st.empty()) st.pop();
for (int i = 1; i <= n; i++) {
while (!st.empty() && b[st.top()] < b[i]) st.pop();
lb[i] = st.empty() ? 1 : st.top()+1, st.emplace(i);
}
}
using info_t = tuple<u64, u64, u64, u64, u64>;
using tag_t = tuple<u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64>;
const tag_t I(1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0);
info_t operator+ (const info_t& u, const info_t& v) {
auto [a1, b1, ab1, c1, l1] = u;
auto [a2, b2, ab2, c2, l2] = v;
return {a1 + a2, b1 + b2, ab1 + ab2, c1 + c2, l1 + l2};
}
tag_t operator* (const tag_t& u, const tag_t& v) {
auto [a00, a04, a11, a14, a20, a21, a22, a24, a30, a31, a32, a34] = u;
auto [b00, b04, b11, b14, b20, b21, b22, b24, b30, b31, b32, b34] = v;
return {
a00 * b00,
a00 * b04 + a04,
a11 * b11,
a11 * b14 + a14,
a20 * b00 + a22 * b20,
a21 * b11 + a22 * b21,
a22 * b22,
a20 * b04 + a21 * b14 + a22 * b24 + a24,
a30 * b00 + a32 * b20 + b30,
a31 * b11 + a32 * b21 + b31,
a32 * b22 + b32,
a30 * b04 + a31 * b14 + a32 * b24 + a34 + b34
};
}
info_t operator* (const tag_t& u, const info_t& v) {
auto [a00, a04, a11, a14, a20, a21, a22, a24, a30, a31, a32, a34] = u;
auto [a, b, ab, c, l] = v;
return {
a00 * a + a04 * l,
a11 * b + a14 * l,
a20 * a + a21 * b + a22 * ab + a24 * l,
a30 * a + a31 * b + a32 * ab + c + a34 * l,
l
};
}
struct segtree {
#define ls (rt << 1)
#define rs (rt << 1 | 1)
info_t val[N << 2];
tag_t tag[N << 2];
void pushup(int rt) {val[rt] = val[ls] + val[rs];}
void apply(int rt, const tag_t& c) {val[rt] = c * val[rt], tag[rt] = c * tag[rt];}
void pushdown(int rt) {
if (tag[rt] == I) return;
apply(ls, tag[rt]), apply(rs, tag[rt]), tag[rt] = I;
}
void build(int l = 1, int r = n, int rt = 1) {
if (l == r) return val[rt] = info_t(0, 0, 0, 0, 1), void();
int mid = (l + r) >> 1;
build(l, mid, ls), build(mid + 1, r, rs), pushup(rt);
}
void update(int tl, int tr, const tag_t& c, int l = 1, int r = n, int rt = 1) {
if (tl <= l && r <= tr) return apply(rt, c), void();
int mid = (l + r) >> 1; pushdown(rt);
if (tl <= mid) update(tl, tr, c, l, mid, ls);
if (tr > mid) update(tl, tr, c, mid + 1, r, rs);
pushup(rt);
}
info_t ask(int tl, int tr, int l = 1, int r = n, int rt = 1) {
if (tl <= l && r <= tr) return val[rt];
int mid = (l + r) >> 1; pushdown(rt);
if (tl <= mid && mid < tr) return ask(tl, tr, l, mid, ls) + ask(tl, tr, mid + 1, r, rs);
return tl <= mid ? ask(tl, tr, l, mid, ls) : ask(tl, tr, mid + 1, r, rs);
}
} T;
void _main() {
cin >> n >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> b[i];
prework(), cin >> m;
for (int i = 1, l, r; i <= m; i++) {
cin >> l >> r;
Q[r].emplace_back(l, i);
}
T.build();
for (int r = 1; r <= n; r++) {
T.update(la[r], r, tag_t(0, a[r], 1, 0, 0, a[r], 0, 0, 0, 0, 0, 0)); // op 1
T.update(lb[r], r, tag_t(1, 0, 0, b[r], b[r], 0, 0, 0, 0, 0, 0, 0)); // op 2
T.update(1, n, tag_t(1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0)); // op 3
for (auto [l, id] : Q[r]) ans[id] += $3(T.ask(l, r));
}
for (int i = 1; i <= m; i++) cout << ans[i] << '\n';
}
:::::
CF1824D LuoTianyi and the Function
给定长为 \(n\) 的序列 \(a_i\),定义函数 \(f(l,r)\) 为
- 若 \(l \le r\),则 \(f(l,r)\) 是满足 \(\{a_i \mid i \in [l,r]\}\subseteq \{a_j \mid [x,r]\}\) 的最大的 \(x\)。
- 若 \(l>r\),则 \(f(l,r)=0\)。
\(q\) 次询问,给定 \(l_1,r_1,l_2,r_2\),查询
\[\sum_{l \in [l_1,r_1]} \sum_{r \in [l_2,r_2]} f(l,r) \]\(n, q \le 10^6\),7 秒,1000 MB。

浙公网安备 33010602011771号