Miliony punktów 3D: jak znaleźć 10 z nich najbliżej danego punktu?

Punkt w 3-d jest zdefiniowany przez (x, y, z). Odległość d pomiędzy dowolnymi dwoma punktami (X,Y,Z) i(x,y,z) wynosi d= Sqrt [(X-x)^2 + (Y-y)^2 + (Z-Z)^2]. Teraz jest milion wpisów w pliku, każdy wpis jest jakimś punktem w przestrzeni, w żadnej określonej kolejności. Biorąc pod uwagę dowolny punkt (a, b, c) znajdź najbliższe 10 punktów do niego. Jak można przechowywać milion punktów i jak można odzyskać te 10 punktów z tej struktury danych.

Author: MatrixFrog, 2010-03-21

12 answers

Milion punktów to niewielka liczba. Najprostsze podejście działa tutaj (kod oparty na KDTree jest wolniejszy (dla zapytania tylko jeden punkt)).

Brute-force approach (time ~1 second)

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

Uruchom go:

$ 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

Oto skrypt generujący milion punktów 3D:

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

Wyjście:

$ 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

Możesz użyć tego kodu do testowania bardziej złożonych struktur danych i algorytmów (na przykład, czy rzeczywiście zużywają mniej pamięci, czy szybciej powyższe najprostsze podejście). Warto zauważyć, że w tej chwili jest to jedyna odpowiedź, która zawiera działający kod.

Rozwiązanie oparte na KDTree (czas ~1,4 sekundy)

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

Uruchom go:

$ 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

Sortowanie częściowe w C++ (czas ~1,1 sekundy)

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

Uruchom go:

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

Kolejka priorytetów w C++ (czas ~1,2 sekundy)

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

Uruchom go:

$ 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

Podejście oparte na wyszukiwaniu liniowym (czas ~1,15 sekundy)

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

Pomiary pokazuje, że większość czasu spędza na czytaniu tablicy z pliku, rzeczywiste obliczenia zajmują rząd wielkości mniej czasu.

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-24 06:38:06

Jeśli milion wpisów jest już w pliku, nie ma potrzeby ładowania ich wszystkich do struktury danych w pamięci. Po prostu miej tablicę z najlepszymi 10 punktami znalezionymi do tej pory, i skanuj ponad milion punktów, aktualizując swoją listę top 10 w miarę upływu czasu.

To Jest O (n) w liczbie punktów.

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-21 05:53:01

Możesz przechowywać punkty w K-wymiarowym drzewie (KD-tree). Drzewa Kd są zoptymalizowane do wyszukiwania najbliższego sąsiada (znalezienie N punktów najbliżej danego punktu).

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-21 06:04:36

Myślę, że jest to podchwytliwe pytanie, które sprawdza, jeśli nie próbujesz przesadzić rzeczy.

Rozważ najprostszy algorytm, który ludzie już podali powyżej: miej tabelę dziesięciu najlepszych do tej pory kandydatów i przejrzyj wszystkie punkty jeden po drugim. Jeśli znajdziesz Punkt bliższy niż którykolwiek z dziesięciu najlepszych do tej pory, wymień go. Co to za złożoność? Cóż, musimy spojrzeć na każdy punkt z pliku raz , obliczyć jego odległość (lub kwadrat odległości w rzeczywistości) i porównać z dziesiątym najbliższy punkt. Jeśli jest lepiej, wstaw go w odpowiednim miejscu w tabeli 10 najlepszych do tej pory.

Więc jaka jest złożoność? Patrzymy na każdy punkt raz, więc jest to n obliczeń odległości i N porównań. Jeśli punkt jest lepszy, musimy wstawić go w odpowiedniej pozycji, wymaga to więcej porównań, ale jest to stały czynnik, ponieważ tabela najlepszych kandydatów ma stałą wielkość 10.

Otrzymujemy algorytm, który działa w czasie liniowym, O (n) w liczbie punktów.

Ale teraz zastanów się, jaka jest dolna granica na takim algorytmie? Jeśli nie ma kolejności w danych wejściowych, musimy spojrzeć na każdy punkt, aby sprawdzić, czy nie jest to jeden z najbliższych. Z tego co widzę, dolna granica to Omega (n), a więc powyższy algorytm jest optymalny.

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-21 11:34:47

Nie ma potrzeby obliczania odległości. Tylko kwadrat odległości powinien służyć twoim potrzebom. Powinno być szybciej. Innymi słowy, możesz pominąć sqrt bit.

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-21 08:04:54

