计算几何杂记

计算几何杂记

基础知识

定义符号函数:

inline int sgn(double x) {
    return fabs(x) < eps ? 0 : (x > 0 ? 1 : -1);
}

向量

由于点和向量通常都用坐标表示,为了方便可以共用一个结构体表示。

struct Vec {
    double x, y;

    inline Vec() : x(0), y(0) {}

    inline Vec(double _x, double _y) : x(_x), y(_y) {}

    inline friend Vec operator + (Vec a, Vec b) {
        return Vec(a.x + b.x, a.y + b.y);
    }

    inline friend Vec operator - (Vec a, Vec b) {
        return Vec(a.x - b.x, a.y - b.y);
    }

    inline Vec operator * (double k) {
        return Vec(x * k, y * k);
    }

    inline Vec operator / (double k) {
        return Vec(x / k, y / k);
    }

    inline friend double dwjr(Vec a, Vec b) {
        return a.x * b.x + a.y * b.y;
    }

    inline friend double xwjr(Vec a, Vec b) {
        return a.x * b.y - a.y * b.x;
    }

    inline double length() {
        return sqrt(x * x + y * y);
    }

    inline double angle() {
        return atan2(y, x);
    }

    inline Vec rotate(double k) {
        return Vec(x * cos(k) - y * sin(k), y * sin(k) + x * sin(k));
    }
};

直线

可用一对互不相同的点确定一条直线,也可以用直线上一点和直线上的方向向量确定一条直线。

判断点 \(P\) 在直线 \(AB\) 的哪一边:

inline double side(Vec a, Vec b, Vec c) {
    return sgn(xwjr(b - a, c - b));
}

直线 \(AB\) 与直线 \(CD\) 求交点:

inline Vec line_intersection(Vec a, Vec b, Vec c, Vec d) {
    Vec u = b - a, v = d - c;
    return a + u * (xwjr(c - a, v) / xwjr(u, v));
}

\(P\) 到直线 \(AB\) 的距离:

inline double dist_line(Vec p, Vec a, Vec b) {
    Vec u = b - a, v = p - a;
    return fabs(xwjr(u, v)) / u.length();
}

\(P\) 到线段 \(AB\) 的距离:

inline double dist_seg(Vec p, Vec a, Vec b) {
    if (a == b)
        return (p - a).length();

    vec u = b - a, va = p - a, vb = p - b;

    if (sgn(dwjr(u, va)) < 0)
        return va.length();

    if (sgn(dwjr(u, vb)) > 0)
        return vb.length();

    return dist_line(p, a, b);
}

\(P\) 在直线 \(AB\) 上的投影:

inline Vec line_projection(Vec p, Vec a, Vec b) {
    Vec u = b - a, v = p - a;
    return a + u * (dwjr(u, v) / dwjr(u, u));
}

\(P\) 是否在线段 \(AB\) 上:

inline bool on_seg(Vec p, Vec a, Vec b) {
    Vec u = p - a, v = p - b;
    return !sgn(xwjr(u, v)) && sgn(dwjr(u, v)) <= 0;
}

判断线段 \(AB\) 与线段 \(CD\) 是否有交点(跨立实验,特判共线不相交的情况):

inline bool seg_intersection(Vec a, Vec b, Vec c, Vec d) {
    return sgn(xwjr(b - a, c - a)) * sgn(xwjr(b - a, d - a)) <= 0 && 
        sgn(xwjr(d - c, a - c)) * sgn(xwjr(d - c, b - c)) <= 0 &&
        (sgn(xwjr(b - a, c - d)) || on_seg(a, c, d) || on_seg(b, c, d));
}

极坐标系

\(A\) 为平面上一点。

  • 极点 \(O\)\(A\) 之间的距离 \(|OA|\) 称为极径,记作 \(\rho\)
  • 以极轴为始边,\(OA\) 为终边的角 \(\angle xOA\) 称为极角,即为 \(\varphi\)

那么有序数对 \((\rho, \varphi)\) 即为 \(A\) 的极坐标。

