Millones de puntos 3D: ¿Cómo encontrar los 10 más cercanos a un punto determinado?


Un punto en 3-d se define por (x,y,z). La distancia d entre dos puntos (X,Y,Z) y (x,y,z) es d= Sqrt[(X-x)^2 + (Y-Y)^2 + (Z-z)^2]. Ahora hay un millón de entradas en un archivo, cada entrada es algún punto en el espacio, en ningún orden específico. Dado cualquier punto (a,b,c) encuentre los 10 puntos más cercanos a él. Cómo almacenaría los millones de puntos y cómo recuperaría esos 10 puntos de esa estructura de datos.

Author: MatrixFrog, 2010-03-21

12 answers

Millones de puntos es un pequeño número. El enfoque más sencillo funciona aquí (el código basado en KDTree es más lento (para consultar solo un punto)).

Aproximación de fuerza bruta (tiempo ~1 segundo)

#!/usr/bin/env python
import numpy

NDIM = 3 # number of dimensions

# read points into array
a = numpy.fromfile('million_3D_points.txt', sep=' ')
a.shape = a.size / NDIM, NDIM

point = numpy.random.uniform(0, 100, NDIM) # choose random point
print 'point:', point
d = ((a-point)**2).sum(axis=1)  # compute distances
ndx = d.argsort() # indirect sort 

# print 10 nearest points to the chosen one
import pprint
pprint.pprint(zip(a[ndx[:10]], d[ndx[:10]]))

Ejecutarlo:

$ time python nearest.py 
point: [ 69.06310224   2.23409409  50.41979143]
[(array([ 69.,   2.,  50.]), 0.23500677815852947),
 (array([ 69.,   2.,  51.]), 0.39542392750839772),
 (array([ 69.,   3.,  50.]), 0.76681859086988302),
 (array([ 69.,   3.,  50.]), 0.76681859086988302),
 (array([ 69.,   3.,  51.]), 0.9272357402197513),
 (array([ 70.,   2.,  50.]), 1.1088022980015722),
 (array([ 70.,   2.,  51.]), 1.2692194473514404),
 (array([ 70.,   2.,  51.]), 1.2692194473514404),
 (array([ 70.,   3.,  51.]), 1.801031260062794),
 (array([ 69.,   1.,  51.]), 1.8636121147970444)]

real    0m1.122s
user    0m1.010s
sys 0m0.120s

Aquí está el script que genera millones de puntos 3D:

#!/usr/bin/env python
import random
for _ in xrange(10**6):
    print ' '.join(str(random.randrange(100)) for _ in range(3))

Salida:

$ head million_3D_points.txt

18 56 26
19 35 74
47 43 71
82 63 28
43 82 0
34 40 16
75 85 69
88 58 3
0 63 90
81 78 98

Podría usar ese código para probar estructuras de datos y algoritmos más complejos (por ejemplo, si realmente consumen menos memoria o más rápido el enfoque más simple anterior). Vale la pena señalar que por el momento es la única respuesta que contiene código de trabajo.

Solución basada en KDTree (tiempo ~1,4 segundos)

#!/usr/bin/env python
import numpy

NDIM = 3 # number of dimensions

# read points into array
a = numpy.fromfile('million_3D_points.txt', sep=' ')
a.shape = a.size / NDIM, NDIM

point =  [ 69.06310224,   2.23409409,  50.41979143] # use the same point as above
print 'point:', point


from scipy.spatial import KDTree

# find 10 nearest points
tree = KDTree(a, leafsize=a.shape[0]+1)
distances, ndx = tree.query([point], k=10)

# print 10 nearest points to the chosen one
print a[ndx]

Ejecutarlo:

$ time python nearest_kdtree.py  

point: [69.063102240000006, 2.2340940900000001, 50.419791429999997]
[[[ 69.   2.  50.]
  [ 69.   2.  51.]
  [ 69.   3.  50.]
  [ 69.   3.  50.]
  [ 69.   3.  51.]
  [ 70.   2.  50.]
  [ 70.   2.  51.]
  [ 70.   2.  51.]
  [ 70.   3.  51.]
  [ 69.   1.  51.]]]

