web-dev-qa-db-fra.com

Comment connaître la médiane géométrique

La question est:

Étant donné N points (en 2D) avec les coordonnées x et y, trouvez un point P (dans N points donnés) tel que la somme des distances des autres (N-1) points à P soit minimale.

Ce point est communément appelé médiane géométrique . Existe-t-il un algorithme efficace pour résoudre ce problème, autre que celui naïf O(N^2)?

28
SexyBeast

J'ai résolu quelque chose de similaire pour un juge en ligne local une fois en utilisant recuit simulé . C'était aussi la solution officielle et le programme a obtenu l'AC.

La seule différence était que le point que je devais trouver ne devait pas faire partie des N points donnés.

C'était mon code C++ et N pouvait être aussi grand que 50000. Le programme s'exécute dans 0.1s sur un pentium 2 GHz 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;
}

Ensuite, je pense qu'il est correct de choisir celui de votre liste qui est le plus proche du (x, y) retourné par cet algorithme.

Cet algorithme tire parti de ce que dit ce paragraphe wikipedia sur la médiane géométrique:

Cependant, il est simple de calculer une approximation de la médiane géométrique en utilisant une procédure itérative dans laquelle chaque étape produit une approximation plus précise. Les procédures de ce type peuvent être dérivées du fait que la somme des distances aux points d'échantillonnage est une fonction convexe, car la distance à chaque point d'échantillonnage est convexe et la somme des fonctions convexes reste convexe. Par conséquent, les procédures qui diminuent la somme des distances à chaque étape ne peuvent pas être piégées dans un optimum local.

Une approche courante de ce type, appelée algorithme de Weiszfeld d'après les travaux d'Endre Weiszfeld, [4] est une forme de moindres carrés itérativement repondérés. Cet algorithme définit un ensemble de poids qui sont inversement proportionnels aux distances entre l'estimation actuelle et les échantillons, et crée une nouvelle estimation qui est la moyenne pondérée des échantillons en fonction de ces poids. C'est,

Le premier paragraphe ci-dessus explique pourquoi cela fonctionne: parce que la fonction que nous essayons d'optimiser n'a pas de minimum local, vous pouvez donc trouver goulûment le minimum en l'améliorant itérativement.

Considérez cela comme une sorte de recherche binaire. D'abord, vous approximez le résultat. Une bonne approximation sera le centre de gravité, que mon code calcule lors de la lecture de l'entrée. Ensuite, vous voyez si des points adjacents à cela vous donnent une meilleure solution. Dans ce cas, un point est considéré comme adjacent s'il se trouve à une distance de step de votre point actuel. Si c'est mieux, alors il est bon de rejeter votre point actuel, car, comme je l'ai dit, cela ne vous emprisonnera pas dans un minimum local en raison de la nature de la fonction que vous essayez de minimiser.

Après cela, vous divisez par deux la taille du pas, tout comme dans la recherche binaire, et continuez jusqu'à ce que vous ayez ce que vous considérez comme une approximation suffisamment bonne (contrôlée par la constante eps).

La complexité de l'algorithme dépend donc de la précision avec laquelle vous souhaitez que le résultat soit.

21
IVlad

Il semble que le problème soit difficile à résoudre en mieux que O(n^2) temps lors de l'utilisation de distances euclidiennes. Cependant, le point qui minimise la somme des distances de Manhattan à d'autres points ou le point qui minimise la somme des carrés de Les distances euclidiennes à d'autres points peuvent être trouvées dans O(n log n) temps. (En supposant que multiplier deux nombres est O(1)). Permettez-moi de copier/coller sans vergogne ma solution pour les distances de Manhattan à partir d'un récent post :

Créez un tableau trié de coordonnées x et pour chaque élément du tableau, calculez le coût "horizontal" du choix de cette coordonnée. Le coût horizontal d'un élément est la somme des distances à tous les points projetés sur l'axe X. Cela peut être calculé en temps linéaire en scannant le réseau deux fois (une fois de gauche à droite et une fois dans le sens inverse). De même, créez un tableau trié de coordonnées y et pour chaque élément du tableau, calculez le coût "vertical" du choix de cette coordonnée.

Maintenant, pour chaque point du tableau d'origine, nous pouvons calculer le coût total de tous les autres points en O(1) temps en ajoutant les coûts horizontaux et verticaux. Nous pouvons donc calculer le point optimal dans O (n). La durée totale de fonctionnement est donc O (n log n).

Nous pouvons suivre une approche similaire pour calculer le point qui minimise la somme des carrés des distances euclidiennes à d'autres points. Soit les coordonnées x triées: x1, X2, X3, ..., Xn. Nous parcourons cette liste de gauche à droite et pour chaque point xje nous calculons:

lje = somme des distances à tous les éléments à gauche de xje = (xje-X1) + (xje-X2) + .... + (xje-Xi-1) , et

slje = somme des carrés des distances à tous les éléments à gauche de xje = (xje-X1) ^ 2 + (xje-X2) ^ 2 + .... + (xje-Xi-1) ^ 2

Notez que compte tenu de lje et slje on peut calculer li + 1 et sli + 1 en O(1) temps comme suit:

Soit d = xi + 1-Xje. Ensuite:

li + 1 = lje + i d et sli + 1 = slje + i d ^ 2 + 2 * i * d

Ainsi, nous pouvons calculer tous les lje et slje en temps linéaire en balayant de gauche à droite. De même, pour chaque élément, nous pouvons calculer le rje: somme des distances à tous les éléments à droite et srje: somme des carrés de distances à tous les éléments à droite en temps linéaire. Ajout de srje et slje pour chaque i, donne la somme des carrés des distances horizontales à tous les éléments, en temps linéaire. De même, calculez la somme des carrés des distances verticales à tous les éléments.

Ensuite, nous pouvons parcourir le tableau de points d'origine et trouver le point qui minimise la somme des carrés des distances verticales et horizontales comme précédemment.

10
krjampani

Comme mentionné précédemment, le type d'algorithme à utiliser dépend de la façon dont vous mesurez la distance. Puisque votre question ne spécifie pas cette mesure, voici les implémentations C pour la distance Manhattan et la distance euclidienne au carré . Utilisez dim = 2 Pour les points 2D. Complexité O(n log n).

distance de Manhattan

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);
}

Brève explication: Nous pouvons additionner la distance par dimension, 2 dans votre cas. Supposons que nous ayons N points et que les valeurs dans une dimension soient v_0, .., v_(N-1) et T = v_0 + .. + v_(N-1). Ensuite, pour chaque valeur v_i, Nous avons S_i = v_0 .. v_(i-1). Nous pouvons maintenant exprimer la distance de Manhattan pour cette valeur en additionnant celles du côté gauche: i * v_i - S_i Et du côté droit: T - S_i - (N - i) * v_i, ce qui donne (2 * i - N) * v_i - 2 * S_i + T. L'ajout de T à tous les éléments ne change pas l'ordre, donc nous laissons cela de côté. Et S_i Peut être calculé à la volée.

Voici le reste du code qui en fait un véritable programme 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;
}

distance euclidienne au carré

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);
}

Explication plus courte: à peu près la même approche que la précédente, mais avec une dérivation légèrement plus compliquée. Dites TT = v_0^2 + .. + v_(N-1)^2 nous obtenons TT + N * v_i^2 - 2 * v_i^2 * T. Encore une fois TT est ajouté à tous afin qu'il puisse être laissé de côté. Plus d'explications sur demande.

5
leo

Étape 1: Trier la collection de points par dimension x (nlogn)
Étape 2: Calculez la distance x entre chaque point et tous les points À GAUCHE de celui-ci:

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

Étape 3: Calculez la distance x entre chaque point et tous les points À DROITE dont:

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

Étape 4: Additionnez les deux pour obtenir la distance x totale de chaque point aux autres N-1 points

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

Répétez les étapes 1, 2, 3, 4 avec la dimension y pour obtenir p[i].yDist

Le point avec la plus petite somme de xDist et yDist est la réponse

Complexité totale O (nlogn)

Réponse en C++

Explication supplémentaire:
L'idée est de réutiliser la distance totale déjà calculée du point précédent.
Disons que nous avons trié ABCD en 3 points, nous voyons que la distance totale gauche de D par rapport aux autres avant:

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

Dans laquelle (AC + BC) Est la distance totale gauche de C aux autres avant elle, nous en avons profité et n'avons besoin que de calculer ldist(C) + 3CD

2
rocketspacer

J'ai implémenté la méthode Weiszfeld (je sais que ce n'est pas ce que vous cherchez, mais cela peut aider à approximer votre point), la complexité est O (N * M/k) où N est le nombre de points, M la dimension du points (dans votre cas est 2) et k est l'erreur souhaitée:

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

2
josue.0

Vous pouvez résoudre le problème comme une programmation convexe (la fonction objectif n'est pas toujours convexe). Le programme convexe peut être résolu en utilisant un itératif tel que L-BFGS. Le coût de chaque itération est O(N) et généralement le nombre d'itérations requises n'est pas important. Un point important pour réduire le nombre d'itérations requises est que nous savons que la réponse optimale est l'une des le point dans l'entrée. Ainsi, l'optimisation peut être arrêtée lorsque sa réponse se rapproche de l'un des points d'entrée.

0
iampat