由终边相同的角的定义可知 \((\rho, \varphi)\)\((\rho, \varphi + 2k \pi), k \in \mathbb{Z}\) 其实表示的是一样的点。特别地,极点的极坐标为 \((0, \varphi), \varphi \in \mathbb{R}\) ,于是平面内的点的极坐标表示有无数多种。

如果规定 \(\rho \geq 0, 0 \leq \varphi < 2\pi\) ,那么除极点外,其他平面内的点可以用唯一有序数对 \((\rho, \varphi)\) 表示,而每个极坐标表示的点是唯一确定的。

当然,有时候研究极坐标系下的图形有些不方便。要想转到直角坐标系下研究,有互化公式。极坐标 \((\rho, \varphi)\) 与直角坐标 \((x, y)\) 有如下关系:

\[\begin{aligned} &\begin{cases} x = \rho \cos \varphi \\ y = \rho \sin \varphi \end{cases} \\ &\begin{cases} \rho^2 = x^2 + y^2 \\ \tan \varphi = \dfrac{y}{x} \quad (x \not = 0) \end{cases} \end{aligned} \]

即:

\[\begin{cases} \rho = \sqrt{x^2 + y^2} \\ \varphi = \operatorname{atan2}(y, x) \end{cases} \]

其中 \(\operatorname{atan2}\) 的值域为 \((-\pi, \pi]\)

其他

任意多边形面积:将多边形上的点逆时针标记为 \(P_{1 \sim n}\) ,再任选一个辅助点 \(O\) ,记 \(\mathbf{v_i} = P_i - O\) ,则:

\[S = \frac{1}{2} \left| \sum_{i = 1}^n \mathbf{v}_i \times \mathbf{v}_{(i \bmod n) + 1} \right| \]

二维凸包

P2742 [USACO5.1] 圈奶牛Fencing the Cows /【模板】二维凸包

凸包:在平面上能包含所有给定点的最小凸多边形叫做凸包,常用 Andrew 算法求解。

首先把所有点以横坐标为第一关键字,纵坐标为第二关键字升序排序。

显然排序后首尾元素一定在凸包上。因为是凸多边形,如果从一个点出发逆时针走,轨迹总是左拐的。一旦出现右拐,就说明这一段不在凸包上。因此考虑用单调栈维护上下凸壳,其中判断拐的方向可以用叉积判断。

由于从左到右是上下凸壳旋转的方向不同,因此考虑做两遍单调栈分别求解。

若要保留凸包边上的点,只需要将弹栈条件的等号去掉即可。

时间复杂度 \(O(n \log n)\) ,瓶颈在于排序。

inline vector<Vec> Andrew(vector<Vec> p) {
    if (p.size() == 1)
        return p;

    sort(p.begin(), p.end(), [](Vec a, Vec b) {
        return a.x == b.x ? a.y < b.y : a.x < b.x;
    });

    if (p.size() == 2)
        return p;

    vector<Vec> down = {p[0], p[1]};

    for (int i = 2; i < p.size(); ++i) {
        while (down.size() > 1 && side(down[down.size() - 2], down.back(), p[i]) <= 0)
            down.pop_back();

        down.emplace_back(p[i]);
    }

    vector<Vec> up = {p.back(), p[p.size() - 2]};

    for (int i = p.size() - 3; ~i; --i) {
        while (up.size() > 1 && side(up[up.size() - 2], up.back(), p[i]) <= 0)
            up.pop_back();

        up.emplace_back(p[i]);
    }

    up.erase(up.begin()), up.pop_back();
    down.insert(down.end(), up.begin(), up.end());
    return down;
}

旋转卡壳

求凸包直径

P1452 【模板】旋转卡壳 | [USACO03FALL] Beauty Contest G

主要思想是用一对平行线夹住这个凸包,那么旋转这条平行线时间距在不断变化,只要找到最大间距,即为凸包的直径,

考虑逆时针遍历凸包上的边,对于每条边都找到离这条边最远的点。那么随着边的转动,对应的最远点也在逆时针旋转,因此可以用双指针维护这个过程做到线性。

