web-dev-qa-db-fra.com

Des millions de points 3D: comment trouver les 10 points les plus proches d'un point donné?

Un point en 3D est défini par (x, y, z). La distance d entre deux points quelconques (X, Y, Z) et (x, y, z) est d = Sqrt [(X-x) ^ 2 + (Y-y) ^ 2 + (Z-z) ^ 2]. Maintenant, il y a un million d'entrées dans un fichier, chaque entrée est un point dans l'espace, sans ordre spécifique. Étant donné n'importe quel point (a, b, c), trouvez les 10 points les plus proches. Comment stockeriez-vous le million de points et comment récupéreriez-vous ces 10 points de cette structure de données.

67
Kazoom

Le million de points est un petit nombre. L'approche la plus simple fonctionne ici (le code basé sur KDTree est plus lent (pour interroger un seul point)).

Approche par force brute (temps ~ 1 seconde)

#!/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]]))

Exécuter:

$ 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

Voici le script qui génère des millions de points 3D:

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

Production:

$ 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

Vous pouvez utiliser ce code pour tester des structures de données et des algorithmes plus complexes (par exemple, s'ils consomment réellement moins de mémoire ou plus rapidement que l'approche la plus simple ci-dessus). Il convient de noter qu'à l'heure actuelle, c'est la seule réponse qui contient du code de travail.

Solution basée sur KDTree (temps ~ 1,4 secondes)

#!/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]

Exécuter:

$ 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

Tri partiel en C++ (temps ~ 1,1 seconde)

// $ 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
}

Exécuter:

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

File d'attente prioritaire en C++ (temps ~ 1,2 seconde)

#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';  
  }
}

Exécuter:

$ 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

Approche basée sur la recherche linéaire (temps ~ 1,15 seconde)

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

Les mesures montrent que la plupart du temps est consacré à la lecture du tableau à partir du fichier, les calculs réels prennent de l'ordre de grandeur moins de temps.

93
jfs

Si les millions d'entrées sont déjà dans un fichier, il n'est pas nécessaire de les charger toutes dans une structure de données en mémoire. Gardez simplement un tableau avec les dix premiers points trouvés jusqu'à présent et parcourez le million de points, en mettant à jour votre liste des dix premiers au fur et à mesure.

C'est O(n) en nombre de points.

20
Will

Vous pouvez stocker les points dans un arbre k-dimensionnel (arbre kd). Les arbres Kd sont optimisés pour les recherches du plus proche voisin (recherche des points n les plus proches d'un point donné).

14
mipadi

Je pense que c'est une question délicate qui teste si vous n'essayez pas d'en faire trop.

Considérez l'algorithme le plus simple que les gens ont déjà donné ci-dessus: gardez un tableau de dix meilleurs candidats jusqu'à présent et passez en revue tous les points un par un. Si vous trouvez un point plus proche que l'un des dix meilleurs jusqu'à présent, remplacez-le. Quelle est la complexité? Eh bien, nous devons regarder chaque point du fichier une fois , calculer sa distance (ou carré de la distance en fait) et comparer avec le 10ème point le plus proche . Si c'est mieux, insérez-le à l'endroit approprié dans le tableau des 10 meilleurs à ce jour.

Quelle est donc la complexité? Nous regardons chaque point une fois, c'est donc n calculs de la distance et n comparaisons. Si le point est meilleur, nous devons l'insérer dans la bonne position, cela nécessite quelques comparaisons supplémentaires, mais c'est un facteur constant car le tableau des meilleurs candidats est d'une taille constante 10.

On se retrouve avec un algorithme qui s'exécute en temps linéaire, O(n) en nombre de points.

Mais considérons maintenant quel est le borne inférieure sur un tel algorithme? S'il n'y a pas d'ordre dans les données d'entrée, nous devons regarder chaque point pour voir s'il n'est pas l'un des plus proches. Donc, pour autant que je puisse voir, la limite inférieure est Omega (n) et donc l'algorithme ci-dessus est optimal.

10
Krystian

Pas besoin de calculer la distance. Juste le carré de la distance devrait répondre à vos besoins. Ça devrait être plus rapide je pense. En d'autres termes, vous pouvez ignorer le bit sqrt.

5
Agnel Kurian

Ce n'est pas une question de devoirs, n'est-ce pas? ;-)

