如何找出几何中位数

时间:2012-10-17 12:20:43

标签: algorithm computational-geometry

问题是:

  

给定具有x和y坐标的N个点(在2D中),找到点P(在N中   给定点)使得距其他(N-1)点的距离之和   P是最小的。

这一点通常称为Geometric Median。有没有有效的算法来解决这个问题,除了天真的O(N^2)一个?

7 个答案:

答案 0 :(得分:21)

我使用simulated annealing为当地在线评委解决了类似问题。这也是官方解决方案,该计划得到了AC。

唯一的区别是我必须找到的点不必是N给定点的一部分。

这是我的C ++代码,N可能与50000一样大。该程序在0.1s上以2ghz奔腾4执行。

// header files for IO functions and math
#include <cstdio>
#include <cmath>

// the maximul value n can take
const int maxn = 50001;

// given a point (x, y) on a grid, we can find its left/right/up/down neighbors
// by using these constants: (x + dx[0], y + dy[0]) = upper neighbor etc.
const int dx[] = {-1, 0, 1, 0};
const int dy[] = {0, 1, 0, -1};

// controls the precision - this should give you an answer accurate to 3 decimals
const double eps = 0.001;

// input and output files
FILE *in = fopen("adapost2.in","r"), *out = fopen("adapost2.out","w");

// stores a point in 2d space
struct punct
{
    double x, y;
};

// how many points are in the input file
int n;

// stores the points in the input file
punct a[maxn];

// stores the answer to the question
double x, y;

// finds the sum of (euclidean) distances from each input point to (x, y)
double dist(double x, double y)
{
    double ret = 0;

    for ( int i = 1; i <= n; ++i )
    {
        double dx = a[i].x - x;
        double dy = a[i].y - y;

        ret += sqrt(dx*dx + dy*dy); // classical distance formula
    }

    return ret;
}

// reads the input
void read()
{
    fscanf(in, "%d", &n); // read n from the first 

    // read n points next, one on each line
    for ( int i = 1; i <= n; ++i )
        fscanf(in, "%lf %lf", &a[i].x, &a[i].y), // reads a point
        x += a[i].x,
        y += a[i].y; // we add the x and y at first, because we will start by approximating the answer as the center of gravity

    // divide by the number of points (n) to get the center of gravity
    x /= n; 
    y /= n;
}

// implements the solving algorithm
void go()
{
    // start by finding the sum of distances to the center of gravity
    double d = dist(x, y);

    // our step value, chosen by experimentation
    double step = 100.0;

    // done is used to keep track of updates: if none of the neighbors of the current
    // point that are *step* steps away improve the solution, then *step* is too big
    // and we need to look closer to the current point, so we must half *step*.
    int done = 0;

    // while we still need a more precise answer
    while ( step > eps )
    {
        done = 0;
        for ( int i = 0; i < 4; ++i )
        {
            // check the neighbors in all 4 directions.
            double nx = (double)x + step*dx[i];
            double ny = (double)y + step*dy[i];

            // find the sum of distances to each neighbor
            double t = dist(nx, ny);

            // if a neighbor offers a better sum of distances
            if ( t < d )
            {
                update the current minimum
                d = t;
                x = nx;
                y = ny;

                // an improvement has been made, so
                // don't half step in the next iteration, because we might need
                // to jump the same amount again
                done = 1;
                break;
            }
        }

        // half the step size, because no update has been made, so we might have
        // jumped too much, and now we need to head back some.
        if ( !done )
            step /= 2;
    }
}

int main()
{
    read();
    go();

    // print the answer with 4 decimal points
    fprintf(out, "%.4lf %.4lf\n", x, y);

    return 0;
}

然后我认为从列表中选择最接近此算法返回的(x, y)的那个是正确的。

此算法利用了几何中位数上的维基百科段落所说的内容:

  

然而,计算近似值是直截了当的   几何中位数使用迭代过程,其中每个步骤   产生更准确的近似值。这种程序可以   源自样本点的距离之和的事实   是一个凸函数,因为到每个采样点的距离是   凸和凸函数之和保持凸。因此,   减少每一步距离总和的程序无法获得   被困在局部最佳状态。

     

这种类型的一种常见方法,称为   Werezfeld的算法在Endre Weiszfeld的工作之后,[4]是一种形式   迭代重新加权最小二乘法。该算法定义了一个集合   重量与距离的距离成反比   当前对样本的估计,并创建一个新的估计   根据这些权重的样本的加权平均值。那   是,