比较长度可以直接比较三角形的面积。

inline int Diameter(vector<Point> p) {
    int n = p.size();

    if (n <= 1)
        return -1;
    else if (n == 2)
        return dist(p[0], p[1]);

    int ans = 0, i = 0, j = 1;

    auto calc = [](Point a, Point b, Point c) {
        return xwjr(Vec(a, b), Vec(b, c));
    };

    do {
        while (calc(p[i], p[(i + 1) % n], p[(j + 1) % n]) > calc(p[i], p[(i + 1) % n], p[j]))
            j = (j + 1) % n, ans = max(ans, dist(p[i], p[j]));

        ans = max(ans, dist(p[i = (i + 1) % n], p[j]));
    } while (i);

    return ans;
}

最小矩形覆盖

P3187 [HNOI2007] 最小矩形覆盖

首先有结论:最小矩形的一条边一定与凸包重合。

考虑旋转卡壳,枚举一条在矩形上的边,维护其对面的平行线,以及一对与之垂直的夹住凸包的平行线。但是维护平行线比较困难,考虑维护另一对平行线切到的点,判断最优性直接用投影长度判断即可。

同样可以直接双指针维护做到线性。

inline Rectangle solve(vector<Vec> p) {
    int n = p.size();

    if (n <= 1)
        return (Rectangle){p[0], p[0], p[0], p[0], 0};
    else if (n == 2)
        return (Rectangle){p[0], p[0], p[1], p[1], 0};

    Rectangle ans;
    ans.s = 1e9;

    auto calc = [](Vec a, Vec b, Vec c) {
        return xwjr(b - a, c - b);
    };

    int i = 0, j = 1, l = 1, r = 1;

    while (i < n) {
        Vec pa = p[i], pb = p[(i + 1) % n];

        while (calc(pa, pb, p[(j + 1) % n]) > calc(pa, pb, p[j])) {
            if (r == j)
                r = (j + 1) % n;

            j = (j + 1) % n;
        }

        while (l != j && dwjr(pb - pa, p[(l + 1) % n] - pa) > dwjr(pb - pa, p[l] - pa))
            l = (l + 1) % n;

        while (r != i && dwjr(pa - pb, p[(r + 1) % n] - pb) > dwjr(pa - pb, p[r] - pb))
            r = (r + 1) % n;

        Vec a = line_projection(p[l], pa, pb),
            b = line_projection(p[l], p[j], p[j] + (pb - pa)),
            c = line_projection(p[r], p[j], p[j] + (pb - pa)),
            d = line_projection(p[r], pa, pb);

        ans = min(ans, (Rectangle){a, b, c, d, (a - b).length() * (b - c).length()});

        if (l == ++i % n)
            l = (l + 1) % n;
    }

    return ans;
}

半平面交

P4196 [CQOI2006] 凸多边形 /【模板】半平面交

半平面:一条直线的一侧所有点构成的点集。包含直线时称为闭半平面,不含直线时称为开半平面。解析式形如 \(Ax + By + C \geq 0\) (右侧,闭半平面),常用 S&I 算法求解。

先将所有向量按极角排序,如果遇到共线向量则取靠近可行域的一个。

用一个双端队列维护目前半平面交的边界向量。对于每个需要插入的向量,若队尾后两条向量的交点不在当前向量的可行域内,则说明队尾的向量是没用的,将其弹出。

但是快结束时,队首的向量也可能没有(形如螺旋形),因此还要对队首向量重复上述操作,最后将当前向量插入双端队列。

注意这里弹出的顺序一定是先队尾再队首,具体原因可以画图理解。

最后用队首的向量排除一下队尾多余的向量。因为队首的向量会被后面的约束,而队尾的向量不会。此时它们围成了一个环,因此队首的向量就可以约束队尾的向量。

对于交集无穷的情况,可以一开始先在无穷远处放一个向量处理。

