洛谷题单指南-分治与倍增-P3517 [POI2011] WYK-Plot

原题链接:https://www.luogu.com.cn/problem/P3517

题意解读:有n个连续的点p1,p2,...,pn,将这n个点分成不超过m堆,每堆点连续,每一堆都缩成一个点qi,要使得原来的点p1~ps距离qi的最大值最小(最相似),求这个相似度,并计算一共分成几堆,以及每堆缩到的点qi的坐标。

解题思路:

要使得若干点缩到一个点,并且原来的点与新点距离的最大值最小,其实就是求以新点为圆心,能覆盖所有点的圆的最小半径。

1、最小圆覆盖

作为前置知识,先来了解一下最小圆覆盖问题:https://www.luogu.com.cn/problem/P1742

最小覆盖圆用到两个重要的性质:

性质一:最小覆盖圆唯一

性质二:若点 P不在点集 S的最小覆盖圆内部,则点 P必在PS的最小覆盖圆边上

由于过三点可以确定一个圆,可以枚举每三个点来确定圆是否包含其他点

但是直接枚举三个点会超时,为了规避掉最坏的情况,需要借助随机增量算法!

随机增量算法:

先将点的分布随机打散,可以借助random_shuffle函数,然后

取第一个点独立构成圆,圆心是第一个点,半径是0

for i:2->n 

  判断点i是否在当前圆内

  如果不在圆内,以i点作为圆

  for j:1->i-1 

    判断点j是否在当前圆内

      如果不在圆内,以i,j两点构成圆

        for k:1->j-1

          判断点k是否在圆内

            如果不在圆内,以i,j,k三点构成圆

两点构成圆:

点p1、p2构成圆的圆心是两点连线的中间点,半径是两点距离的一半

圆心坐标x = (p1.x + p2.x) / 2, y = (p1.y + p2.y) / 2

半径 = p1到圆心的距离

三点构成圆:

设三个点为(x1,y1)、(x2,y2)、(x3,y3),圆心为(x,y),半径为r,可得到方程组:

(x1-x)2 + (y1-y)2 = r2

(x2-x)2 + (y2-y)2 = r2

(x3-x)2 + (y3-y)2 = r2

展开后:

x12 - 2x1x + x2 + y12 - 2y1y + y2 = r2          (1)

x22 - 2x2x + x2 + y22 - 2y2y + y2 = r2        (2)

x32 - 2x3x + x2 + y32 - 2y3y + y2 = r2        (3)

用(1)-(2),(2)-(3),再移向后得到

2(x1-x2)x + 2(y1-y2)y = x12 - x22 + y12 - y22

2(x2-x3)x + 2(y2-y3)y = x22 - x32 + y22 - y32

a1 = (x1-x2),b1 = (y1-y2),c1 = x12 - x22 + y12 - y22

a2 = (x2-x3),b2 = (y2-y3),c2 = x22 - x32 + y22 - y32

解方程得到

x = (c1b2-c2b1)/(2a1b2-2a2b1)

y = (c1a2-c2a1)/(2a2b1-2a1b2)

半径是任意一个点到圆心的距离

100分代码(P1742):

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

const int N = 100005;
const double eps = 1e-9;

struct Point
{
    double x, y;
} a[N];

int n;

//计算两点之间距离
double distance(Point p1, Point p2)
{
    return sqrt((p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y));
}

//两个点形成圆的圆心
Point circle(Point p1, Point p2)
{
    return {(p1.x+p2.x) / 2, (p1.y+p2.y) / 2};
}

//三个点形成圆的圆心
Point circle(Point p1, Point p2, Point p3)
{
    double x1 = p1.x, y1 = p1.y;
    double x2 = p2.x, y2 = p2.y;
    double x3 = p3.x, y3 = p3.y;
    double a1 = x1 - x2, b1 = y1 - y2, c1 = x1*x1 - x2*x2 + y1*y1 - y2*y2;
    double a2 = x2 - x3, b2 = y2 - y3, c2 = x2*x2 - x3*x3 + y2*y2 - y3*y3;
    double x = (c1*b2 - c2*b1) / (2*a1*b2 - 2*a2*b1);
    double y = (c1*a2 - c2*a1) / (2*a2*b1 - 2*a1*b2);
    return {x, y};
}

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
    random_shuffle(a + 1, a + n + 1);

    Point o = a[1]; //圆心
    double r = 0; //半径
    for(int i = 2; i <= n; i++)
    {
        if(distance(a[i], o) - r < eps) continue; //i点在圆内
        o = a[i]; //更新圆为点i
        for(int j = 1; j < i; j++)
        {
            if(distance(a[j], o) - r < eps) continue; //j点在圆内
            o = circle(a[i], a[j]); //更新圆为i,j两点确定的圆
            r = distance(a[i], o);
            for(int k = 1; k < j; k++)
            {
                if(distance(a[k], o) - r < eps) continue; //k点在圆内
                o = circle(a[i], a[j], a[k]); //更新圆为i,j,k三点确定的圆
                r = distance(a[i], o);
            }
        }
    }
    cout << fixed << setprecision(10) << r << endl << o.x << " " << o.y;
}

