1 条题解
-
0
F. Grand Finale: Circles 详细题解
问题重述
给定 个圆,第 个圆由圆心 和半径 给出。
求一个圆 (圆心 ,半径 ),使得 被所有给定的圆包含,即对每个 , 且 最大。输出满足条件的 ,允许 的误差。
问题转化
设 为待求圆心, 为半径。条件可改写为 其中 , 为欧氏距离。
定义 ,则问题变为:寻找最大的 ,使得存在点 满足 对所有 成立。
当 增大时,每个 减小,这些圆的交集逐渐缩小。最大可行的 就是这些圆仍然有公共交点的最大值。
换句话说,我们要在所有给定圆内部找一个半径最大的内切圆。
算法思想
这是一个典型的最大空圆问题,但约束不是点而是圆。可以转化为最小外接圆的对偶问题,使用随机增量法(Welzl 算法)求解。
核心观察:
最大内含圆 要么由单个圆决定(即 与某个给定圆内切,且圆心为该圆圆心,半径为该圆半径),要么由两个圆决定( 同时与两个给定圆内切,圆心位于两圆圆心连线的中垂线上,半径由几何关系确定),要么由三个圆决定( 与三个给定圆内切,可通过解方程组得到)。
因此,最优解一定由至多三个给定圆“支撑”,这与最小圆覆盖(包含所有点的最小圆)的性质类似。Welzl 算法(随机增量)可以高效地找到这样的圆。算法如下:
- 随机打乱所有圆的顺序。
- 递归函数
rec(S, R):S是尚未处理的圆的下标范围(或向量),R是当前已经选中的支撑圆集合(大小不超过 3)。- 初始
R为空,S包含所有圆。 - 若
R的大小为 3,则直接计算由这三个圆确定的最大内含圆(切于三者)。 - 若
S为空,则根据R的大小(0/1/2)分别返回对应的圆(无穷大半径、单个圆、切于两个圆的圆)。 - 否则,先递归处理
S去掉最后一个圆,得到当前圆C。 - 如果
C已经包含在S的最后一个圆中(即C完全位于该圆内部),则直接返回C。 - 否则,将该圆加入
R中,重新递归处理S去掉最后一个圆。 - 返回结果。
由于每一步
R的大小最多增加 1,递归深度为 。随机打乱保证了期望复杂度为 。
几何基础函数
代码中定义了若干辅助函数:
midp(a, b):两点中点。addi(a, b),subt(a, b),mult(a, k):向量运算。abs(p),dist(a, b):长度和距离。basis0():返回半径无穷大的圆(代表无约束)。basis1(a):返回圆a本身。basis2(a, b):返回同时内切于圆a和b的最大圆。
设 为圆心, 为半径。作向量 ,分别在 和 的方向上延长 和 得到点 ,则所求圆心为 的中点,半径为 。basis3(a, b, c):返回同时内切于三个圆的最大圆。
通过解方程组得到,代码中的公式是解析解。viol(c, a):判断圆c是否被圆a包含(即是否c.r + dist(c.p, a.p) > a.r)。如果超出,则c不满足被a包含的条件。
随机增量法实现
标程中的
solve(v)函数:- 随机种子固定为
1999999,但为了随机化,后续会打乱顺序。 basis存储当前选中的支撑圆(最多 3 个)。trivial()根据basis的大小返回对应的圆(0 → 无穷大,1 → 那个圆,2 → 两圆内切圆,3 → 三圆内切圆)。rec是递归函数,参数n表示处理前n个圆(下标 0..n-1)。- 递归出口:
n == 0或basis.size() == 3,返回trivial()。 - 为了防止极端情况,加入计数器
counter,当递归次数超过 时返回 NaN,触发重新打乱。 - 先递归处理前
n-1个圆,得到c。 - 取第
n-1个圆p = v[n-1],如果c不被p包含(viol(c, p)为真)且c.r不是 NaN,则说明c需要更新:将p加入basis,递归求解前n-1个圆,然后弹出p。 - 返回结果。
- 递归出口:
- 第一次调用
rec(rec, v.size())得到候选圆c,如果c.r是 NaN(表示陷入死循环),则重新打乱整个序列,重复直到获得有效结果。
最后输出圆心坐标和半径。
复杂度分析
- 每次递归调用可能增加
basis的大小,但最多增加到 3。 - 随机化保证了每次圆被加入
basis的概率很低,期望总递归次数为 。 - 实际实现中加上计数器限制,避免最坏情况。
- 总体复杂度 ,在 时运行很快。
关于精度
算法使用
long double(80 位扩展精度),能够满足 的误差要求。
输出时使用setprecision(16)输出足够多的小数位。
标程代码
#include<bits/stdc++.h> using namespace std; using ll=long long; using lf=long double; using pt=pair<lf,lf>; lf& real(pt& p){return p.first;} lf& imag(pt& p){return p.second;} pt midp(pt a,pt b){return pt{(real(a)+real(b))/2,(imag(a)+imag(b))/2};} pt addi(pt a,pt b){return pt{(real(a)+real(b)),(imag(a)+imag(b))};} pt subt(pt a,pt b){return pt{(real(a)-real(b)),(imag(a)-imag(b))};} pt mult(pt a,lf b){return pt{real(a)*b,imag(a)*b};} lf abs(pt a){return sqrtl(powl(real(a),2)+powl(imag(a),2));} lf dist(pt a,pt b){return sqrtl(powl(real(a)-real(b),2)+powl(imag(a)-imag(b),2));} struct circ{pt p;lf r;}; const lf inf=1e18; circ basis0(){return {pt{0,0},inf};} circ basis1(circ a){return a;} circ basis2(circ a,circ b) { pt aa=a.p,bb=b.p; pt ab=subt(bb,aa),ba=subt(aa,bb); lf aab=abs(ab),aba=abs(ba); real(ab)/=aab; imag(ab)/=aab; real(ba)/=aba; imag(ba)/=aba; pt ar=addi(aa,mult(ab,a.r)),br=addi(bb,mult(ba,b.r)); return {midp(ar,br),dist(ar,br)/2.0L}; } circ basis3(circ a,circ b,circ c) { lf x1=real(a.p),y1=imag(a.p),r1=a.r; lf x2=real(b.p),y2=imag(b.p),r2=b.r; lf x3=real(c.p),y3=imag(c.p),r3=c.r; lf a2=x1-x2,a3=x1-x3,b2=y1-y2,b3=y1-y3,c2=r2-r1,c3=r3-r1; lf d1=x1*x1+y1*y1-r1*r1,d2=d1-x2*x2-y2*y2+r2*r2,d3=d1-x3*x3-y3*y3+r3*r3; lf ab=a3*b2-a2*b3; lf xa=(b2*d3-b3*d2)/(ab*2)-x1; lf xb=(b3*c2-b2*c3)/ab; lf ya=(a3*d2-a2*d3)/(ab*2)-y1; lf yb=(a2*c3-a3*c2)/ab; lf A=xb*xb+yb*yb-1; lf B=2*(r1+xa*xb+ya*yb); lf C=xa*xa+ya*ya-r1*r1; lf r=-(A?(B-sqrtl(B*B-4*A*C))/(2*A):C/B); return {pt{x1+xa+xb*r,y1+ya+yb*r},r}; } bool viol(circ p,circ a) { return a.r<p.r+dist(a.p,p.p); } circ solve(vector<circ>&v) { lf _nan=nan("aaa"); mt19937_64 mt(1999999); //shuffle(begin(v),end(v),mt); vector<circ>basis; auto trivial=[&]() { if(basis.size()==0)return basis0(); if(basis.size()==1)return basis[0]; if(basis.size()==2)return basis2(basis[0],basis[1]); return basis3(basis[0],basis[1],basis[2]); }; int counter=0; auto rec=[&](auto rec,int n)->circ { if(n==0||basis.size()==3)return trivial(); counter++; if(counter>30*(int)v.size())return {pt{0,0},_nan}; auto c=rec(rec,n-1); auto p=v[n-1]; if(!viol(c,p)||isnan(c.r))return c; basis.push_back(p); c=rec(rec,n-1); basis.pop_back(); return c; }; auto c=rec(rec,(int)v.size()); while(isnan(c.r)) { counter=0; shuffle(begin(v),end(v),mt); c=rec(rec,(int)v.size()); } return c; } int main() { cin.tie(0)->sync_with_stdio(0); int n;cin>>n; vector<circ>v(n); for(auto&[p,r]:v) { ll x,y,rr;cin>>x>>y>>rr; p={x,y};r=rr; } auto ans=solve(v); cout<<setprecision(16)<<fixed<<real(ans.p)<<" "<<imag(ans.p)<<" "<<ans.r<<"\n"; }
总结
本题要求在所有给定圆的内部找一个最大内切圆,可以转化为求一个点 使得它到各圆心距离加上自身半径不超过给定半径。随机增量法(Welzl 算法)通过维护至多三个支撑圆,递归求解,具有期望线性时间复杂度,实现简洁,精度满足要求。
代码中
basis2和basis3的几何推导是核心,理解了两圆/三圆内切圆的构造方法,就能理解整个算法。
- 1
信息
- ID
- 7278
- 时间
- 4000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 10
- 已通过
- 1
- 上传者