real    0m1.359s
user    0m1.280s
sys 0m0.080s

Ordenación parcial en C++ (tiempo ~1,1 segundos)

// $ g++ nearest.cc && (time ./a.out < million_3D_points.txt )
#include <algorithm>
#include <iostream>
#include <vector>

#include <boost/lambda/lambda.hpp>  // _1
#include <boost/lambda/bind.hpp>    // bind()
#include <boost/tuple/tuple_io.hpp>

namespace {
  typedef double coord_t;
  typedef boost::tuple<coord_t,coord_t,coord_t> point_t;

  coord_t distance_sq(const point_t& a, const point_t& b) { // or boost::geometry::distance
    coord_t x = a.get<0>() - b.get<0>();
    coord_t y = a.get<1>() - b.get<1>();
    coord_t z = a.get<2>() - b.get<2>();
    return x*x + y*y + z*z;
  }
}

int main() {
  using namespace std;
  using namespace boost::lambda; // _1, _2, bind()

  // read array from stdin
  vector<point_t> points;
  cin.exceptions(ios::badbit); // throw exception on bad input
  while(cin) {
    coord_t x,y,z;
    cin >> x >> y >> z;    
    points.push_back(boost::make_tuple(x,y,z));
  }

  // use point value from previous examples
  point_t point(69.06310224, 2.23409409, 50.41979143);
  cout << "point: " << point << endl;  // 1.14s

  // find 10 nearest points using partial_sort() 
  // Complexity: O(N)*log(m) comparisons (O(N)*log(N) worst case for the implementation)
  const size_t m = 10;
  partial_sort(points.begin(), points.begin() + m, points.end(), 
               bind(less<coord_t>(), // compare by distance to the point
                    bind(distance_sq, _1, point), 
                    bind(distance_sq, _2, point)));
  for_each(points.begin(), points.begin() + m, cout << _1 << "\n"); // 1.16s
}

Ejecutarlo:

g++ -O3 nearest.cc && (time ./a.out < million_3D_points.txt )
point: (69.0631 2.23409 50.4198)
(69 2 50)
(69 2 51)
(69 3 50)
(69 3 50)
(69 3 51)
(70 2 50)
(70 2 51)
(70 2 51)
(70 3 51)
(69 1 51)

real    0m1.152s
user    0m1.140s
sys 0m0.010s

Cola de prioridad en C++ (tiempo ~1.2 segundos)

#include <algorithm>           // make_heap
#include <functional>          // binary_function<>
#include <iostream>

#include <boost/range.hpp>     // boost::begin(), boost::end()
#include <boost/tr1/tuple.hpp> // get<>, tuple<>, cout <<

namespace {
  typedef double coord_t;
  typedef std::tr1::tuple<coord_t,coord_t,coord_t> point_t;

  // calculate distance (squared) between points `a` & `b`
  coord_t distance_sq(const point_t& a, const point_t& b) { 
    // boost::geometry::distance() squared
    using std::tr1::get;
    coord_t x = get<0>(a) - get<0>(b);
    coord_t y = get<1>(a) - get<1>(b);
    coord_t z = get<2>(a) - get<2>(b);
    return x*x + y*y + z*z;
  }

  // read from input stream `in` to the point `point_out`
  std::istream& getpoint(std::istream& in, point_t& point_out) {    
    using std::tr1::get;
    return (in >> get<0>(point_out) >> get<1>(point_out) >> get<2>(point_out));
  }

  // Adaptable binary predicate that defines whether the first
  // argument is nearer than the second one to given reference point
  template<class T>
  class less_distance : public std::binary_function<T, T, bool> {
    const T& point;
  public:
    less_distance(const T& reference_point) : point(reference_point) {}

