计算几何杂记
计算几何杂记
基础知识
定义符号函数:
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)\) 有如下关系:
即:
其中 \(\operatorname{atan2}\) 的值域为 \((-\pi, \pi]\) 。
其他
任意多边形面积:将多边形上的点逆时针标记为 \(P_{1 \sim n}\) ,再任选一个辅助点 \(O\) ,记 \(\mathbf{v_i} = P_i - O\) ,则:
二维凸包
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;
}
最小矩形覆盖
首先有结论:最小矩形的一条边一定与凸包重合。
考虑旋转卡壳,枚举一条在矩形上的边,维护其对面的平行线,以及一对与之垂直的夹住凸包的平行线。但是维护平行线比较困难,考虑维护另一对平行线切到的点,判断最优性直接用投影长度判断即可。
同样可以直接双指针维护做到线性。
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}\) 最小。展开得到:
因此只要将每条边的边权视为 \(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;
}

浙公网安备 33010602011771号