signed main() {
    scanf("%d", &n);
    vector<Line> e;

    for (int i = 1; i <= n; ++i) {
        int m;
        scanf("%d", &m);
        vector<Vec> p(m);

        for (Vec &it : p)
            scanf("%lf%lf", &it.x, &it.y);

        for (int i = 0; i < m; ++i)
            e.emplace_back(p[i], p[(i + 1) % m]);
    }

    sort(e.begin(), e.end(), [](const Line &a, const Line &b) {
        return sgn(a.angle - b.angle) ? a.angle < b.angle : side(a.s, b.s, b.t) > 0;
    });

    e.erase(unique(e.begin(), e.end(), [](const Line &a, const Line &b) {
        return !sgn(a.angle - b.angle);
    }), e.end());

    deque<Line> q = {e[0], e[1]};

    for (int i = 2; i < e.size(); ++i) {
        while (q.size() > 1 && check(e[i], q.back(), q[q.size() - 2]))
            q.pop_back();

        while (q.size() > 1 && check(e[i], q[0], q[1]))
            q.pop_front();

        q.emplace_back(e[i]);
    }

    while (q.size() > 1 && check(q[0], q[q.size() - 2], q.back()))
        q.pop_back();

    vector<Vec> p;

    for (int i = 0; i + 1 < q.size(); ++i)
        p.emplace_back(line_intersection(q[i].s, q[i].t, q[i + 1].s, q[i + 1].t));

    if (q.size() > 2)
        p.emplace_back(line_intersection(q[0].s, q[0].t, q.back().s, q.back().t));

    double ans = 0;

    for (int i = 0; i < p.size(); ++i)
        ans += xwjr(p[i], p[(i + 1) % p.size()]);

    printf("%.3lf", fabs(ans) / 2);
    return 0;
}

经典结论

  • \(n \times n\) 的整点网格中,整点集合的最大凸包大小(点的数量)为 \(O(n^{\frac{2}{3}})\) 级别。

    证明:考虑下凸包,由于凸包上各线段的斜率递增,且每条线段的斜率都可被写成分数 \(\frac{a}{b}\) 的形式,其中 \(\sum a, \sum b \leq n\)

    考虑 \(a + b \leq n^{\frac{1}{3}}\) 的分数,显然只有 \(O(n^{\frac{1}{3}}) \times O(n^{\frac{1}{3}}) = O(n^{\frac{2}{3}})\) 个这样的点。对于 \(a + b > n^{\frac{1}{3}}\) 的分数,由于 \(\sum a + \sum b \leq 2n\) ,因此这样的分数也只有 \(O(\frac{n}{n^{\frac{1}{3}}}) = O(n^{\frac{2}{3}})\) 个。

    事实上由于 \(a \bot b\)\(\sum a, \sum b\) 不超过 \(1\) 到该点的距离,因此这个数量级是很松的。

  • Pick 定理:对于顶点均为整点的简单多边形,记面积为 \(S\) ,内部整点数位 \(a\) ,边上整点数为 \(b\) ,则 \(S = a + \frac{b}{2} - 1\)

杂题选做

CF607E Cross Sum

给定 \(n\) 条互不平行的直线,共有 \(\frac{n(n - 1)}{2}\) 个交点(同一位置算多个),求离 \((X, Y)\)\(m\) 近的交点距离 \((X, Y)\) 的距离和。

\(n \leq 5 \times 10^4\)\(m \leq \min(3 \times 10^7, \frac{n(n - 1)}{2})\) ,TL = 7s

先考虑求第 \(k\) 近的距离([ABC263Ex] Intersection 2),考虑二分答案 \(r\) ,则可能的交点都在半径为 \(r\) 的圆内。

不难发现圆内的交点一定是两条弦的交点,找出所有的弦,问题转化为求有多少区间相交,不难排序后用树状数组做扫描线解决。

下面考虑求前 \(m\) 近的距离和,发现这些点一定在圆内或圆上,因此暴力统计圆内的所有交点,圆上的交点只要统计数量(\(m\) 减去圆内交点数量)然后乘上半径 \(r\) 即可。圆内的所有交点可以用 set 维护,暴力遍历即可。