上面的第一段解释了为什么这样做:因为我们试图优化的函数没有任何局部最小值,所以你可以通过迭代改进它来贪婪地找到最小值。

将此视为一种二元搜索。首先,您估算结果。一个很好的近似将是重心,我的代码在读取输入时计算。然后,您会看到相邻的点是否为您提供了更好的解决方案。在这种情况下,如果点距离当前点step的距离,则认为该点是相邻的。如果它更好,那么抛弃你当前的点就可以了,因为正如我所说,由于你试图最小化的函数的性质,这不会让你陷入局部最小值。

在此之后,您将步长的一半,就像在二分搜索中一样,并继续直到您认为具有足够好的近似值(由eps常数控制)。

因此,算法的复杂性取决于您希望结果的准确程度。

答案 1 :(得分:10)

在使用欧几里德距离时,问题似乎很难在O(n^2)时间内解决。然而,最小化的点 曼哈顿距离的总和与其他点或最小化欧几里德距离的平方和到其他点的点 可以在O(n log n)时间找到。 (假设乘以两个数字是O(1))。让我无耻地从最近的post

复制/粘贴我的曼哈顿距离解决方案
  

为x中的每个元素创建一个x坐标的排序数组   数组计算选择该坐标的“水平”成本。该   元素的水平成本是到所有的距离的总和   投影到X轴上的点。这可以在线性时间内计算   通过扫描阵列两次(一次从左到右,一次在   反方向)。类似地,创建一个y坐标的排序数组   并且对于数组中的每个元素计算“垂直”成本   选择那个坐标。

     

现在,对于原始数组中的每个点,我们可以计算总数   O(1)时间内所有其他点的成本加上水平和   垂直成本。因此我们可以计算O(n)中的最佳点。就这样   总运行时间为O(n log n)。

我们可以采用类似的方法来计算最小化欧几里德到其他点的距离平方和的点。让 排序的x坐标为:x 1 ,x 2 ,x 3 ,...,x n 。我们从左到右扫描这个列表,对于我们计算的每个点x i

l i = x i =左边所有元素的距离之和=(x i -x 1 )+(x i -x 2 )+ .... +(x i -x i- 1 )和

sl i =到x i =(x i -x 1 )^ 2 +(x i -x 2 )^ 2 + .... +(x i - X <子> I-1 )^ 2

注意,给定l i 和sl i 我们可以计算l i + 1 和sl i + 1 O(1)时间如下:

设d = x i + 1 -x i 。然后:

l i + 1 = l i + i d和sl i + 1 = sl i + i d ^ 2 + 2 * i * d

因此,我们可以通过从左到右扫描来计算线性时间内的所有l i 和sl i 。同样,对于我们可以计算的每个元素 r i :到右边所有元素的距离和sr i 的距离之和:线性到右边所有元素的距离的平方和 时间。为每个i添加sr i 和sl i ,给出线性时间内所有元素的水平距离的平方和。同样的, 计算所有元素的垂直距离的平方和。

然后我们可以扫描原始点阵列并找到最小化垂直和水平距离的平方和的点。

答案 2 :(得分:5)

如前所述,要使用的算法类型取决于您测量距离的方式。由于您的问题没有指定此度量,因此这里是 Manhattan distance Squared Euclidean distance 的C实现。将dim = 2用于2D点。复杂性O(n log n)

曼哈顿距离

double * geometric_median_with_manhattan(double **points, int N, int dim) {
    for (d = 0; d < dim; d++) {
        qsort(points, N, sizeof(double *), compare);
        double S = 0;
        for (int i = 0; i < N; i++) {
            double v = points[i][d];
            points[i][dim] += (2 * i - N) * v - 2 * S;
            S += v;
        }
    }
    return min(points, N, dim);
}

简短说明:我们可以总结每个维度的距离,在您的情况下为2。假设我们有N个点,并且一个维度中的值为v_0,..,v_(N-1)T = v_0 + .. + v_(N-1)。然后,对于每个值v_i,我们都有S_i = v_0 .. v_(i-1)。现在我们可以通过对左侧的那些进行求和来表示该值的曼哈顿距离:i * v_i - S_i和右侧:T - S_i - (N - i) * v_i,这导致(2 * i - N) * v_i - 2 * S_i + T。向所有元素添加T不会改变顺序,因此我们将其排除在外。 S_i可以动态计算。

