質問は:
X座標とy座標を持つN点(2Dで)が与えられたら、他の(N-1)点からPまでの距離の合計が最小になるような点P(N与えられた点で)を見つけます。
この点は、一般的に Geometric Median として知られています。素朴なO(N^2)
以外に、この問題を解決するための効率的なアルゴリズムはありますか?
シミュレートされたアニーリング を使用して、ローカルのオンライン裁判官と同様の問題を解決しました。それも公式の解決策であり、プログラムはACを取得しました。
唯一の違いは、私が見つけなければならない点は、与えられたN
の一部である必要はなかったことです。
これは私のC++コードであり、N
は50000
と同じ大きさになる可能性があります。プログラムは、2 GHzのペンティアム4の0.1s
で実行されます。
// header files for IO functions and math
#include <cstdio>
#include <cmath>
// the maximul value n can take
const int maxn = 50001;
// given a point (x, y) on a grid, we can find its left/right/up/down neighbors
// by using these constants: (x + dx[0], y + dy[0]) = upper neighbor etc.
const int dx[] = {-1, 0, 1, 0};
const int dy[] = {0, 1, 0, -1};
// controls the precision - this should give you an answer accurate to 3 decimals
const double eps = 0.001;
// input and output files
FILE *in = fopen("adapost2.in","r"), *out = fopen("adapost2.out","w");
// stores a point in 2d space
struct punct
{
double x, y;
};
// how many points are in the input file
int n;
// stores the points in the input file
punct a[maxn];
// stores the answer to the question
double x, y;
// finds the sum of (euclidean) distances from each input point to (x, y)
double dist(double x, double y)
{
double ret = 0;
for ( int i = 1; i <= n; ++i )
{
double dx = a[i].x - x;
double dy = a[i].y - y;
ret += sqrt(dx*dx + dy*dy); // classical distance formula
}
return ret;
}
// reads the input
void read()
{
fscanf(in, "%d", &n); // read n from the first
// read n points next, one on each line
for ( int i = 1; i <= n; ++i )
fscanf(in, "%lf %lf", &a[i].x, &a[i].y), // reads a point
x += a[i].x,
y += a[i].y; // we add the x and y at first, because we will start by approximating the answer as the center of gravity
// divide by the number of points (n) to get the center of gravity
x /= n;
y /= n;
}
// implements the solving algorithm
void go()
{
// start by finding the sum of distances to the center of gravity
double d = dist(x, y);
// our step value, chosen by experimentation
double step = 100.0;
// done is used to keep track of updates: if none of the neighbors of the current
// point that are *step* steps away improve the solution, then *step* is too big
// and we need to look closer to the current point, so we must half *step*.
int done = 0;
// while we still need a more precise answer
while ( step > eps )
{
done = 0;
for ( int i = 0; i < 4; ++i )
{
// check the neighbors in all 4 directions.
double nx = (double)x + step*dx[i];
double ny = (double)y + step*dy[i];
// find the sum of distances to each neighbor
double t = dist(nx, ny);
// if a neighbor offers a better sum of distances
if ( t < d )
{
update the current minimum
d = t;
x = nx;
y = ny;
// an improvement has been made, so
// don't half step in the next iteration, because we might need
// to jump the same amount again
done = 1;
break;
}
}
// half the step size, because no update has been made, so we might have
// jumped too much, and now we need to head back some.
if ( !done )
step /= 2;
}
}
int main()
{
read();
go();
// print the answer with 4 decimal points
fprintf(out, "%.4lf %.4lf\n", x, y);
return 0;
}
次に、このアルゴリズムによって返される(x, y)
に最も近いものをリストから選択するのが正しいと思います。
このアルゴリズムは、幾何学的中央値に関するこのWikipediaの段落の内容を利用しています。
ただし、各手順がより正確な近似を生成する反復手順を使用して、幾何学的中央値への近似を計算することは簡単です。このタイプの手順は、各サンプルポイントまでの距離が凸であり、凸関数の合計が凸のままであるため、サンプルポイントまでの距離の合計が凸関数であるという事実から派生できます。したがって、各ステップでの距離の合計を減らす手順は、局所最適に陥ることはありません。
このタイプの一般的なアプローチの1つは、Endre Weiszfeldの作業の後でWeiszfeldのアルゴリズムと呼ばれる[4]で、繰り返し再重み付けされた最小二乗の形式です。このアルゴリズムは、現在の推定からサンプルまでの距離に反比例する一連の重みを定義し、これらの重みに従ってサンプルの加重平均である新しい推定を作成します。あれは、
上記の最初の段落では、これが機能する理由を説明します。最適化しようとしている関数にはローカル最小値がないため、反復的に改善することにより、最小値を貪欲に見つけることができます。
これを一種のバイナリ検索と考えてください。最初に、結果を近似します。適切な近似は重心であり、コードが入力を読み取るときに計算されます。次に、これに隣接するポイントがより良い解決策を与えるかどうかを確認します。この場合、現在のポイントからの距離がstep
である場合、ポイントは隣接していると見なされます。それがより良い場合は、現在のポイントを破棄しても問題ありません。これは、先に述べたように、最小化しようとしている関数の性質上、ローカルミニマムにトラップされないためです。
この後、バイナリ検索と同様にステップサイズを半分にして、十分な近似であると見なされるものになるまで続けます(eps
定数によって制御されます)。
したがって、アルゴリズムの複雑さは、結果をどの程度正確にしたいかによって異なります。
ユークリッド距離を使用する場合、問題はO(n^2)
時間よりも早く解決することが難しいようです。ただし、他のポイントへのマンハッタン距離の合計を最小化するポイント、または他のポイントへのユークリッド距離の二乗の合計を最小化するポイントは、O(n log n)
時間。 (2つの数値を乗算すると仮定するとO(1)
)。最近の post からマンハッタンの距離に対する私のソリューションを恥知らずにコピー/貼り付けさせてください:
X座標のソートされた配列を作成し、配列内の各要素について、その座標を選択する「水平」コストを計算します。要素の水平コストは、X軸に投影されたすべてのポイントまでの距離の合計です。これは、アレイを2回(左から右に1回、逆方向に1回)スキャンすることにより、線形時間で計算できます。同様に、y座標の並べ替えられた配列を作成し、配列内の各要素について、その座標を選択する「垂直」コストを計算します。
これで、元の配列の各ポイントについて、他のすべてのポイントの合計コストをO(1)時間で水平および垂直コストを追加することで計算できます。したがって、最適なポイントをO(n)したがって、実行時間の合計はO(n log n)です。
他のポイントまでのユークリッド距離の二乗の合計を最小化するポイントを計算するための同様のアプローチに従うことができます。ソートされたx座標を次のようにします:x1、 バツ2、 バツ3、 ...、 バツん。このリストを左から右にスキャンし、各ポイントx私 私たちは計算します:
l私 = xの左側にあるすべての要素までの距離の合計私 =(x私-バツ1)+(x私-バツ2)+ .... +(x私-バツi-1)、および
sl私 = xの左側にあるすべての要素までの距離の2乗の合計私 =(x私-バツ1)^ 2 +(x私-バツ2)^ 2 + .... +(x私-バツi-1)^ 2
与えられたl私 そしてsl私 私たちはlを計算することができますi + 1 そしてsli + 1O(1)
時間では、次のようになります。
D = xとするi + 1-バツ私。次に:
li + 1 = l私 + idおよびsli + 1 = sl私 + id ^ 2 + 2 * i * d
したがって、すべてのlを計算できます私 そしてsl私 左から右にスキャンすることにより線形時間で。同様に、すべての要素について、rを計算できます私:右側のすべての要素とsrまでの距離の合計私:線形時間での右側のすべての要素までの距離の2乗の合計。 SRを追加する私 そしてsl私 各iについて、すべての要素への水平距離の二乗の合計を線形時間で示します。同様に、すべての要素への垂直距離の二乗の合計を計算します。
次に、元のポイント配列をスキャンして、以前のように垂直距離と水平距離の平方の合計を最小化するポイントを見つけます。
前述のように、使用するアルゴリズムのタイプは、距離の測定方法によって異なります。あなたの質問はこの尺度を指定していないので、ここにマンハッタン距離と平方ユークリッド距離の両方のC実装があります。 2Dポイントには_dim = 2
_を使用します。複雑さO(n log n)
。
マンハッタン距離
_double * geometric_median_with_manhattan(double **points, int N, int dim) {
for (d = 0; d < dim; d++) {
qsort(points, N, sizeof(double *), compare);
double S = 0;
for (int i = 0; i < N; i++) {
double v = points[i][d];
points[i][dim] += (2 * i - N) * v - 2 * S;
S += v;
}
}
return min(points, N, dim);
}
_
簡単な説明:ディメンションごとの距離を合計できます。 N
ポイントがあり、1次元の値が_v_0
_、..、v_(N-1)
およびT = v_0 + .. + v_(N-1)
であるとします。次に、各値に対して_v_i
_にS_i = v_0 .. v_(i-1)
があります。これで、左側の_i * v_i - S_i
_と右側のT - S_i - (N - i) * v_i
を合計することにより、この値のマンハッタン距離を表すことができ、結果は_(2 * i - N) * v_i - 2 * S_i + T
_になります。すべての要素にT
を追加しても順序は変更されないため、省略します。そして_S_i
_はその場で計算できます。
これを実際のCプログラムにする残りのコードは次のとおりです。
_#include <stdio.h>
#include <stdlib.h>
int d = 0;
int compare(const void *a, const void *b) {
return (*(double **)a)[d] - (*(double **)b)[d];
}
double * min(double **points, int N, int dim) {
double *min = points[0];
for (int i = 0; i < N; i++) {
if (min[dim] > points[i][dim]) {
min = points[i];
}
}
return min;
}
int main(int argc, const char * argv[])
{
// example 2D coordinates with an additional 0 value
double a[][3] = {{1.0, 1.0, 0.0}, {3.0, 1.0, 0.0}, {3.0, 2.0, 0.0}, {0.0, 5.0, 0.0}};
double *b[] = {a[0], a[1], a[2], a[3]};
double *min = geometric_median_with_manhattan(b, 4, 2);
printf("geometric median at {%.1f, %.1f}\n", min[0], min[1]);
return 0;
}
_
二乗ユークリッド距離
_double * geometric_median_with_square(double **points, int N, int dim) {
for (d = 0; d < dim; d++) {
qsort(points, N, sizeof(double *), compare);
double T = 0;
for (int i = 0; i < N; i++) {
T += points[i][d];
}
for (int i = 0; i < N; i++) {
double v = points[i][d];
points[i][dim] += v * (N * v - 2 * T);
}
}
return min(points, N, dim);
}
_
短い説明:以前とほとんど同じアプローチですが、少し複雑な派生があります。 TT = v_0^2 + .. + v_(N-1)^2
と言うと、_TT + N * v_i^2 - 2 * v_i^2 * T
_が得られます。もう一度TTがすべてに追加されるので、省略できます。リクエストについての詳細な説明。
ステップ1:ポイントコレクションをx次元(nlogn)でソートします
ステップ2:各ポイントとすべてのポイントの間のx距離を計算します左へその:
_xLDist[0] := 0
for i := 1 to n - 1
xLDist[i] := xLDist[i-1] + ( ( p[i].x - p[i-1].x ) * i)
_
ステップ3:各ポイントとすべてのポイントの間のx距離を計算しますその通りその:
_xRDist[n - 1] := 0
for i := n - 2 to 0
xRDist[i] := xRDist[i+1] + ( ( p[i+1].x - p[i].x ) * i)
_
ステップ4:両方を合計すると、各ポイントから他のN-1ポイントまでのx距離の合計が得られます
_for i := 0 to n - 1
p[i].xDist = xLDist[i] + xRDist[i]
_
Y次元を使用してステップ1、2、3、4を繰り返し、_
p[i].yDist
_を取得します。
xDist
とyDist
の合計が最も小さい点が答えです
合計複雑度O(nlogn)
詳しい説明:
アイデアは、以前に計算された前のポイントの合計距離を再利用することです。
3ポイントABCDでソートされたとしましょう。Dから他のオブジェクトまでの合計左距離は次のとおりです。
AD + BD + CD =(AC + CD)+(BC + CD)+ CD = AC + BC + 3CD
_(AC + BC)
_は、Cから他のCまでの合計左距離であり、これを利用してldist(C) + 3CD
を計算するだけで済みます
私はワイズフェルト法を実装しました(あなたが探しているものではないことを知っていますが、それはあなたのポイントを近似するのに役立つかもしれません)、複雑さはO(N * M/k)です(Nはポイントの数、Mはポイント(あなたの場合は2)であり、kは望ましいエラーです:
問題は、凸計画として解決できます(目的関数は必ずしも凸ではありません)。凸型プログラムは、L-BFGSなどの反復法を使用して解くことができます。各反復のコストはO(N)であり、通常、必要な反復の数は大きくありません。必要な反復の数を減らすための1つの重要なポイントは、最適な答えが入力のポイントです。そのため、その回答が入力ポイントの1つに近づいたときに最適化を停止できます。