Ma pensée: itérer sur tous les points et les mettre dans un tas minimum ou une file d'attente prioritaire bornée, indexée par la distance de la cible.

4
David Z

Cette question teste essentiellement vos connaissances et/ou votre intuition de algorithmes de partitionnement d'espace . Je dirais que le stockage des données dans un octree est votre meilleur pari. Il est couramment utilisé dans les moteurs 3D qui gèrent exactement ce type de problème (stockage de millions de sommets, lancer de rayons, recherche de collisions, etc.). Le temps de recherche sera de l'ordre de log(n) dans le pire des cas (je crois).

4
Kai

Algorithme simple:

Stockez les points sous forme de liste de tuples et parcourez les points, calculez la distance et gardez une liste "la plus proche".

Plus créatif:

Regroupez les points en régions (comme le cube décrit par "0,0,0" à "50,50,50" ou "0,0,0" à "-20, -20, -20"), de sorte que vous peut "indexer" en eux à partir du point cible. Vérifiez dans quel cube se trouve la cible et recherchez uniquement parmi les points de ce cube. S'il y a moins de 10 points dans ce cube, vérifiez les cubes "voisins", etc.

Après réflexion, ce n'est pas un très bon algorithme: si votre point cible est plus proche du mur d'un cube que de 10 points, vous devrez également rechercher dans le cube voisin.

J'opterais pour l'approche kd-tree et trouverais le plus proche, puis supprimerais (ou marquerais) ce nœud le plus proche et rechercherais le nouveau nœud le plus proche. Rincez et répétez.

2
Jeff Meatball Yang

Pour deux points P1 (x1, y1, z1) et P2 (x2, y2, z2) quelconques, si la distance entre les points est d, toutes les conditions suivantes doivent être remplies:

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

Tenez les 10 plus proches pendant que vous parcourez l'ensemble de votre ensemble, mais maintenez également la distance jusqu'au 10e plus proche. Économisez beaucoup de complexité en utilisant ces trois conditions avant de calculer la distance pour chaque point que vous regardez.

2
Kirk Broadhurst

essentiellement une combinaison des deux premières réponses au-dessus de moi. puisque les points sont dans un fichier, il n'est pas nécessaire de les garder en mémoire. Au lieu d'un tableau ou d'un tas min, j'utiliserais un tas max, car vous ne voulez vérifier que les distances inférieures au 10e point le plus proche. Pour un tableau, vous devez comparer chaque distance nouvellement calculée avec les 10 distances que vous avez conservées. Pour un tas minimum, vous devez effectuer 3 comparaisons avec chaque distance nouvellement calculée. Avec un segment de mémoire max, vous effectuez uniquement une comparaison lorsque la distance nouvellement calculée est supérieure au nœud racine.

1
Yiling

Cette question doit être davantage définie.

1) La décision concernant les algorithmes de pré-indexation des données varie beaucoup selon que vous pouvez ou non conserver l'ensemble des données en mémoire.

Avec kdtrees et octrees, vous n'aurez pas à conserver les données en mémoire et les performances en bénéficieront, non seulement parce que l'empreinte mémoire est plus faible, mais simplement parce que vous n'aurez pas à lire le fichier entier.

Avec bruteforce, vous devrez lire tout le fichier et recalculer les distances pour chaque nouveau point que vous recherchez.

Pourtant, cela pourrait ne pas être important pour vous.

2) Un autre facteur est le nombre de fois que vous devrez rechercher un point.

Comme indiqué par JF Sebastian, parfois, bruteforce est plus rapide même sur de grands ensembles de données, mais veillez à prendre en compte que ses repères mesurent la lecture de l'ensemble des données à partir du disque (ce qui n'est pas nécessaire une fois que kd-tree ou octree est construit et écrit quelque part) et qu'ils ne mesurent qu'une seule recherche.

0
Unreason

Calculez la distance pour chacun d'eux et sélectionnez (1..10, n) en O(n) temps. Ce serait l'algorithme naïf je suppose.

0
Rubys