    bool operator () (const T& a, const T& b) const {
      return distance_sq(a, point) < distance_sq(b, point);
    } 
  };
}

int main() {
  using namespace std;

  // use point value from previous examples
  point_t point(69.06310224, 2.23409409, 50.41979143);
  cout << "point: " << point << endl;

  const size_t nneighbours = 10; // number of nearest neighbours to find
  point_t points[nneighbours+1];

  // populate `points`
  for (size_t i = 0; getpoint(cin, points[i]) && i < nneighbours; ++i)
    ;

  less_distance<point_t> less_distance_point(point);
  make_heap  (boost::begin(points), boost::end(points), less_distance_point);

  // Complexity: O(N*log(m))
  while(getpoint(cin, points[nneighbours])) {
    // add points[-1] to the heap; O(log(m))
    push_heap(boost::begin(points), boost::end(points), less_distance_point); 
    // remove (move to last position) the most distant from the
    // `point` point; O(log(m))
    pop_heap (boost::begin(points), boost::end(points), less_distance_point);
  }

  // print results
  push_heap  (boost::begin(points), boost::end(points), less_distance_point);
  //   O(m*log(m))
  sort_heap  (boost::begin(points), boost::end(points), less_distance_point);
  for (size_t i = 0; i < nneighbours; ++i) {
    cout << points[i] << ' ' << distance_sq(points[i], point) << '\n';  
  }
}

Ejecutarlo:

$ g++ -O3 nearest.cc && (time ./a.out < million_3D_points.txt )

point: (69.0631 2.23409 50.4198)
(69 2 50) 0.235007
(69 2 51) 0.395424
(69 3 50) 0.766819
(69 3 50) 0.766819
(69 3 51) 0.927236
(70 2 50) 1.1088
(70 2 51) 1.26922
(70 2 51) 1.26922
(70 3 51) 1.80103
(69 1 51) 1.86361

real    0m1.174s
user    0m1.180s
sys 0m0.000s

Enfoque lineal basado en la búsqueda (tiempo ~1.15 segundos)

// $ g++ -O3 nearest.cc && (time ./a.out < million_3D_points.txt )
#include <algorithm>           // sort
#include <functional>          // binary_function<>
#include <iostream>

#include <boost/foreach.hpp>
#include <boost/range.hpp>     // begin(), end()
#include <boost/tr1/tuple.hpp> // get<>, tuple<>, cout <<

#define foreach BOOST_FOREACH

namespace {
  typedef double coord_t;
  typedef std::tr1::tuple<coord_t,coord_t,coord_t> point_t;

  // calculate distance (squared) between points `a` & `b`
  coord_t distance_sq(const point_t& a, const point_t& b);

  // read from input stream `in` to the point `point_out`
  std::istream& getpoint(std::istream& in, point_t& point_out);    

  // Adaptable binary predicate that defines whether the first
  // argument is nearer than the second one to given reference point
  class less_distance : public std::binary_function<point_t, point_t, bool> {
    const point_t& point;
  public:
    explicit less_distance(const point_t& reference_point) 
        : point(reference_point) {}
    bool operator () (const point_t& a, const point_t& b) const {
      return distance_sq(a, point) < distance_sq(b, point);
    } 
  };
}

int main() {
  using namespace std;

  // use point value from previous examples
  point_t point(69.06310224, 2.23409409, 50.41979143);
  cout << "point: " << point << endl;
  less_distance nearer(point);

  const size_t nneighbours = 10; // number of nearest neighbours to find
  point_t points[nneighbours];

  // populate `points`
  foreach (point_t& p, points)
    if (! getpoint(cin, p))
      break;

  // Complexity: O(N*m)
  point_t current_point;
  while(cin) {
    getpoint(cin, current_point); //NOTE: `cin` fails after the last
                                  //point, so one can't lift it up to
                                  //the while condition

    // move to the last position the most distant from the
    // `point` point; O(m)
    foreach (point_t& p, points)
      if (nearer(current_point, p)) 
        // found point that is nearer to the `point` 

        //NOTE: could use insert (on sorted sequence) & break instead
        //of swap but in that case it might be better to use
        //heap-based algorithm altogether
        std::swap(current_point, p);
  }

  // print results;  O(m*log(m))
  sort(boost::begin(points), boost::end(points), nearer);
  foreach (point_t p, points)
    cout << p << ' ' << distance_sq(p, point) << '\n';  
}

