You can approximate the answer in $O(N log N + kN)$ time where $k$ is the number of sample lines you wish to test. To do so:
- Choose a uniform set of $k$ points $\theta$ spanning the range [0,π). These are your sample lines.
- Do a loop starting with the line for $\theta=0$. ($O(N)$)
- Get a unit vector $u$ in the direction of $\theta$. ($O(N)$)
- Get a vector $\hat u$ perpendicular to this vector. ($O(N)$)
- Project all the points onto $\hat u$. ($O(N)$)
- Sort the points along $\hat u$. ($O(N)$)
- Slide a window the size of your margin along the sorted points and note when the maximal number of points are within this window along with the midpoint of the window when this occurs. ($O(N)$)
- Return information about the line which had the maximum point count.
The algorithm takes $O(N log N)$ time for initially sorting the points. However, after this we are projecting the points onto a slowly rotating line. The slow rotation means that each reprojection results in the points already being very close to their previously sorted positions:
Most sorting algorithms are able to deal with this situation in nearly-linear time. Note also that as the number of samples increases, the degree to which the points are already sorted increases as well.
The full algorithm looks like this:
#include <algorithm>
#include <cassert>
#include <cmath>
#include <functional>
#include <iostream>
#include <random>
#include <vector>
class FracPoint {
public:
float frac = std::numeric_limits<float>::quiet_NaN();
float x;
float y;
FracPoint(float x0, float y0) : x(x0), y(y0) {}
bool operator<(const FracPoint &o) const {
return frac<o.frac;
}
bool operator>(const FracPoint &o) const {
return frac>o.frac;
}
};
struct BestLine {
int count;
double slope;
double intercept;
};
typedef std::vector<FracPoint> Points;
typedef std::pair<int,double> CountIntercept;
///Projects points to their closest orthogonal point on a line at an angle theta
///to the x-axis. Sets each point's `frac` property to the number of unit
///vectors their projection is from the origin. This can be used to sort points
///along the length of the line.
void GetFractionalDistPointsProjectedToVector(
Points &pts,
const double theta
){
const double tvx = std::cos(theta);
const double tvy = std::sin(theta);
//Get a line perpendicular to the one we just generated. This is the line we
//sort the points along to find the maximal margin.
const auto vx = -tvy;
const auto vy = tvx;
//Do a dot product (v dot s)/(s dot s) and multiply by s to get projection
for(auto &pt: pts)
pt.frac = (pt.x*vx+pt.y*vy)/(vx*vx+vy*vy);
}
CountIntercept FindBestIntercept(Points &pts, const double margin){
Points::iterator upper=pts.begin();
Points::iterator lower=pts.begin();
int best_count = 0;
double best_pos = 0;
for(;upper!=pts.end();++upper){
while(upper->frac-lower->frac>margin)
++lower;
const auto count = upper-lower+1;
if(count>best_count){
best_count = count;
best_pos = (upper->frac-lower->frac)/2;
}
}
return CountIntercept(best_count,best_pos);
}
BestLine FindBestLine(Points &pts, const int samples, const double margin){
int best_count = 0;
double best_theta;
for(int i=0;i<samples;i++){
const double theta = i*(M_PI/samples);
//Project points onto the line defined by theta
GetFractionalDistPointsProjectedToVector(pts, theta);
//Since points are already mostly sorted from the last projection, use
//insertion sort's O(N) best case scenario to make them totally sorted.
// ska_sort(pts.begin(),pts.end(),[](const FracPoint &pt){return pt.frac;});
std::sort(pts.begin(),pts.end());
const auto count_intercept = FindBestIntercept(pts, margin);
if(count_intercept.first>best_count){
best_count = count_intercept.first;
best_theta = theta;
}
}
//Project points onto the line defined by the best theta
GetFractionalDistPointsProjectedToVector(pts, best_theta);
//Since points are being projected onto a "randomly" chosen line, we use an
//O(N log N) sort
std::sort(pts.begin(),pts.end());
const auto count_intercept = FindBestIntercept(pts, margin);
//`count_intercept.second` isn't actually the intercept, it's the distance
//along a line perpendicular to the one defined theta at which the best
//intercept is located. Let's find the real intercept now.
double intercept;
{
//Get vector along theta
const double tvx = std::cos(best_theta);
const double tvy = std::sin(best_theta);
//Get perpendicular vector
auto vx = -tvy;
auto vy = tvx;
//Multiply by `counter_intercept.second` to get absolute position of best
//point.
vx *= count_intercept.second;
vy *= count_intercept.second;
//Get slope of line
double slope = std::tan(best_theta);
//Use point-slope equation of line y-y1=m*(x-x1) and reformulate to y=m*x+b
//to recover the actual intercept
intercept = -slope*vx+vy;
}
return {best_count, std::tan(best_theta), intercept};
}
int main(int argc, char **argv){
if(argc!=3){
std::cout<<"Syntax: "<<argv[0]<<" <N> <Samples>"<<std::endl;
return -1;
}
const int N = std::stoi(std::string(argv[1]));
const int samples = std::stoi(std::string(argv[2]));
//Poor way of initializing PRNG. Fine for this application.
std::mt19937 gen(1234567);
std::uniform_real_distribution<> dis(-100, 100);
auto dice = std::bind(dis,gen);
Points pts;
for(int i=0;i<N;i++)
pts.emplace_back(dice(),dice());
const auto ret = FindBestLine(pts, samples, 25);
std::cout<<"Count = "<<ret.count<<std::endl;
std::cout<<"Best slope = "<<ret.slope<<std::endl;
std::cout<<"Best intercept = "<<ret.intercept<<std::endl;
return 0;
}
Now, we time the algorithm for 100 samples at various numbers of points, which gives the following:
1000, 0.014
10000, 0.083
50000, 0.445
100000, 0.943
500000, 5.615
1000000, 12.092
5000000, 68.748
10000000, 144.155
which, indeed, is almost linear (slope of 1.028 on a log-log graph with R2=0.9962).
OpenMP can be used as a simple way to parallelize the operation:
#pragma omp parallel for firstprivate(pts)
for(int i=0;i<samples;i++){
const double theta = i*(M_PI/samples);
//Project points onto the line defined by theta
GetFractionalDistPointsProjectedToVector(pts, theta);
//Since points are already mostly sorted from the last projection, use
//insertion sort's O(N) best case scenario to make them totally sorted.
// ska_sort(pts.begin(),pts.end(),[](const FracPoint &pt){return pt.frac;});
std::sort(pts.begin(),pts.end());
const auto count_intercept = FindBestIntercept(pts, margin);
#pragma omp critical
if(count_intercept.first>best_count){
best_count = count_intercept.first;
best_theta = theta;
}
}