把平面操作统一为仿射变换,在线段树中维护区间的一次矩和二次矩并懒惰复合变换。
OJ: shumeng
题目 ID: CSP202206E
难度:提高+/省选-
标签:数据结构线段树仿射变换线性代数计算几何
日期: 2026-07-31 16:21
形式化题目
给定
坐标和操作参数都是实数,任意时刻点坐标绝对值不超过
思路
先看逐点执行操作的朴素程序,它只在数据很小时可行,但能直观对应题目语义:
cpp
/**
* 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-07-31 16:21
* update_at: 2026-08-17 22:40
*/
// brute.cpp:小数据暴力解,逐点执行每次仿射变换,用来辅助对拍。
#include <bits/stdc++.h>
using namespace std;
struct Point {
long double x, y;
};
int n, query_count;
vector<Point> point;
void translate(int left, int right, long double x, long double y) {
for (int i = left; i <= right; i++) {
point[i].x += x;
point[i].y += y;
}
}
void rotate_point(int left, int right, long double x, long double y, long double theta) {
long double c = cos(theta), s = sin(theta);
for (int i = left; i <= right; i++) {
long double old_x = point[i].x - x;
long double old_y = point[i].y - y;
point[i].x = c * old_x - s * old_y + x;
point[i].y = s * old_x + c * old_y + y;
}
}
void scale_point(int left, int right, long double x, long double y, long double lambda) {
for (int i = left; i <= right; i++) {
point[i].x = x + lambda * (point[i].x - x);
point[i].y = y + lambda * (point[i].y - y);
}
}
void line_transform(int left, int right, long double theta, long double y0, bool projection) {
long double c = cos(theta), s = sin(theta);
long double m00, m01, m10, m11;
if (projection) {
m00 = c * c;
m01 = c * s;
m10 = c * s;
m11 = s * s;
} else {
m00 = c * c - s * s;
m01 = 2 * c * s;
m10 = 2 * c * s;
m11 = s * s - c * c;
}
long double vx = -m01 * y0;
long double vy = y0 - m11 * y0;
for (int i = left; i <= right; i++) {
long double old_x = point[i].x;
long double old_y = point[i].y;
point[i].x = m00 * old_x + m01 * old_y + vx;
point[i].y = m10 * old_x + m11 * old_y + vy;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> query_count;
point.resize(n + 1);
for (int i = 1; i <= n; i++) cin >> point[i].x >> point[i].y;
cout << fixed << setprecision(6);
while (query_count--) {
int type, left, right;
cin >> type >> left >> right;
if (type == 1) {
long double a, b;
cin >> a >> b;
translate(left, right, a, b);
} else if (type == 2) {
long double a, b, theta;
cin >> a >> b >> theta;
rotate_point(left, right, a, b, theta);
} else if (type == 3) {
long double a, b, lambda;
cin >> a >> b >> lambda;
scale_point(left, right, a, b, lambda);
} else if (type == 4 || type == 5) {
long double theta, y0;
cin >> theta >> y0;
line_transform(left, right, theta, y0, type == 5);
} else if (type == 6) {
long double sx = 0, sy = 0;
for (int i = left; i <= right; i++) {
sx += point[i].x;
sy += point[i].y;
}
long double count = right - left + 1;
cout << sx / count << ' ' << sy / count << '\n';
} else {
long double a, b, answer = 0;
cin >> a >> b;
for (int i = left; i <= right; i++) {
long double dx = point[i].x - a;
long double dy = point[i].y - b;
answer += dx * dx + dy * dy;
}
cout << answer << '\n';
}
}
return 0;
}正解的关键是把五种几何操作统一成同一个数学形式,再用线段树批量维护。
统一表示平面操作
把点写成列向量
其中
- 平移
: , ; - 绕
逆时针旋转 : , ; - 绕
放缩 : , 。
令直线方向单位向量为
若连续执行两个变换
因此线段树的懒标记只需保存一个仿射变换。
维护区间矩
对每个线段树节点维护一次矩与二次矩:
设一次仿射变换为
这样整段覆盖时只更新节点统计量和懒标记,不需要访问区间内的每个点。
回答查询
- 重心:
。 - 到
的距离平方和: 。
区间查询用线段树合并矩;下传懒标记前先把同一个仿射变换应用到左右儿子。
代码
cpp
/**
* 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-07-31 16:21
* update_at: 2026-08-17 22:40
*/
#include <bits/stdc++.h>
using namespace std;
struct Moment {
// 区间内点的数量、一次矩和二次矩。
long double count, sx, sy, sxx, syy, sxy;
};
struct Affine {
// (x,y) -> M(x,y) + v。
long double m00, m01, m10, m11, vx, vy;
};
struct Node {
Moment data;
Affine lazy;
};
int n, query_count, base;
vector<Node> tree;
Affine identity() {
return {1, 0, 0, 1, 0, 0};
}
Affine compose(const Affine &first, const Affine &second) {
// 先执行 second,再执行 first。
return {
first.m00 * second.m00 + first.m01 * second.m10,
first.m00 * second.m01 + first.m01 * second.m11,
first.m10 * second.m00 + first.m11 * second.m10,
first.m10 * second.m01 + first.m11 * second.m11,
first.m00 * second.vx + first.m01 * second.vy + first.vx,
first.m10 * second.vx + first.m11 * second.vy + first.vy,
};
}
Moment merge_moment(const Moment &left, const Moment &right) {
return {left.count + right.count, left.sx + right.sx, left.sy + right.sy,
left.sxx + right.sxx, left.syy + right.syy, left.sxy + right.sxy};
}
void apply_node(int node, const Affine &transform) {
Moment old = tree[node].data;
long double nx = transform.m00 * old.sx + transform.m01 * old.sy + old.count * transform.vx;
long double ny = transform.m10 * old.sx + transform.m11 * old.sy + old.count * transform.vy;
long double nxx = transform.m00 * transform.m00 * old.sxx
+ transform.m01 * transform.m01 * old.syy
+ 2 * transform.m00 * transform.m01 * old.sxy
+ 2 * transform.vx * (transform.m00 * old.sx + transform.m01 * old.sy)
+ old.count * transform.vx * transform.vx;
long double nyy = transform.m10 * transform.m10 * old.sxx
+ transform.m11 * transform.m11 * old.syy
+ 2 * transform.m10 * transform.m11 * old.sxy
+ 2 * transform.vy * (transform.m10 * old.sx + transform.m11 * old.sy)
+ old.count * transform.vy * transform.vy;
long double nxy = transform.m00 * transform.m10 * old.sxx
+ (transform.m00 * transform.m11 + transform.m01 * transform.m10) * old.sxy
+ transform.m01 * transform.m11 * old.syy
+ transform.vx * (transform.m10 * old.sx + transform.m11 * old.sy)
+ transform.vy * (transform.m00 * old.sx + transform.m01 * old.sy)
+ old.count * transform.vx * transform.vy;
tree[node].data.sx = nx;
tree[node].data.sy = ny;
tree[node].data.sxx = nxx;
tree[node].data.syy = nyy;
tree[node].data.sxy = nxy;
tree[node].lazy = compose(transform, tree[node].lazy);
}
void pull(int node) {
tree[node].data = merge_moment(tree[node << 1].data, tree[node << 1 | 1].data);
}
void push(int node) {
Affine lazy = tree[node].lazy;
if (lazy.m00 == 1 && lazy.m01 == 0 && lazy.m10 == 0 && lazy.m11 == 1
&& lazy.vx == 0 && lazy.vy == 0) return;
apply_node(node << 1, lazy);
apply_node(node << 1 | 1, lazy);
tree[node].lazy = identity();
}
void update(int node, int left, int right, int ql, int qr, const Affine &transform) {
if (ql <= left && right <= qr) {
apply_node(node, transform);
return;
}
push(node);
int middle = (left + right) >> 1;
if (ql <= middle) update(node << 1, left, middle, ql, qr, transform);
if (qr > middle) update(node << 1 | 1, middle + 1, right, ql, qr, transform);
pull(node);
}
Moment query(int node, int left, int right, int ql, int qr) {
if (ql <= left && right <= qr) return tree[node].data;
push(node);
int middle = (left + right) >> 1;
Moment result = {};
if (ql <= middle) result = merge_moment(result, query(node << 1, left, middle, ql, qr));
if (qr > middle) result = merge_moment(result, query(node << 1 | 1, middle + 1, right, ql, qr));
return result;
}
Affine transform_line(long double theta, long double y0, bool projection) {
long double c = cos(theta), s = sin(theta);
Affine result;
if (projection) result = {c * c, c * s, c * s, s * s, 0, 0};
else result = {c * c - s * s, 2 * c * s, 2 * c * s, s * s - c * c, 0, 0};
result.vx = -result.m01 * y0;
result.vy = y0 - result.m11 * y0;
return result;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> query_count;
base = 1;
while (base < n) base <<= 1;
tree.resize(base << 1);
for (int i = 1; i < (base << 1); i++) tree[i].lazy = identity();
for (int i = 1; i <= n; i++) {
long double x, y;
cin >> x >> y;
tree[base + i - 1].data = {1, x, y, x * x, y * y, x * y};
}
for (int i = base - 1; i >= 1; i--) pull(i);
cout << fixed << setprecision(6);
while (query_count--) {
int type, left, right;
cin >> type >> left >> right;
if (type <= 5) {
long double a, b, theta, lambda;
Affine transform;
if (type == 1) {
cin >> a >> b;
transform = {1, 0, 0, 1, a, b};
} else if (type == 2) {
cin >> a >> b >> theta;
long double c = cos(theta), s = sin(theta);
transform = {c, -s, s, c, a - c * a + s * b, b - s * a - c * b};
} else if (type == 3) {
cin >> a >> b >> lambda;
transform = {lambda, 0, 0, lambda, a * (1 - lambda), b * (1 - lambda)};
} else {
cin >> theta >> b;
transform = transform_line(theta, b, type == 5);
}
update(1, 1, base, left, right, transform);
} else if (type == 6) {
Moment result = query(1, 1, base, left, right);
cout << result.sx / result.count << ' ' << result.sy / result.count << '\n';
} else {
long double a, b;
cin >> a >> b;
Moment result = query(1, 1, base, left, right);
long double answer = result.sxx + result.syy - 2 * a * result.sx - 2 * b * result.sy
+ result.count * (a * a + b * b);
cout << answer << '\n';
}
}
return 0;
}复杂度
建树时间为
总结
这道题的关键是把“对很多点做同一个几何操作”转成“对点集的低阶矩做一次更新”。仿射变换保证二阶矩仍能用有限个统计量表示,线段树负责组织区间覆盖与查询。使用 long double 可以减少大数二次矩在展开式中相减时的精度损失。