namespace {
  coord_t distance_sq(const point_t& a, const point_t& b) { 
    // boost::geometry::distance() squared
    using std::tr1::get;
    coord_t x = get<0>(a) - get<0>(b);
    coord_t y = get<1>(a) - get<1>(b);
    coord_t z = get<2>(a) - get<2>(b);
    return x*x + y*y + z*z;
  }

  std::istream& getpoint(std::istream& in, point_t& point_out) {    
    using std::tr1::get;
    return (in >> get<0>(point_out) >> get<1>(point_out) >> get<2>(point_out));
  }
}

Mediciones muestra que la mayor parte del tiempo se gasta leyendo matriz del archivo, los cálculos reales toman en orden de magnitud menos tiempo.

 89
Author: jfs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-24 06:38:06

Si el millón de entradas ya están en un archivo, no hay necesidad de cargarlas todas en una estructura de datos en memoria. Simplemente mantenga una matriz con los diez primeros puntos encontrados hasta ahora, y escanee más de los millones de puntos, actualizando su lista de los diez primeros a medida que avanza.

Esto es O(n) en el número de puntos.

 19
Author: Will,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-21 05:53:01

Puede almacenar los puntos en un árbol k-dimensional (árbol kd). Los árboles Kd están optimizados para búsquedas de vecinos más cercanos (encontrando los n puntos más cercanos a un punto dado).

 14
Author: mipadi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-21 06:04:36

Creo que esta es una pregunta difícil que prueba si no intentas exagerar las cosas.

Considere el algoritmo más simple que la gente ya ha dado anteriormente: mantenga una tabla de diez candidatos mejores hasta ahora y revise todos los puntos uno por uno. Si encuentra un punto más cercano que cualquiera de los diez mejores hasta ahora, reemplácelo. ¿Cuál es la complejidad? Bueno, tenemos que mirar cada punto del archivo una vez , calcular su distancia (o cuadrado de la distancia en realidad) y comparar con el 10 el punto más cercano. Si es mejor, insértelo en el lugar apropiado en la tabla 10-best-so-far.

Entonces, ¿cuál es la complejidad? Miramos cada punto una vez, por lo que es n cálculos de la distancia y n comparaciones. Si el punto es mejor, necesitamos insertarlo en la posición correcta, esto requiere algunas comparaciones más, pero es un factor constante ya que la tabla de mejores candidatos es de un tamaño constante 10.

Terminamos con un algoritmo que se ejecuta en tiempo lineal, O (n) en el número de puntos.

Pero ahora considere cuál es el límite inferior en tal algoritmo? Si no hay orden en los datos de entrada, tenemos que mirar cada punto para ver si no es uno de los más cercanos. Por lo que puedo ver, el límite inferior es Omega (n) y por lo tanto el algoritmo anterior es óptimo.

 10
Author: Krystian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-21 11:34:47

No hay necesidad de calcular la distancia. Solo el cuadrado de la distancia debe satisfacer sus necesidades. Creo que debería ser más rápido. En otras palabras, puede omitir el bit sqrt.

 5
Author: Agnel Kurian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-21 08:04:54

Esta no es una pregunta de tarea, ¿verdad? ;-)

Mi pensamiento: iterar sobre todos los puntos y ponerlos en un montón de min o una cola de prioridad limitada, marcada por la distancia desde el objetivo.

 4
Author: David Z,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-21 05:51:33