时间复杂度 \(O(n \log n \log V + m \log n)\)

#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-8;
const int N = 5e4 + 7;

struct Line {
    double a, b, c;
} p[N];

int id[N];

double X, Y;
int n, m;

namespace BIT {
int c[N];

inline void update(int x, int k) {
    for (; x <= n; x += x & -x)
        c[x] += k;
}

inline int query(int x) {
    int res = 0;

    for (; x; x -= x & -x)
        res += c[x];

    return res;
}
} // namespace BIT

inline vector<tuple<double, int, int> > prework(double R) {
    vector<tuple<double, int, int> > vec;

    for (int i = 1; i <= n; ++i) {
        double a = p[i].a, b = p[i].b, c = p[i].c;

        if (fabs(b) < eps) {
            double x = -c / a;

            if (fabs(x) <= R) {
                double t = atan2(sqrt(R * R - x * x), x), tt = atan2(-sqrt(R * R - x * x), x);
                vec.emplace_back(min(t, tt), -1, i), vec.emplace_back(max(t, tt), 1, i);
            }
        } else {
            double res = 4 * a * a * c * c - 4 * (a * a + b * b) * (c * c - b * b * R * R);

            if (res < 0)
                continue;

            double x = (-2 * a * c - sqrt(res)) / (2 * (a * a + b * b)), y = -(a * x + c) / b, t = atan2(y, x),
                xx = (-2 * a * c + sqrt(res)) / (2 * (a * a + b * b)), yy = -(a * xx + c) / b, tt = atan2(yy, xx);
            vec.emplace_back(min(t, tt), -1, i), vec.emplace_back(max(t, tt), 1, i);
        }
    }

    sort(vec.begin(), vec.end());
    return vec;
}

inline int check(double R) {
    vector<tuple<double, int, int> > vec = prework(R);
    int tot = 0, sum = 0;

    for (int i = 0; i < vec.size(); ++i) {
        int x = get<2>(vec[i]);

        if (get<1>(vec[i]) == -1) {
            if (!i || get<0>(vec[i]) != get<0>(vec[i - 1]))
                ++tot;

            BIT::update(id[x] = tot, 1);
        } else
            sum += BIT::query(n) - BIT::query(id[x]), BIT::update(id[x], -1);
    }

    return sum >= m;
}

signed main() {
    scanf("%d%lf%lf%d", &n, &X, &Y, &m), X /= 1e3, Y /= 1e3;

    for (int i = 1; i <= n; ++i) {
        double a, b;
        scanf("%lf%lf", &a, &b);
        a /= 1e3, b /= 1e3;
        p[i] = (Line){a, -1, b + a * X - Y};
    }

    double l = 0, r = 2e9;

    while (r - l > eps) {
        double mid = (l + r) / 2;

        if (check(mid))
            r = mid;
        else
            l = mid;
    }

    vector<tuple<double, int, int> > vec = prework(l);
    set<pair<double, int> > st;
    double ans = 0;
    int tot = 0, sum = 0;

    for (int i = 0; i < vec.size(); ++i) {
        int x = get<2>(vec[i]);

        if (get<1>(vec[i]) == -1) {
            if (!i || get<0>(vec[i]) != get<0>(vec[i - 1]))
                ++tot;
            
            st.emplace(id[x], x);
        } else {
            for (auto it = st.upper_bound(make_pair(id[x], n + 1)); it != st.end(); ++it) {
                int y = it->second;
                double px = (p[x].b * p[y].c - p[y].b * p[x].c) / (p[x].a * p[y].b - p[y].a * p[x].b),
                    py = (p[y].a * p[x].c - p[x].a * p[y].c) / (p[x].a * p[y].b - p[y].a * p[x].b);
                ++sum, ans += sqrt(px * px + py * py);
            }

            st.erase(make_pair(id[x], x));
        }
    }

    printf("%.8lf", ans + (m - sum) * l);
    return 0;
}

P4864 Jerry Loves Lines