2、二分答案求结果

回到本题,要将n个点分成不超过m堆,使得每堆的最小圆覆盖半径最小,可以借助二分。

二分答案mid,找尽可能多的点最小圆覆盖半径不超过mid,这些点即可分为一堆

如何找尽可能多的点分到一堆,可以一个一个枚举,直到最小圆覆盖半径超过mid,则分为一堆,最后统计堆数,不超过m则返回true,否则false

这样枚举+求最小圆覆盖总体复杂度就在O(n^2),再算上二分,复杂度一共是O(logn*n*n)

因此,还要继续优化!

3、倍增+二分找一堆的右端点

在找尽可能多的点分到一堆时,给定左端点l,可以通过倍增,先找到右端点r,使得最小圆覆盖半径超过mid

然后在l~r之间再二分准确的右端点R,这样l~R分为一堆

再更新l=R+1,继续找下一堆,最后记录堆数,看是否超过m

这样先倍增找一堆的范围,再在范围内二分找右端点,再计算最小圆覆盖半径,算法二分答案,总体复杂度在O(logn*logn*n)

100分代码:

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

const int N = 1000005;
const double eps = 1e-9;

struct Point
{
    double x, y;
} a[N], b[N], anso[N];
int cnt;
int n, m;

Point o; //圆心
double radius; //半径

//计算两点之间距离
double distance(Point p1, Point p2)
{
    return sqrt((p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y));
}

//两个点形成圆的圆心
Point circle(Point p1, Point p2)
{
    return {(p1.x+p2.x) / 2, (p1.y+p2.y) / 2};
}

//三个点形成圆的圆心
Point circle(Point p1, Point p2, Point p3)
{
    double x1 = p1.x, y1 = p1.y;
    double x2 = p2.x, y2 = p2.y;
    double x3 = p3.x, y3 = p3.y;
    double a1 = x1 - x2, b1 = y1 - y2, c1 = x1*x1 - x2*x2 + y1*y1 - y2*y2;
    double a2 = x2 - x3, b2 = y2 - y3, c2 = x2*x2 - x3*x3 + y2*y2 - y3*y3;
    double x = (c1*b2 - c2*b1) / (2*a1*b2 - 2*a2*b1);
    double y = (c1*a2 - c2*a1) / (2*a2*b1 - 2*a1*b2);
    return {x, y};
}

//找l~r之间点的最小覆盖圆圆心和半径
void find(int l, int r)
{
    int idx = 0;
    for(int i = l; i <= r; i++) b[++idx] = a[i];
    random_shuffle(b + 1, b + idx + 1);
    o = b[1]; //圆心
    radius = 0; //半径
    for(int i = 2; i <= idx; i++)
    {
        if(distance(b[i], o) - radius < eps) continue; //i点在圆内
        o = b[i]; //更新圆为点i
        for(int j = 1; j < i; j++)
        {
            if(distance(b[j], o) - radius < eps) continue; //j点在圆内
            o = circle(b[i], b[j]); //更新圆为i,j两点确定的圆
            radius = distance(b[i], o);
            for(int k = 1; k < j; k++)
            {
                if(distance(b[k], o) - radius < eps) continue; //k点在圆内
                o = circle(b[i], b[j], b[k]); //更新圆为i,j,k三点确定的圆
                radius = distance(b[i], o);
            }
        }
    }
}

bool check(double maxx)
{
    cnt = 0;
    for(int i = 1; i <= n; i++) //枚举左边界
    {
        int j;
        for(j = 1; i + (1 << j) - 1 <= n; j++) //倍增找右边界
        {
            find(i, i + (1 << j) - 1);
            if(radius - maxx > eps) break; //一段点的最小圆覆盖半径超过限定值
        }
        int l = i + (1 << j - 1) - 1, r = min(n, i + (1 << j) - 1), ans = i;
        //二分找到准确的右端点,使得找到的一段最小圆覆盖半径都在maxx以内
        while(l <= r)
        {
            int mid = (l + r) >> 1;
            find(i, mid);
            if(radius - maxx < eps) ans = mid, l = mid + 1;
            else r = mid - 1;
        }
        find(i, ans); //更新圆心、半径
        i = ans;
        cnt++;
        anso[cnt] = o;
        if(cnt > m) return false;
    }
    return true;
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) scanf("%lf%lf", &a[i].x, &a[i].y);
    find(1, n); //在所有点中计算最小圆覆盖的圆心、半径,得到的半径就是最大半径

    double l = 0, r = radius;
    //二分找答案
    while(r - l > eps)
    {
        double mid = (l + r) / 2; 
        if(check(mid)) r = mid;
        else l = mid;
    }
    check(r); //更新一次正确答案对应的圆心、半径等
    
    printf("%.8lf\n", r);
    printf("%d\n", cnt);
    for(int i = 1; i <= cnt; i++) printf("%.8lf %.8lf\n", anso[i].x, anso[i].y);
    
    return 0;
}

 

posted @ 2024-09-26 13:24  五月江城  阅读(23)  评论(0编辑  收藏  举报