Миллионы 3D-точек: как найти 10 из них ближе всего к данной точке?

Точка в 3-d определяется (x, y, z). Расстояние d между любыми двумя точками (X, Y, Z) и (x, y, z) равно d = Sqrt [(X-x) ^ 2 + (Y-y) ^ 2 + (Z-z) ^ 2]. Теперь в файле содержится миллион записей, каждая запись - это некоторая точка в пространстве, в определенном порядке. Для любой точки (a, b, c) найдите ближайшие 10 баллов. Как бы вы сохранили миллион очков и как бы вы извлекли эти 10 баллов из этой структуры данных.

Ответ 1

Миллион очков - это небольшое число. Здесь наиболее простой подход (код на основе KDTree медленнее (для запроса только одной точки)).

Подход грубой силы (время ~ 1 секунда)

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

Запустите его:

$ 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

Здесь script, который генерирует миллионы трехмерных точек:

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

Вывод:

$ 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

Вы можете использовать этот код для тестирования более сложных структур данных и алгоритмов (например, действительно ли они потребляют меньше памяти или быстрее, чем самый простой подход). Стоит отметить, что на данный момент это единственный ответ, содержащий рабочий код.

Решение основано на KDTree (время ~ 1,4 секунды)

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

Запустите его:

$ 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

Частичная сортировка в С++ (время ~ 1,1 секунды)

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

Запустите его:

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

Приоритетная очередь в С++ (время ~ 1,2 секунды)

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

Запустите его:

$ 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

Линейный подход, основанный на поиске (время ~ 1,15 секунды)

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

Измерения показывают, что большая часть времени тратится на чтение массива из файла, фактические вычисления занимают на порядок меньше времени.

Ответ 2

Если миллион записей уже находится в файле, нет необходимости загружать их все в структуру данных в памяти. Просто держите массив с десятью точками, найденными до сих пор, и просматривайте миллион очков, обновляя свой список из десяти по мере продвижения.

Это O (n) в количестве точек.

Ответ 3

Вы можете сохранить точки в k-мерном дереве (kd-tree). Kd-деревья оптимизированы для поиска ближайших соседей (поиск n точек, ближайших к данной точке).

Ответ 4

Я думаю, что это сложный вопрос, который проверяет, не пытаетесь ли вы переусердствовать.

Рассмотрим простейший алгоритм, который люди уже дали выше: держите таблицу из десяти лучших кандидатов и просматривайте все точки один за другим. Если вы найдете более близкую точку, чем любая из десяти лучших до сих пор, замените ее. Какая сложность? Ну, мы должны посмотреть каждую точку из файла один раз, рассчитать ее расстояние (или квадрат расстояния на самом деле) и сравнить с 10-й ближайшей точкой. Если это лучше, вставьте его в нужное место в таблице 10 лучших.

И какая сложность? Мы рассматриваем каждую точку один раз, так что это n вычислений расстояний и n сравнений. Если точка лучше, нам нужно вставить ее в нужное положение, это потребует еще нескольких сравнений, но это постоянный фактор, поскольку таблица лучших кандидатов имеет постоянный размер 10.

В итоге мы получаем алгоритм, который работает в линейном времени, O (n) в количестве точек.

А теперь рассмотрим, что такое нижняя граница на таком алгоритме? Если во входных данных нет порядка, мы должны посмотреть на каждую точку, чтобы увидеть, не является ли она одной из ближайших. Итак, насколько я могу судить, нижняя граница - Омега (n), и, следовательно, алгоритм выше оптимален.

Ответ 5

Это не вопрос домашней работы, не так ли?; -)

Моя мысль: перебирать все точки и помещать их в мини-кучу или в ограниченную очередь приоритетов, привязанных к расстоянию от цели.

Ответ 6

Нет необходимости вычислять расстояние. Просто квадрат расстояния должен служить вашим потребностям. Должно быть быстрее, я думаю. Другими словами, вы можете пропустить бит sqrt.

Ответ 7

Этот вопрос по существу проверяет ваши знания и/или интуицию алгоритмы разбиения пространства. Я бы сказал, что хранить ваши данные в octree - ваш лучший выбор. Он обычно используется в 3d-машинах, которые обрабатывают именно эту проблему (храня миллионы вершин, трассировку лучей, поиск столкновений и т.д.). Время поиска будет порядка log(n) в худшем случае (я считаю).

Ответ 8

Простой алгоритм:

Храните точки в виде списка кортежей и просматривайте точки, вычисляя расстояние и сохраняя "ближайший" список.

Больше объявлений:

Группируйте точки в области (такие как куб, который описывается "от 0,0,0" до "50,50,50", или "0,0,0" до "-20, -20, -20" ), поэтому вы можете "индексировать" их с целевой точки. Проверьте, в каком кубе находится цель, и только просматривайте точки в этом кубе. Если в этом кубе меньше 10 баллов, проверьте "соседние" кубы и т.д.

С другой стороны, это не очень хороший алгоритм: если ваша целевая точка ближе к стене куба, чем 10 баллов, вам также придется искать в соседнем кубе.

Я бы пошел с подходом kd-tree и нашел ближайший, а затем удаляю (или отмечаю) ближайший node и повторно просматриваю новый ближайший node. Промыть и повторить.

Ответ 9

Для любых двух точек P1 (x1, y1, z1) и P2 (x2, y2, z2), если расстояние между точками равно d, то должно выполняться все следующее:

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

Удерживайте 10 ближайших, когда вы перебираете весь свой набор, но также держите расстояние до ближайшего 10-го. Сэкономьте себе много сложностей, используя эти три условия, прежде чем вычислять расстояние для каждой точки, на которую вы смотрите.

Ответ 10

в основном комбинация первых двух ответов выше меня. так как точки находятся в файле, нет необходимости хранить их в памяти. Вместо массива или минимальной кучи я бы использовал максимальную кучу, так как вы хотите только проверить расстояния меньше 10-й ближайшей точки. Для массива вам нужно будет сравнить каждое новое расчётное расстояние со всеми 10 оставшимися расстояниями. Для минимальной кучи вам необходимо выполнить 3 сравнения с каждым новым расчетным расстоянием. При максимальной куче вы выполняете только одно сравнение, когда только что вычисленное расстояние больше корня node.

Ответ 11

Этот вопрос нуждается в дальнейшем определении.

1) Решение относительно алгоритмов, которые предварительно индексируют данные, сильно зависит от того, можете ли вы хранить все данные в памяти или нет.

С помощью kdtrees и octrees вам не нужно будет хранить данные в памяти, а производительность зависит от этого факта не только потому, что объем памяти ниже, а просто потому, что вам не придется читать весь файл.

С помощью bruteforce вам нужно будет прочитать весь файл и пересчитать расстояния для каждой новой точки, которую вы будете искать.

Тем не менее, это может быть не важно для вас.

2) Другим фактором является то, сколько раз вам придется искать точку.

Как утверждает JF Sebastian, иногда брутфорс быстрее даже на больших наборах данных, но учтите, что его бенчмарки измеряют считывание всего набора данных с диска (что не обязательно, когда построено kd-tree или octree и где-то) и что они измеряют только один поиск.

Ответ 12

Рассчитайте расстояние для каждого из них и сделайте Select (1..10, n) в O (n) времени. Наверное, это наивный алгоритм.