把子矩阵和拆成 4 个二维前缀角点,CDQ 分治时间维 + 归并排序 x + 树状数组维护 y,三维偏序 O((M+Q) log²W) 完成带插入的矩阵求和。
OJ: roj
题目 ID: 3130
难度:省选/NOI-
标签:CDQ分治树状数组三维偏序python
创建: 2026-10-01 18:55
更新: 2026-10-10 11:30
形式化题目
维护一个初始全
- 修改:给格子
的权值增加 ; - 询问:求以
为左下角、 为右上角的子矩阵内所有权值之和。
修改操作数
正解
思路
朴素做法。 直接开一个
第一步优化:单点插入 + 前缀和。 子矩阵和可以用四个二维前缀和角点做容斥:
其中
第二步优化:把"时刻"变成一维偏序。 由于修改在时间上单调累加,
的修改点权值
第三步:CDQ 分治 + 树状数组。 类似三维偏序求个数,采用"时间维分治,一维排序,一维数据结构"的套路:
- 把修改点和询问角点统一成事件序列,按下标(即时间)分治
; - 递归处理完左右两半的内部贡献后,计算左半的修改点对右半的询问角点的贡献:此时左半的时间天然全部早于右半,只剩二维偏序;
- 两半各自按
归并有序(分治时顺手归并,代替预排序),用双指针把 的修改点插入树状数组(按下标 加 ),每个询问角点用 查询前缀和即可。
注意坐标可能是
下图展示一次询问被拆成 4 个角点、以及左半修改如何通过树状数组贡献给右半询问的过程:
flowchart LR
subgraph L["分治左半(时间早)"]
M1["修改 (t,x,y,+a)"]
M2["修改 ..."]
end
subgraph R["分治右半(时间晚)"]
P1["角点 (t,X,Y) 按 x 归并扫描"]
end
M1 -->|"x' <= X 时插入 BIT(y)"| BIT["树状数组(按 y 前缀和)"]
M2 -->|"x' <= X 时插入 BIT(y)"| BIT
BIT -->|"query(Y) 得 Σa'"| P1
每个角点拿到的是"时间在本层分治左侧"的修改贡献;所有包含它的
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
// Mokia:三维偏序(时间, x, y),CDQ 分治时间维,x 排序后树状数组维护 y。
#include <cstdio>
#include <vector>
typedef long long ll;
struct Ev {
ll x, y;
ll w; // 修改的权值
ll qid; // 询问编号;-1 表示这是修改
ll sgn; // 询问角点的符号
};
std::vector<Ev> ev;
std::vector<Ev> tmp;
ll W;
std::vector<ll> tree;
std::vector<int> stamp;
ll cur_stamp = 0;
std::vector<ll> ans;
ll qcnt = 0;
void cdq(ll l, ll r) {
if (r - l <= 1) return;
ll mid = (l + r) >> 1;
cdq(l, mid);
cdq(mid, r);
// 递归保证两半各自按 x 有序;左半修改(时间早)贡献给右半询问
cur_stamp++;
ll i = l;
for (ll j = mid; j < r; j++) {
if (ev[j].qid == -1) continue;
ll qx = ev[j].x, qy = ev[j].y;
while (i < mid && ev[i].x <= qx) {
if (ev[i].qid == -1) {
ll k = ev[i].y, ew = ev[i].w;
while (k <= W) {
if (stamp[k] != cur_stamp) {
stamp[k] = (int)cur_stamp;
tree[k] = 0;
}
tree[k] += ew;
k += k & -k;
}
}
i++;
}
if (ev[j].sgn) {
ll s = 0, k = qy;
while (k) {
if (stamp[k] == cur_stamp) s += tree[k];
k -= k & -k;
}
ans[ev[j].qid] += ev[j].sgn * s;
}
}
// 归并两半按 x 有序,维持不变式
ll a = l, b = mid, t = l;
while (a < mid && b < r) {
if (ev[a].x <= ev[b].x) tmp[t++] = ev[a++];
else tmp[t++] = ev[b++];
}
while (a < mid) tmp[t++] = ev[a++];
while (b < r) tmp[t++] = ev[b++];
for (ll k = l; k < r; k++) ev[k] = tmp[k];
}
int main() {
ll S;
if (scanf("%lld %lld", &S, &W) != 2) return 0; // 空输入安全返回
tree.assign(W + 1, 0);
stamp.assign(W + 1, 0);
while (true) {
ll op;
if (scanf("%lld", &op) != 1) break;
if (op == 1) {
ll x, y, a;
scanf("%lld %lld %lld", &x, &y, &a);
Ev e;
e.x = x; e.y = y; e.w = a; e.qid = -1; e.sgn = 0;
ev.push_back(e);
} else if (op == 2) {
ll x1, y1, x2, y2;
scanf("%lld %lld %lld %lld", &x1, &y1, &x2, &y2);
// 每个询问拆成 4 个前缀角点询问
Ev e;
e.x = x1 - 1; e.y = y1 - 1; e.w = 0; e.qid = qcnt; e.sgn = 1; ev.push_back(e);
e.x = x2; e.y = y2; e.w = 0; e.qid = qcnt; e.sgn = 1; ev.push_back(e);
e.x = x1 - 1; e.y = y2; e.w = 0; e.qid = qcnt; e.sgn = -1; ev.push_back(e);
e.x = x2; e.y = y1 - 1; e.w = 0; e.qid = qcnt; e.sgn = -1; ev.push_back(e);
qcnt++;
} else {
break;
}
}
ans.assign(qcnt, 0);
tmp.resize(ev.size());
if (!ev.empty()) cdq(0, (ll)ev.size());
for (ll i = 0; i < qcnt; i++) {
printf("%lld\n", ans[i]);
}
return 0;
}import sys
def main():
data = iter(map(int, sys.stdin.buffer.read().split()))
S = next(data) # S 保证为 0,无需处理
W = next(data)
# 每个询问 (x1,y1,x2,y2) 拆成 4 个前缀角点询问 f(X,Y)=时间更早的修改中
# 满足 x<=X 且 y<=Y 的权值和:f(x2,y2)-f(x1-1,y2)-f(x2,y1-1)+f(x1-1,y1-1)。
# 这样问题变成三维偏序(时间, x, y):CDQ 分治时间维,x 排序后树状数组维护 y。
ev = [] # (x, y, w, qid, sgn);修改 qid=-1,询问角点带 qid 与符号 sgn
cnt = 0
while True:
op = next(data)
if op == 1:
x, y, a = next(data), next(data), next(data)
ev.append((x, y, a, -1, 0))
elif op == 2:
x1, y1, x2, y2 = next(data), next(data), next(data), next(data)
ev.append((x1 - 1, y1 - 1, 0, cnt, 1))
ev.append((x2, y2, 0, cnt, 1))
ev.append((x1 - 1, y2, 0, cnt, -1))
ev.append((x2, y1 - 1, 0, cnt, -1))
cnt += 1
else:
break
m = len(ev)
ans = [0] * cnt
tree = [0] * (W + 1) # 树状数组按下标 y 维护
stamp = [0] * (W + 1) # 时间戳清零,免去每层回滚
cur = 0
def solve(l, r):
nonlocal cur
if r - l <= 1:
return
mid = (l + r) >> 1
solve(l, mid)
solve(mid, r)
# 递归保证两半各自按 x 有序;左半修改(时间早)贡献给右半询问
cur += 1
i = l
for j in range(mid, r):
qx, qy, _, qid, sgn = ev[j]
if qid == -1:
continue
while i < mid and ev[i][0] <= qx:
ex, ey, ew, eq, _ = ev[i]
if eq == -1:
k = ey
while k <= W:
if stamp[k] != cur:
stamp[k] = cur
tree[k] = 0
tree[k] += ew
k += k & -k
i += 1
if sgn:
s = 0
k = qy
while k:
if stamp[k] == cur:
s += tree[k]
k -= k & -k
ans[qid] += sgn * s
# 归并两半按 x 有序,维持不变式
tmp = []
a, b = l, mid
while a < mid and b < r:
if ev[a][0] <= ev[b][0]:
tmp.append(ev[a]); a += 1
else:
tmp.append(ev[b]); b += 1
tmp.extend(ev[a:mid]); tmp.extend(ev[b:r])
ev[l:r] = tmp
solve(0, m)
sys.stdout.write('\n'.join(map(str, ans)) + '\n')
main()复杂度
设事件总数
总结
- 稀疏矩阵的单点加 + 子矩阵求和,不要被
吓到:真正参与运算的只有 个修改点。 - 核心转化链:子矩阵和
四角容斥的二维前缀和 引入时间维后的三维偏序求和 CDQ 分治(时间)+ 归并( )+ 树状数组( )。 - 实现细节:询问拆角点时坐标会出现
;分治内用"时间戳清零"的树状数组,避免每层 回滚;左右两半按 归并保持有序,省去每层重新排序。
图示解析
样例推演
以样例为例(
| 角点 |
容斥符号 | 命中的修改( |
贡献 |
|---|---|---|---|
| 无( |
|||
| 无( |
|||
| 无 |
四个角点的贡献相加得
观察重点是:每个角点本身只做一次"历史修改中