前言
最近点对问题是分治法的典型应用案例,下面分别从一维和二维的角度给出了利用分治法求解最近点对的方法和代码,并且使用递归式和递归树的方法分析了时间复杂度。
一、一维最近点对问题
1、问题提出及代码求解
先输入一组点的个数,再输入数轴上这组点的坐标(整数),输出这组点之中最近的两点的距离。
![](https://raw.githubusercontent.com/DavidZyy/PicBed/master/img/图片1.png)
例:
输入 | 5 1 3 5 6 8 |
---|---|
输出 | 1 |
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6;
int A[N];//保存输入的数组
int closet_pot(int p, int q)
{
if(p == q)
return INFINITY;
if(p == q-1)
return A[q] - A[p];
int mid = (p+q)/2;
int a = closet_pot(p, mid);
int b = closet_pot(mid+1, q);
int Min = min(a, b);
Min = min(Min, A[mid+1] - A[mid]);
return Min;
}
int main()
{
int n;
cin>>n;
for(int i = 0; i < n; i++)
cin>>A[i];
sort(A, A+n);
cout<<closet_pot(0, n-1);
system("pause");
}
2、时间复杂度分析
按照分治法divide-conquer-merge的步骤分析,上述代码中的合并步骤:
Min = min(Min, A[mid+1] - A[mid]);
只有
O
(
1
)
O(1)
O(1)的时间复杂度,我们可以写出下列的递归式:
T
(
n
)
=
{
O
(
1
)
,
n
=
1
2
T
(
n
2
)
+
O
(
1
)
,
n
>
1
T(n) = \begin{cases} O(1), & n=1 \\[2ex] 2T(\frac{n}{2}) + O(1), & n>1 \end{cases}
T(n)=⎩⎨⎧O(1),2T(2n)+O(1),n=1n>1
使用递归树法进行分析:
计算公式如下:
O
(
1
)
+
O
(
2
)
+
O
(
4
)
+
O
(
8
)
⋯
+
O
(
n
)
≈
O
(
2
n
)
O(1)+O(2)+O(4)+O(8)\cdots+O(n) \approx O(2n)
O(1)+O(2)+O(4)+O(8)⋯+O(n)≈O(2n)
所以使用分治法计算一维最近点对的时间复杂度大约为
O
(
2
n
)
O(2n)
O(2n)。
二、二维最近点对问题
1、问题提出及求解
下面是一道求二维最近点对的例题:Quoit Design。简单来说,就是把原先分布在数轴上的点分布在平面上,在这种情况下求最近点对。
求解代码参考了博客最近点对问题。
/**
最近点对问题,时间复杂度为O(n*logn*logn)
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const double INF = 1e20;
const int N = 100005;
struct Point
{
double x;
double y;
}point[N];
int n;
int tmpt[N];
bool cmpxy(const Point& a, const Point& b)
{
if(a.x != b.x)
return a.x < b.x;
return a.y < b.y;
}
bool cmpy(const int& a, const int& b)
{
return point[a].y < point[b].y;
}
double min(double a, double b)
{
return a < b ? a : b;
}
double dis(int i, int j)
{
return sqrt((point[i].x-point[j].x)*(point[i].x-point[j].x)
+ (point[i].y-point[j].y)*(point[i].y-point[j].y));
}
double Closest_Pair(int left, int right)
{
double d = INF;
if(left==right)
return d;
if(left + 1 == right)
return dis(left, right);
int mid = (left+right)>>1;
double d1 = Closest_Pair(left,mid);
double d2 = Closest_Pair(mid+1,right);
d = min(d1,d2);
int i,j,k=0;
//分离出宽度为d的区间
for(i = left; i <= right; i++)
{
if(fabs(point[mid].x-point[i].x) <= d)
tmpt[k++] = i;
}
sort(tmpt,tmpt+k,cmpy);
//线性扫描
for(i = 0; i < k; i++)
{
for(j = i+1; j < k && point[tmpt[j]].y-point[tmpt[i]].y<d; j++)
{
double d3 = dis(tmpt[i],tmpt[j]);
if(d > d3)
d = d3;
}
}
return d;
}
int main()
{
while(true)
{
scanf("%d",&n);
if(n==0)
break;
for(int i = 0; i < n; i++)
scanf("%lf %lf",&point[i].x,&point[i].y);
sort(point,point+n,cmpxy);
printf("%.2lf\n",Closest_Pair(0,n-1)/2);
}
return 0;
}
2、时间复杂度分析
关于合并(merge) 有更优化的做法,但是总的来说,时间复杂度是
O
(
n
)
O(n)
O(n),可以写出下列递归式:
T
(
n
)
=
{
O
(
1
)
,
n
<
4
2
T
(
n
2
)
+
O
(
n
)
,
n
≥
4
T(n) = \begin{cases} O(1), & n<4 \\[2ex] 2T(\frac{n}{2}) + O(n), & n \geq 4 \end{cases}
T(n)=⎩⎨⎧O(1),2T(2n)+O(n),n<4n≥4
得出下列的递归树:
![](https://i-blog.csdnimg.cn/blog_migrate/9988729df55d32d01dfc048ea3b323c5.png)
可以看到,一共有 log 2 n \log_2{n} log2n层,每层时间复杂度为 O ( n ) O(n) O(n),总的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
总结
在最近点对问题上分治法得到了充分的应用,其实分治法不一定每次分为两边,可以分为三边甚至更多,比如改写二维点对代码中的 Closest_Pair 函数,每回分为3个子问题解决:
double Closest_Pair(int left, int right)
{
double d = INF;
if(left >= right)
return d;
if(left + 1 == right)
return dis(left, right);
int mid1 = (left+right)/3;
int mid2 = (left+right)*2/3;
double d1 = Closest_Pair(left,mid1);
double d2 = Closest_Pair(mid1+1,mid2);
double d3 = Closest_Pair(mid2+1, right);
d = min(d1,d2);
d = min(d, d3);
int i,j,k=0;
//分离出宽度为d的区间
for(i = left; i <= right; i++)
{
if(fabs(point[mid1].x-point[i].x) <= d)
tmpt[k++] = i;
}
sort(tmpt,tmpt+k,cmpy);
//线性扫描
for(i = 0; i < k; i++)
{
for(j = i+1; j < k; j++)
{
double d3 = dis(tmpt[i],tmpt[j]);
if(d > d3)
d = d3;
}
}
k = 0;
for(i = left; i <= right; i++)
{
if(fabs(point[mid2].x-point[i].x) <= d)
tmpt[k++] = i;
}
sort(tmpt,tmpt+k,cmpy);
//线性扫描
for(i = 0; i < k; i++)
{
for(j = i+1; j < k; j++)
{
double d3 = dis(tmpt[i],tmpt[j]);
if(d > d3)
d = d3;
}
}
return d;
}
在测试样例上都通过了,但可惜的是笔者在提交时遇到了超出内存限制的问题,未能解决。
如果分为3个子问题,递归式可以写成:
T
(
n
)
=
{
O
(
1
)
,
n
<
6
3
T
(
n
3
)
+
O
(
c
n
)
,
n
≥
6
T(n) = \begin{cases} O(1), & n<6 \\[2ex] 3T(\frac{n}{3}) + O(cn), & n \geq 6 \end{cases}
T(n)=⎩⎨⎧O(1),3T(3n)+O(cn),n<6n≥6
相应的,递归树将有3个分叉,层数降低,但是每一层的时间复杂度比分为两个子问题增加了。
其中 c c c不太好确定,但是总的来说,和分为两个子问题的时间复杂度应该差别不大。
水平所限,难免纰漏,如有错误,欢迎指出。
参考资料
【1】最近点对问题