二维平面上有 \(n\) 条形如 \(y = kx + b\) 的直线,\(m\) 次询问直线 \(x = a\)\(n\) 条直线交点的 \(y\) 坐标的 \(k\) 大值,其中 \(k\) 一开始就给定(即每次询问的 \(k\) 相同)。

\(n \leq 2000\)\(m \leq 5 \times 10^5\)

首先可以发现两条直线在交点的左侧与右侧排名会交换,并且在交点处两条直线排名相同,因此可能的排名只有 \(O(n^2)\) 个。

考虑将询问离线,这样扫一遍 \(x\) 轴,动态维护排名即可,时间复杂度 \(O((n^2 + m) \log (n^2 + m))\)

#include <bits/stdc++.h>
typedef long double ld;
typedef long long ll;
using namespace std;
const int N = 2e3 + 7, M = 5e5 + 7;

struct Line {
    ld k, b;
} p[N];

struct Node {
    ld x;
    int op, a, b;

    inline bool operator < (const Node &rhs) const {
        return x < rhs.x;
    }
};

ll ans[M];
int id[N], rk[N];

int n, m, k;

signed main() {
    scanf("%d%d%d", &n, &m, &k);

    for (int i = 1; i <= n; ++i)
        scanf("%LF%LF", &p[i].k, &p[i].b);

    vector<Node> vec;

    for (int i = 1; i <= n; ++i)
        for (int j = i + 1; j <= n; ++j)
            if (p[i].k != p[j].k)
                vec.emplace_back((Node){(p[i].b - p[j].b) / (p[j].k - p[i].k), 1, i, j});

    for (int i = 1; i <= m; ++i) {
        ld x;
        scanf("%LF", &x);
        vec.emplace_back((Node){x, 2, i, 0});
    }

    sort(vec.begin(), vec.end());
    iota(id + 1, id + n + 1, 1);

    sort(id + 1, id + n + 1, [](const int &a, const int &b) {
        return p[a].k == p[b].k ? p[a].b < p[b].b : p[a].k > p[b].k;
    });

    for (int i = 1; i <= n; ++i)
        rk[id[i]] = i;

    for (Node it : vec) {
        if (it.op == 1)
            swap(id[rk[it.a]], id[rk[it.b]]), swap(rk[it.a], rk[it.b]);
        else
            ans[it.a] = it.x * p[id[k]].k + p[id[k]].b;
    }

    for (int i = 1; i <= m; ++i)
        printf("%lld\n", ans[i]);

    return 0;
}

P5540 [BalkanOI 2011] timeismoney

给定一张无向图,第 \(i\) 条边有两个权值 \(a_i, b_i\) ,求一棵生成树 \(T\) ,最小化

\[\left( \sum_{e \in T} a_e \right) \times \left( \sum_{e \in T} b_e \right) \]

若有多解则取 \(\sum_{e \in T} a_e\) 最小的。

\(n \leq 200\)\(m \leq 10^4\)\(0 \leq a_i, b_i \leq 255\)

考虑对于所有 \(T\) ,将 \((\sum_{e \in T} a_e, \sum_{e \in T} b_e)\) 放在二维坐标系上,则答案在这些点围成的下凸壳上。

证明:考虑用曲线 \(xy = k\) 来切这些点,则需要最小化 \(k\) 满足有点在该曲线上。

考虑下凸壳上的两个点 \((a, b), (a + c, b + d)\) ,其中 \(c > 0, d < 0\) 。那么对于一个凸包内的点 \((a + ct, b + dt)\) ,其中 \(t \in [0, 1]\) ,其权值为:

\[(a + ct)(b + dt) = cdt^2 + (ad + bc)t + ab \]

由于 \(cd < 0\) ,因此二次函数开口向下,其最小值一定在 \(t = 0\)\(t = 1\) 处取得。

因此凸包内的点一定不优于下凸壳的边界,从而不优于下凸壳上的点。

首先可以用 Prim 求出 \(\sum_{e \in T} a_e\)\(\sum_{e \in T} b_e\) 分别取到最小值的两个点,然后考虑在这两点构成的直线上不断找凸壳上的点,将其分为两段后递归处理。