To nie jest zadanie domowe, prawda? ;-)

Moja myśl: iterować wszystkie punkty i umieścić je w kolejce min lub ograniczonego priorytetu, keyed przez odległość od celu.

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-21 05:51:33

To pytanie jest zasadniczo testem Twojej wiedzy i / lub intuicji algorytmów partycjonowania przestrzeni . Powiedziałbym, że przechowywanie danych w oktree jest najlepszym rozwiązaniem. Jest powszechnie stosowany w silnikach 3d, które radzą sobie z tego typu problemami (przechowywanie milionów wierzchołków, ray tracing, znajdowanie kolizji itp.). Czas wyszukiwania będzie w kolejności log(n) w najgorszym przypadku (jak sądzę).

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-21 06:39:14

Prosty algorytm:

Przechowuj punkty jako listę krotek i skanuj je, obliczając odległość i zachowując "najbliższą" listę.

Bardziej kreatywne:

Grupuj punkty na regiony (takie jak kostka opisana przez "0,0,0" do "50,50,50" lub "0,0,0" do "-20, -20,-20"), dzięki czemu możesz "indeksować" do nich od punktu docelowego. Sprawdź, w której kostce znajduje się cel i przeszukaj tylko punkty w tej kostce. Jeśli w tej kostce jest mniej niż 10 punktów, sprawdź" sąsiednie " kostki, i tak dalej.

Po zastanowieniu się, nie jest to zbyt dobry algorytm: jeśli twój punkt docelowy jest bliżej ściany sześcianu niż 10 punktów, będziesz musiał również przeszukać sąsiedni sześcian.

Wybrałbym podejście kd-tree i znalazł najbliższy, a następnie usunął (lub oznaczył) najbliższy węzeł i ponownie wyszukał nowy najbliższy węzeł. Spłukać i powtórzyć.

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-21 06:30:14

Dla dowolnych dwóch punktów P1 (x1, y1, Z1) i P2 (x2, y2, z2), jeśli odległość między punktami wynosi d, to wszystkie z poniższych wartości muszą być prawdziwe:

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

Trzymaj 10 najbliżej, gdy powtarzasz cały zestaw, ale także trzymaj odległość do 10 najbliżej. Zaoszczędź sobie wiele złożoności, korzystając z tych trzech warunków przed obliczeniem odległości dla każdego punktu, na który patrzysz.

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-22 23:26:02

Zasadniczo kombinacja dwóch pierwszych odpowiedzi nade mną. ponieważ punkty są w pliku, nie ma potrzeby przechowywania ich w pamięci. Zamiast tablicy lub sterty min, użyłbym sterty max, ponieważ chcesz tylko sprawdzić odległości mniejsze niż 10 najbliższy punkt. W przypadku tablicy należy porównać każdą nowo obliczoną odległość ze wszystkimi 10 zachowanymi odległościami. Dla min sterty, trzeba wykonać 3 porównania z każdej nowo obliczonej odległości. Z max sterty, tylko wykonaj 1 porównanie, gdy nowo obliczona odległość jest większa niż węzeł główny.

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-21 06:03:07

To pytanie wymaga dalszej definicji.

1) Decyzja dotycząca algorytmów, które wstępnie indeksują dane, różni się bardzo w zależności od tego, czy można trzymać całe dane w pamięci, czy nie.

Z kdtrees i octrees nie będziesz musiał przechowywać danych w pamięci, a wydajność z tego korzysta, nie tylko dlatego, że ślad w pamięci jest niższy, ale po prostu dlatego, że nie będziesz musiał czytać całego pliku.

Z bruteforce będziesz musiał przeczytać cały plik i Przelicz odległości dla każdego nowego punktu, którego szukasz.

To może nie być dla Ciebie ważne.

2) Innym czynnikiem jest to, ile razy będziesz musiał szukać punktu.

Jak stwierdził J. F. Sebastian czasami bruteforce jest szybszy nawet na dużych zestawach danych, ale należy wziąć pod uwagę, że jego benchmarki mierzą odczyt całego zestawu danych z dysku (co nie jest konieczne, gdy gdzieś zostanie zbudowany i napisany KD-tree lub octree) i że zmierz tylko jedno wyszukiwanie.

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-21 14:11:21

Oblicz odległość dla każdego z nich i wybierz(1..10, n) W O(N) czasie. To chyba naiwny algorytm.

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-21 06:30:04