K Symmetry: Convex

题意:给定 nn 个点的凸多边形 CnC_n,输出前 ii 个点构成的凸包的对称轴。n1×105n \leq 1\times 10^5

解法:找对称轴可以使用 Manacher。本题先预处理整个凸包构成的串的 Manacher(注意不要倍长),然后依次加入点去考虑。

容易发现一个性质:随着 ii 的增大,CiC0C1\angle C_iC_0C_1 是在不断变大的。除非该对称轴是其角平分线,则该角必然与另一个角大小相同(对称)。使用一个 map 记录每个角的大小,然后去枚举 CiC0C1\angle C_iC_0C_1 角对称的角在哪里,使用 Manacher 预处理出来的回文半径再附带考虑几个新添加的角和边即可。代码中有更详细的解释。

#include <bits/stdc++.h>
using namespace std;

using _T = long long; // 全局数据类型,可修改为 long long 等

constexpr _T eps = 0;
constexpr long double PI = 3.1415926535897932384l;

// 点与向量
template<typename T> struct point
{
	T x, y;

	bool operator==(const point &a) const { return (abs(x - a.x) <= eps && abs(y - a.y) <= eps); }
	bool operator<(const point &a) const
	{
		if (abs(x - a.x) <= eps)
			return y < a.y - eps;
		return x < a.x - eps;
	}
	bool operator>(const point &a) const { return !(*this < a || *this == a); }
	point operator+(const point &a) const { return {x + a.x, y + a.y}; }
	point operator-(const point &a) const { return {x - a.x, y - a.y}; }
	point operator-() const { return {-x, -y}; }
	point operator*(const T k) const { return {k * x, k * y}; }
	point operator/(const T k) const { return {x / k, y / k}; }
	T operator*(const point &a) const { return x * a.x + y * a.y; } // 点积
	T operator^(const point &a) const { return x * a.y - y * a.x; } // 叉积,注意优先级
	int toleft(const point &a) const
	{
		const auto t = (*this) ^ a;
		return (t > eps) - (t < -eps);
	}									   // to-left 测试
	T len2() const { return (*this) * (*this); }				  // 向量长度的平方
	T dis2(const point &a) const { return (a - (*this)).len2(); } // 两点距离的平方

	// 涉及浮点数
	long double len() const { return sqrtl(len2()); }																	   // 向量长度
	long double dis(const point &a) const { return sqrtl(dis2(a)); }													   // 两点距离
	long double ang(const point &a) const { return acosl(max(-1.0, min(1.0, ((*this) * a) / (len() * a.len())))); }		   // 向量夹角
	point rot(const long double rad) const { return {x * cos(rad) - y * sin(rad), x * sin(rad) + y * cos(rad)}; }		   // 逆时针旋转(给定角度)
	point rot(const long double cosr, const long double sinr) const { return {x * cosr - y * sinr, x * sinr + y * cosr}; } // 逆时针旋转(给定角度的正弦与余弦)
};

using Point = point<_T>;

// 直线
template<typename T> struct line
{
	point<T> p, v; // p 为直线上一点,v 为方向向量

	bool operator==(const line &a) const { return v.toleft(a.v) == 0 && v.toleft(p - a.p) == 0; }
	int toleft(const point<T> &a) const { return v.toleft(a - p); } // to-left 测试

  // 涉及浮点数
	point<T> inter(const line &a) const { return p + v * ((a.v ^ (p - a.p)) / (v ^ a.v)); } // 直线交点
	long double dis(const point<T> &a) const { return abs(v ^ (a - p)) / v.len(); }			// 点到直线距离
	point<T> proj(const point<T> &a) const { return p + v * ((v * (a - p)) / (v * v)); }	// 点在直线上的投影
};

using Line = line<_T>;

vector<int> Manacher(vector<long long> &s)
{
    int n = s.size();
    vector<int> p(n, 1);
    for (int i = 0, r = 0, m = 0; i < n; i++)
    {
        if (i < r)
            p[i] = min(p[m * 2 - i], r - i);
        while (i >= p[i] && i + p[i] < n && s[i - p[i]] == s[i + p[i]])
            p[i]++;
        if (i + p[i] > r)
        {
            m = i;
            r = i + p[i];
        }
    }
    return p;
}

Line prep(Point a, Point b)//中垂线,经过的点为(A.x+B.x, A.y+B.y) 最后输出要还原
{
    Point dir = (Point){b.y - a.y, a.x - b.x}, p = a + b;
    return (Line){p, dir};
}
void print(Line l)
{
    long long a = -l.v.y, b = l.v.x;
    long long c = -a * l.p.x - b * l.p.y;
    a <<= 1;
    b <<= 1;
    long long d = __gcd(abs(c), __gcd(abs(a), abs(b)));
    printf("%lld %lld %lld\n", a / d, b / d, c / d);
}

int main()
{
    int t, n;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d", &n);
        vector<Point> C(n);
        for (int i = 0; i < n;i++)
            scanf("%lld%lld", &C[i].x, &C[i].y);
        vector<long long> s;
        for (int i = 0; i < n;i++)
        {
            s.push_back(C[i].dis2(C[(i + 1) % n]));
            s.push_back((C[i] - C[(i + 1) % n]) * (C[(i + 2) % n] - C[(i + 1) % n]));
        }
        map<long long, vector<int>> ang;
        for (int i = 1; i < 2 * n; i += 2)
            ang[s[i]].push_back(i);
        auto para = Manacher(s);
        for (int i = 2; i < n;i++)
        {
            vector<Line> ans;
            if (para[i] >= i && s[0] == C[i].dis2(C[0]))// i01角平分线,用i的对踵点/边(para数组中的第i位)判断。若刚好能覆盖到0,则除了01和0i的边其余均相等。
                ans.push_back(prep(C[1], C[i]));
            auto lastang = (C[1] - C[0]) * (C[i] - C[0]);
            if (para[i - 1] >= i && lastang == (C[0] - C[i]) * (C[i - 1] - C[i]))//0i的中垂线。用0i的对踵点(para中第i-1位判断)是否能覆盖到0,剩下要判断的就是i01角和0i边
                ans.push_back(prep(C[0], C[i]));
            //接下来将整个凸包分成[0,j] 侧和 [j,i] 侧,0与j角对称相等
            for (auto j : ang[lastang])
            {
                if (j >= 2 * i - 1)
                    break;
                if (para[(j - 1) / 2] < (j - 1) / 2 + 1) //确保[0,j]侧匹配(对称),即是找这一段的中点,查询其覆盖半径能否到达0
                    continue;
                if (j == 2 * i - 3) //(i-1)i0的角平分线
                {
                    if (C[i].dis2(C[0]) == C[i].dis2(C[i - 1]))
                        ans.push_back(prep(C[0], C[i - 1]));
                }
                else
                {
                    int outercen = (2 * i - 2 + j + 3) / 2;
                    if (para[outercen] >= (2 * i - 2 - j - 3) / 2 + 1)//外侧[j,i]可以匹配
                    {
                        int oriid = (j + 1) / 2;
                        if (C[i].dis2(C[0]) == C[oriid].dis2(C[oriid + 1]) && (C[0] - C[i]) * (C[i - 1] - C[i]) == (C[oriid] - C[oriid + 1]) * (C[oriid + 2] - C[oriid + 1]))
                        //最后的一条边和一个角判断。这一段由于是新加的因而需要手动判断
                            ans.push_back(prep(C[0], C[oriid]));
                    }
                }
            }
            printf("%d\n", ans.size());
            for (auto j : ans)
                print(j);
        }
    }
    return 0;
}