对于当前递归到的两个点 \(A, B\) ,则需要找到一个点 \(P\) 使得 \(P\)\(AB\) 的距离最大,即 \(S_{\triangle ABP}\) 最大,即 \(\vec{PA} \times \vec{PB}\) 最小。展开得到:

\[\begin{aligned} \vec{PA} \times \vec{PB} &= (x_A - x_P)(y_B - y_P) - (y_A - y_P)(x_B - x_P) \\ &= x_A y_B - x_A y_P - x_P y_B - y_A x_B + y_A x_P + y_P x_B \\ &= x_P(y_A - y_B) + y_P(x_B - x_A) + (x_A y_B - y_A x_B) \end{aligned} \]

因此只要将每条边的边权视为 \(a (y_A - y_B) + b (x_B - x_A)\) ,跑 Prim 即可。

\(V = \max(\sum a, \sum b)\) ,时间复杂度 \(O(V^{\frac{2}{3}} (n^2 + m))\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e2 + 7, M = 1e4 + 7;

struct Graph {
    vector<pair<int, tuple<int, int, int> > > e[N];

    inline void clear(int n) {
        for (int i = 0; i < n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v, int w, int a, int b) {
        e[u].emplace_back(v, make_tuple(w, a, b));
    }
} G;

struct Edge {
    int u, v, a, b;
} e[M];

tuple<int, int, int> dis[N];
bool vis[N];

pair<ll, int> ans = make_pair((ll)1e18, 0);
int n, m;

inline tuple<int, int, int> operator + (tuple<int, int, int> a, tuple<int, int, int> b) {
    return make_tuple(get<0>(a) + get<0>(b), get<1>(a) + get<1>(b), get<2>(a) + get<2>(b));
}

inline pair<int, int> Prim() {
    for (int i = 0; i < n; ++i)
        get<0>(dis[i]) = 1e9;

    memset(vis, false, sizeof(bool) * n);
    dis[0] = make_tuple(0, 0, 0);
    tuple<int, int, int> res = make_tuple(0, 0, 0);

    for (int i = 1; i <= n; ++i) {
        int u = -1;

        for (int j = 0; j < n; ++j)
            if (!vis[j] && (u == -1 || dis[j] < dis[u]))
                u = j;

        res = res + dis[u], vis[u] = true;

        for (auto it : G.e[u])
            dis[it.first] = min(dis[it.first], it.second);
    }
    
    ans = min(ans, make_pair(1ll * get<1>(res) * get<2>(res), get<1>(res)));
    return make_pair(get<1>(res), get<2>(res));
}

void solve(pair<int, int> A, pair<int, int> B) {
    G.clear(n);

    for (int i = 1; i <= m; ++i) {
        G.insert(e[i].u, e[i].v, e[i].a * (A.second - B.second) + e[i].b * (B.first - A.first), e[i].a, e[i].b);
        G.insert(e[i].v, e[i].u, e[i].a * (A.second - B.second) + e[i].b * (B.first - A.first), e[i].a, e[i].b);
    }

    auto C = Prim();

    if (C != A && C != B)
        solve(A, C), solve(C, B);
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i)
        scanf("%d%d%d%d", &e[i].u, &e[i].v, &e[i].a, &e[i].b);

    for (int i = 1; i <= m; ++i)
        G.insert(e[i].u, e[i].v, e[i].a, e[i].a, e[i].b), G.insert(e[i].v, e[i].u, e[i].a, e[i].a, e[i].b);

    auto A = Prim();
    G.clear(n);

    for (int i = 1; i <= m; ++i)
        G.insert(e[i].u, e[i].v, e[i].b, e[i].a, e[i].b), G.insert(e[i].v, e[i].u, e[i].b, e[i].a, e[i].b);

    auto B = Prim();
    solve(A, B);
    printf("%d %d", ans.second, ans.first / ans.second);
    return 0;
}
posted @ 2025-05-25 20:31  wshcl  阅读(39)  评论(0)    收藏  举报