Esta pregunta esencialmente está probando su conocimiento y/o intuición de algoritmos de partición de espacio. Yo diría que almacenar los datos en un octree es su mejor apuesta. Se usa comúnmente en motores 3d que manejan este tipo de problemas (almacenando millones de vértices, trazando rayos, encontrando colisiones, etc.).). El tiempo de búsqueda será del orden de log(n) en el peor de los casos (creo).

 4
Author: Kai,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-21 06:39:14

Algoritmo sencillo:

Almacene los puntos como una lista de tuplas, y escanee los puntos, calculando la distancia y manteniendo una lista 'más cercana'.

Más creativo:

Agrupe los puntos en regiones (como el cubo descrito por "0,0,0" a "50,50,50", o "0,0,0" a "-20,-20,-20"), para que pueda "indexarlos" desde el punto objetivo. Compruebe en qué cubo se encuentra el objetivo, y solo busque a través de los puntos en ese cubo. Si hay menos de 10 puntos en ese cubo, compruebe los cubos "vecinos", y así sucesivamente.

Pensándolo mejor, este no es un algoritmo muy bueno: si tu punto objetivo está más cerca de la pared de un cubo que 10 puntos, entonces tendrás que buscar en el cubo vecino también.

Iría con el enfoque de árbol kd y encontraría el nodo más cercano, luego eliminaría (o marcaría) ese nodo más cercano y volvería a buscar el nuevo nodo más cercano. Enjuague y repita.

 2
Author: Jeff Meatball Yang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-21 06:30:14

Para dos puntos cualesquiera P1 (x1, y1, z1) y P2 (x2, y2, z2), si la distancia entre los puntos es d entonces todo lo siguiente debe ser verdadero:

|x1 - x2| <= d 
|y1 - y2| <= d
|z1 - z2| <= d

Mantenga los 10 más cercanos a medida que itera sobre todo su conjunto, pero también mantenga la distancia hasta el 10 más cercano. Ahórrate mucha complejidad al usar estas tres condiciones antes de calcular la distancia para cada punto que mires.

 2
Author: Kirk Broadhurst,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-22 23:26:02

Básicamente una combinación de las dos primeras respuestas por encima de mí. dado que los puntos están en un archivo no hay necesidad de mantenerlos en memoria. En lugar de una matriz, o un montón mínimo, usaría un montón máximo, ya que solo desea comprobar las distancias menores que el punto más cercano 10. Para una matriz, tendría que comparar cada distancia recién calculada con las 10 distancias que mantuvo. Para un montón mínimo, debe realizar 3 comparaciones con cada distancia recién calculada. Con un montón máximo, solo realice 1 comparación cuando la distancia recién calculada sea mayor que el nodo raíz.

 1
Author: Yiling,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-21 06:03:07

Esta pregunta necesita una definición más detallada.

1) La decisión con respecto a los algoritmos que pre-indexan los datos varía mucho dependiendo de si puede mantener los datos completos en la memoria o no.

Con kdtrees y octrees no tendrá que mantener los datos en memoria y el rendimiento se beneficia de ese hecho, no solo porque la huella de memoria es menor, sino simplemente porque no tendrá que leer todo el archivo.

Con fuerza bruta tendrás que leer todo el archivo y volver a calcular las distancias para cada nuevo punto que va a buscar.

Sin embargo, esto podría no ser importante para usted.

2) Otro factor es cuántas veces tendrá que buscar un punto.

Como declaró J. F. Sebastian a veces bruteforce es más rápido incluso en grandes conjuntos de datos, pero tenga cuidado de tener en cuenta que sus puntos de referencia miden la lectura de todo el conjunto de datos desde el disco (que no es necesario una vez que kd-tree o octree se construye y escribe en mide solo una búsqueda.

 1
Author: Unreason,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-21 14:11:21

Calcule la distancia para cada uno de ellos, y seleccione(1..10, n) en O (n) tiempo. Eso sería el algoritmo ingenuo, supongo.

 0
Author: Rubys,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-21 06:30:04