原文链接 https://www.cnblogs.com/cly-none/p/loj2159.html
题意:给出(n)个点,你需要按编号将其划分成不超过(m)段连续的区间,使得所有每个区间的最小圆覆盖的半径的最大值最小。输出这个最小化的最大值和方案(圆心)。
(n,m leq 10^5)
显然要二分答案。然后,一个区间就是越长越好。
首先,考虑最小圆覆盖(O(n))的随机增量法,但为了保证复杂度,我们需要能够random_shuffle
。如果直接按照编号顺序添加点,时间复杂度会是(O(n^3 log n))的。
为了能够random_shuffle
,我们不能一个个添加结点,而是在一开始就知道要对哪些点求最小圆覆盖。
一个思路是二分区间长度。因为有(m)段区间,所以这么做是(O(nm log^2 n))的。并不优秀,在数据水的情况下还不如上一种做法。
为何二分的复杂度不优秀?在于它没有利用所有区间长度和是(n)这一性质,也就是二分的上界太大了。
于是我们可以考虑增加区间长度。一个套路是分块。不断增加(sqrt n)的长度,到不行时再改为增加(1)的长度。这样在这个区间长度为(l)的情况下,复杂度是(O(l sqrt l))的。于是就得到了(O(n sqrt n log n))的做法。
然而实际上倍增就好了。分为两步,首先,我们找到答案二进制下最高的一位,然后,向下确定每一位。这样,对于一个长度为(l)的区间,我们要做(log l)最小圆覆盖,每次要处理的点的个数都是不超过(2l)的。因此,这个区间的复杂度就是(O(l log l)),总复杂度为(O(n log^2 n)),可以通过本题。
#include <bits/stdc++.h>
using namespace std;
#define gc() getchar()
template <typename tp>
inline void read(tp& x) {
x = 0; char tmp; bool key = 0;
for (tmp = gc() ; !isdigit(tmp) ; tmp = gc())
key = (tmp == '-');
for ( ; isdigit(tmp) ; tmp = gc())
x = (x << 3) + (x << 1) + (tmp ^ '0');
if (key) x = -x;
}
typedef double db;
const db eps = 1e-8;
inline int jud(db x) {
return x > -eps ? x > eps ? 1 : 0 : -1;
}
struct point {
db x,y;
point(db x_=0, db y_=0): x(x_), y(y_) {}
point operator + (const point& a) const {
return point(x + a.x, y + a.y);
}
point operator - (const point& a) const {
return point(x - a.x, y - a.y);
}
db abs() const {
return sqrt(x * x + y * y);
}
db norm() const {
return x * x + y * y;
}
point perp() const {
return point(- y, x);
}
point operator * (const db& a) const {
return point(x * a, y * a);
}
};
db cross(point a,point b) {
return a.x * b.y - a.y * b.x;
}
db dot(point a,point b) {
return a.x * b.x + a.y * b.y;
}
point unit(point a) {
db d = a.abs();
a.x /= d;
a.y /= d;
return a;
}
struct line {
point u,v;
line(point u_=point(), point v_=point()): u(u_) {
v = unit(v_);
}
};
db dis(point a,line b) {
return cross(a - b.u, b.v);
}
point inse(line a,line b) {
assert(jud(cross(a.v, b.v)) != 0);
db d = dis(a.u, b) / cross(a.v, b.v);
return a.u - (a.v * d);
}
struct circle {
point o;
db r;
circle(point o_=point(), db r_ = 0): o(o_), r(r_) {}
};
circle circum(point a,point b,point c) {
line l1 = line((a + b) * (0.5), (a - b).perp());
line l2 = line((b + c) * (0.5), (b - c).perp());
point d = inse(l1,l2);
return circle(d, (a - d).abs());
}
const int N = 100010;
int n,m,cnt,num;
point po[N], ans[N], tmp[N];
bool check(int l,int r,db x) {
for (int i = l ; i <= r ; ++ i)
tmp[i] = po[i];
random_shuffle(tmp+l,tmp+r+1);
circle cir = circle(tmp[l], 0);
for (int i = l+1 ; i <= r ; ++ i) {
if (jud(cir.r - (tmp[i] - cir.o).abs()) >= 0);
else {
cir = circle(tmp[i], 0);
for (int j = l ; j < i ; ++ j) {
if (jud(cir.r - (tmp[j] - cir.o).abs()) >= 0);
else {
cir = circle((tmp[i] + tmp[j]) * (0.5), (tmp[i] - tmp[j]).abs() * 0.5);
for (int k = l ; k < j ; ++ k) {
if (jud(cir.r - (tmp[k] - cir.o).abs()) >= 0);
else cir = circum(tmp[i], tmp[j], tmp[k]);
}
}
}
}
}
ans[cnt] = cir.o;
return jud(x - cir.r) >= 0;
}
bool doit(db x) {
cnt = 1;
for (int lp = 1, len ; lp <= n ; lp += len, ++ cnt) {
for (len = 1 ; lp + (len<<1) - 1 <= n && check(lp, lp + (len << 1) - 1, x) ; len <<= 1);
for (int i = (len >> 1) ; i >= 1 ; i >>= 1)
if (lp + len + i - 1 <= n && check(lp, lp + len + i - 1, x)) len += i;
check(lp, lp + len - 1, x);
}
-- cnt;
return cnt <= m;
}
int main() {
read(n), read(m);
for (int i = 1, x, y ; i <= n ; ++ i) {
read(x), read(y);
po[i] = point(x, y);
}
db l = 0, r = 2000000, mid;
while (r - l > 1e-8) {
mid = (l + r) / 2.0;
if (doit(mid)) r = mid;
else l = mid;
}
printf("%.7lf
",r);
doit(r);
printf("%d
",cnt);
for (int i = 1 ; i <= cnt ; ++ i)
printf("%.7lf %.7lf
", ans[i].x, ans[i].y);
return 0;
}
小结:本题反应了倍增的特性。也就是每次需要判断的大小和答案大小是同一级别的。因此,在(O(ANS))判断一个答案的合法性时,可以用倍增替代二分来保证复杂度。