PS 无限版

把平面操作统一为仿射变换,在线段树中维护区间的一次矩和二次矩并懒惰复合变换。

OJ: shumeng

题目 ID: CSP202206E

难度:提高+/省选-

标签:数据结构线段树仿射变换线性代数计算几何

日期: 2026-07-31 16:21

形式化题目

给定 nn 个平面点,支持对编号连续区间执行平移、旋转、放缩、关于直线对称、投影五种修改操作,并查询区间点集的重心,或到指定点 (a,b)(a,b) 的距离平方和。

坐标和操作参数都是实数,任意时刻点坐标绝对值不超过 10610^6n5×105n\le 5\times 10^5,操作数不超过 2×1042\times 10^4,因此不能每次操作都逐点修改。

思路

先看逐点执行操作的朴素程序,它只在数据很小时可行,但能直观对应题目语义:

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;
}

正解的关键是把五种几何操作统一成同一个数学形式,再用线段树批量维护。

统一表示平面操作

把点写成列向量 p=(x,y)Tp=(x,y)^T。五种修改都可以写成仿射变换:

p=Mp+v,p' = Mp+v,

其中 MM2×22\times2 矩阵,vv 是平移向量。例如:

  • 平移 (a,b)(a,b)M=IM=Iv=(a,b)Tv=(a,b)^T
  • (a,b)(a,b) 逆时针旋转 θ\thetaM=[cosθsinθsinθcosθ]M=\begin{bmatrix}\cos\theta&-\sin\theta\\\sin\theta&\cos\theta\end{bmatrix}v=(a,b)TM(a,b)Tv=(a,b)^T-M(a,b)^T
  • (a,b)(a,b) 放缩 λ\lambdaM=λIM=\lambda Iv=(1λ)(a,b)Tv=(1-\lambda)(a,b)^T

令直线方向单位向量为 u=(cosθ,sinθ)Tu=(\cos\theta,\sin\theta)^T,直线上点 p0=(0,y0)Tp_0=(0,y_0)^T。投影的线性部分是 uuTuu^T,对称的线性部分是 2uuTI2uu^T-I,两者的平移部分都为 p0Mp0p_0-Mp_0

若连续执行两个变换 T1(p)=M1p+v1T_1(p)=M_1p+v_1T2(p)=M2p+v2T_2(p)=M_2p+v_2,合成结果仍是仿射变换:

T1(T2(p))=(M1M2)p+(M1v2+v1)T_1(T_2(p))=(M_1M_2)p+(M_1v_2+v_1)。

因此线段树的懒标记只需保存一个仿射变换。

维护区间矩

对每个线段树节点维护一次矩与二次矩:

(N,Sx,Sy,Sxx,Syy,Sxy)=(m,xi,yi,xi2,yi2,xiyi)(N,S_x,S_y,S_{xx},S_{yy},S_{xy}) =\left(m,\sum x_i,\sum y_i,\sum x_i^2,\sum y_i^2,\sum x_iy_i\right)。

设一次仿射变换为 x=ax+by+ux'=ax+by+uy=cx+dy+vy'=cx+dy+v,那么整段覆盖时矩可以整体更新:

Sx=aSx+bSy+Nu,Sy=cSx+dSy+Nv,Sxx=a2Sxx+b2Syy+2abSxy+2u(aSx+bSy)+Nu2,Syy=c2Sxx+d2Syy+2cdSxy+2v(cSx+dSy)+Nv2,Sxy=acSxx+(ad+bc)Sxy+bdSyy+u(cSx+dSy)+v(aSx+bSy)+Nuv \begin{aligned} S_x'&=aS_x+bS_y+Nu,\\ S_y'&=cS_x+dS_y+Nv,\\ S_{xx}'&=a^2S_{xx}+b^2S_{yy}+2abS_{xy}+2u(aS_x+bS_y)+Nu^2,\\ S_{yy}'&=c^2S_{xx}+d^2S_{yy}+2cdS_{xy}+2v(cS_x+dS_y)+Nv^2,\\ S_{xy}'&=acS_{xx}+(ad+bc)S_{xy}+bdS_{yy}\\ &\quad+u(cS_x+dS_y)+v(aS_x+bS_y)+Nuv。 \end{aligned}

这样整段覆盖时只更新节点统计量和懒标记,不需要访问区间内的每个点。

回答查询

  • 重心:(SxN,SyN)\left(\dfrac{S_x}{N},\dfrac{S_y}{N}\right)
  • (a,b)(a,b) 的距离平方和:Sxx+Syy2aSx2bSy+N(a2+b2)S_{xx}+S_{yy}-2aS_x-2bS_y+N(a^2+b^2)

区间查询用线段树合并矩;下传懒标记前先把同一个仿射变换应用到左右儿子。

代码

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;
}

复杂度

建树时间为 O(n)O(n)。每次区间修改或查询访问 O(logn)O(\log n) 个线段树节点,每个节点的矩更新和合并都是 O(1)O(1),总时间复杂度为 O((n+q)logn)O((n+q)\log n),空间复杂度为 O(n)O(n)

总结

这道题的关键是把“对很多点做同一个几何操作”转成“对点集的低阶矩做一次更新”。仿射变换保证二阶矩仍能用有限个统计量表示,线段树负责组织区间覆盖与查询。使用 long double 可以减少大数二次矩在展开式中相减时的精度损失。