以下是使其成为实际C程序的其余代码:

#include <stdio.h>
#include <stdlib.h>

int d = 0;
int compare(const void *a, const void *b) {
    return (*(double **)a)[d] - (*(double **)b)[d];
}

double * min(double **points, int N, int dim) {
    double *min = points[0];
    for (int i = 0; i < N; i++) {
        if (min[dim] > points[i][dim]) {
            min = points[i];
        }
    }
    return min;
}

int main(int argc, const char * argv[])
{
    // example 2D coordinates with an additional 0 value
    double a[][3] = {{1.0, 1.0, 0.0}, {3.0, 1.0, 0.0}, {3.0, 2.0, 0.0}, {0.0, 5.0, 0.0}};
    double *b[] = {a[0], a[1], a[2], a[3]};
    double *min = geometric_median_with_manhattan(b, 4, 2);
    printf("geometric median at {%.1f, %.1f}\n", min[0], min[1]);
    return 0;
}

平方欧几里德距离

double * geometric_median_with_square(double **points, int N, int dim) {
    for (d = 0; d < dim; d++) {
        qsort(points, N, sizeof(double *), compare);
        double T = 0;
        for (int i = 0; i < N; i++) {
            T += points[i][d];
        }
        for (int i = 0; i < N; i++) {
            double v = points[i][d];
            points[i][dim] += v * (N * v - 2 * T);
        }
    }
    return min(points, N, dim);
}

更短的解释:几乎与前一种方法相同,但稍微复杂一点。说TT = v_0^2 + .. + v_(N-1)^2我们得到TT + N * v_i^2 - 2 * v_i^2 * T。再次将TT添加到所有,所以它可以被省略。有关要求的更多解释。

答案 3 :(得分:2)

我实现了Weiszfeld方法(我知道它不是你想要的,但它可能有助于使你的点近似),复杂度是O(N * M / k)其中N是点数,M点的维数(在你的情况下是2),k是所需的错误:

https://github.com/j05u3/weiszfeld-implementation

答案 4 :(得分:2)

第1步:按x维度(nlogn)对点集合进行排序 第2步:计算每个点与左侧的所有点之间的x距离:

xLDist[0] := 0
for i := 1 to n - 1
       xLDist[i] := xLDist[i-1] + ( ( p[i].x - p[i-1].x ) * i)

第3步:计算每个点与所有点之间的x距离直到

xRDist[n - 1] := 0
for i := n - 2 to 0
       xRDist[i] := xRDist[i+1] + ( ( p[i+1].x - p[i].x ) * i)  

第4步:总结你得到从每个点到其他N-1点的总x距离

for i := 0 to n - 1
       p[i].xDist = xLDist[i] + xRDist[i]
  

使用y维重复步骤1,2,3,4以获得p[i].yDist

xDistyDist之和最小的点是答案

总复杂度O(nlogn)

Answer in C++

进一步说明:
我们的想法是重复使用已经计算的前一点的总距离 让我们说我们有3点ABCD排序,我们看到D之前的总剩余距离是:

  

AD + BD + CD =(AC + CD)+(BC + CD)+ CD = AC + BC + 3CD

其中(AC + BC)是C与其他人之前的总剩余距离,我们利用了这一点,只需要计算ldist(C) + 3CD

答案 5 :(得分:0)

您可以将问题解决为凸面编程(目标函数并不总是凸起的)。可以使用诸如L-BFGS的迭代来求解凸程序。每次迭代的成本是O(N),并且通常所需的迭代次数不大。减少所需迭代次数的一个重点是我们知道最佳答案是输入中的一个点。因此,当其答案接近其中一个输入点时,可以停止优化。

答案 6 :(得分:-2)

我们需要找到的答案是几何中位数

C++ 代码

    #include <bits/stdc++.h>
    using namespace std;
    int main()
    {
        int n;
        cin >> n;

        int a[n],b[n];
        f(int i=0;i<n;i++) cin >> a[i] >> b[i];
        int res = 0;
        sort(a,a+n);
        sort(b,b+n);

        int m1 = a[n/2];
        int m2 = b[n/2];

        f(int i=0;i<n;i++) res += abs(m1 - a[i]);
        f(int i=0;i<n;i++) res += abs(m2 - b[i]);

        cout << res << '\n';
    }