【算法设计与分析】实验一:分治法 - 最近点对问题
一、实验目的与要求
1. 实验目的
- 理解并掌握分治法的基本原理,并能将其应用于解决实际问题。
- 学会最近点对问题求解方法。
2. 实验要求
- 准确理解最近点对问题的含义和解决方法。
- 掌握分治法的基本原理,能灵活运用在最近点对问题上。
- 能编写出正确实现分治法解决最近点对问题的代码。
- 分析程序的运行结果,并与理论分析比较。
二、实验内容
编写一个程序,利用分治法解决最近点对问题,并测试程序的运行结果。
三、实验方法
- 学习并深入理解最近点对问题的基本概念和应用背景。
- 学习并理解分治法的基本理论,并掌握如何应用分治法来求解最近点对问题。
- 根据理论知识,用编程语言(C或C++)编写实现分治法解决最近点对问题的代码。
- 运行程序,记录程序的运行时间,并且与理论预测的时间复杂度做对比分析。
四、详细的算法设计及运行结果
1. 算法设计及结果
实验结果用10个随机浮点数测试完成。
实验中要求求解最近点对,对于蛮力法我们都很了解,将问题看成一个求n边无向完全图的问题。则对于n个顶点,每个顶点存在连边 n - 1 条,又因为是无向图,则共有连边 n(n-1)/2 条。因此时间复杂度为 O(n²) ,但当问题规模达到 10^5 时运行时间就会超时,这时就需要我们对蛮力法进行优化,采用分治思想,将大问题拆解成小问题,进而得到最优解。
蛮力法求解过程:
cpp
#include <bits/stdc++.h>
using namespace std;
int main(){
int t;cin >> t;
while(t--){
int n;cin >> n;
if(n&(n-1)) cout << "Ben" << endl;
else cout << "Fang" << endl;
}
return 0;
}
1.1 首先定义结构体
cpp
struct Point {
double x, y;
};
1.2 算法思想
① 对所有点按照其横坐标进行排序;
② 若 n = 1,返回无穷大;n = 2,计算距离并返回;n > 2,以该组数组下标中值分割,即 mid = (l+r)/2, mid_x = point[mid].x,将规模为n的问题分解为两个规模为 n/2 的子问题并重复步骤②,直到得到两个子问题的解 mindis1, mindis2;
③ 合并子问题的点并将其按y坐标排序;
④ 令 mindis 为两个子问题解中的较小值,遍历当前规模下所有点,找出横坐标范围在 [mid_x - mindis, mid_x + mindis] 的所有点;
⑤ 将步骤④中的所有点与按y坐标排好序后的其后5个基准点进行比较,如果距离小于 mindis 则更新 mindis 的值。
将平面的点分为两个点集,此时对于某点对中两个点 p1, p2 存在如下三种情况:
- ①
p1, p2都位于左侧点集 - ②
p1, p2都位于右侧点集 - ③
p1, p2一个位于左侧点集,另一个位于右侧点集
对于情况①与②,可以通过递归进行解决,难点在于如何处理两个点不处于同一个点集的情况。
假设左右两侧点集的最小距离为 dl, dr,d = min(dl, dr)。我们对分割线两边 d 区域进行检测就行,得到最优解。
1.3 代码实现
定义计算距离函数:
cpp
double dis(Point p1, Point p2) {
return sqrt(pow((p1.x - p2.x), 2) + pow((p1.y - p2.y), 2));
}
横坐标排序:
cpp
bool cmpx(const Point &P1, const Point &P2) {
return P1.x < P2.x;
}
💡 归并时利用STL库中的
inplace_merge函数
核心分治函数:
cpp
double Divide(int left, int right) {
if (left == right) {
return 1e18;
}
if (left + 1 == right) {
if (p[left].y > p[right].y) {
swap(p[left], p[right]);
}
return dis(p[left], p[right]);
}
int mid = left + (right - left) / 2;
double mid_x = p[mid].x;
double d = min(Divide(left, mid), Divide(mid + 1, right));
inplace_merge(p.begin() + left, p.begin() + mid + 1, p.begin() + right + 1, cmpy);
vector<Point> temp;
for (int i = left; i <= right; i++) {
if (abs(p[i].x - mid_x) <= d) {
temp.push_back(p[i]);
}
}
int i_size = temp.size();
for (int i = 0; i < i_size; i++) {
for (int j = i + 1; j < i_size && j < i + 6; j++) {
if ((temp[j].y - temp[i].y) >= d) break;
d = min(d, dis(temp[i], temp[j]));
}
}
return d;
}
完整代码(文末)

cpp
#include <bits/stdc++.h>
#include <ctime>
using namespace std;
struct point {
double x;
double y;
};
point ans_p1, ans_p2;
double min_dist;
//计算距离
double dis(const point& a, const point& b) {
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
// x排序
bool cmpx(const point& a, const point& b) {
return a.x < b.x;
}
// y排序
bool cmpy(const point& a, const point& b) {
return a.y < b.y;
}
vector<point> p;
double Divide(int left, int right) {
if (left == right) {
return 1e18;
}
if (left + 1 == right) {
if (p[left].y > p[right].y) {
swap(p[left], p[right]);
}
double cur_dis = dis(p[left], p[right]);
if (cur_dis < min_dist) {
min_dist = cur_dis;
ans_p1 = p[left];
ans_p2 = p[right];
}
return cur_dis;
}
int mid = left + (right - left) / 2;
double mid_x = p[mid].x;
double d = min(Divide(left, mid), Divide(mid + 1, right));
inplace_merge(p.begin() + left, p.begin() + mid + 1, p.begin() + right + 1, cmpy);
vector<point> temp;
for (int i = left; i <= right; i++) {
if (abs(p[i].x - mid_x) <= d) {
temp.push_back(p[i]);
}
}
int size = temp.size();
for (int i = 0; i < size; i++) {
for (int j = i + 1; j < size && j < i + 9; j++) {
if (temp[j].y - temp[i].y >= d) break;
double cur_dis = dis(temp[i], temp[j]);
if (cur_dis < d) {
d = cur_dis;
if (d < min_dist) {
min_dist = d;
ans_p1 = temp[i];
ans_p2 = temp[j];
}
}
}
}
return d;
}
int main() {
srand(time(0));
const int nums = 30;
p.resize(nums);
// 生成30个 1~10 的随机浮点数点
for (int i = 0; i < nums; i++) {
p[i].x = (double)rand() / RAND_MAX * 9 + 1;
p[i].y = (double)rand() / RAND_MAX * 9 + 1;
printf("第%2d个点:(%.4f, %.4f)\n", i + 1, p[i].x, p[i].y);
}
sort(p.begin(), p.end(), cmpx);
min_dist = 1e18;
Divide(0, nums - 1);
printf("点A坐标:(%.4f, %.4f)\n", ans_p1.x, ans_p1.y);
printf("点B坐标:(%.4f, %.4f)\n", ans_p2.x, ans_p2.y);
printf("两点最短距离:%.4f\n", min_dist);
return 0;
}

2. 算法的特色
分治法通过递归分割点集、合并子问题解,将最近点对问题的时间复杂度从暴力法的 O(n²) 优化至 O(n log n),利用鸽笼原理减少跨区比较次数,能提升大规模数据下的计算效率。

五、实验感想
-
设计不同算法解决问题的基础是算法的正确性。在测试不同算法的时候,一定要保证不同算法的正确性。在本实验中,可以通过暴力穷举来验证分治法答案的正确性。
-
对于一些数学类问题,都可以通过数学分析并借助分治法进行求解,从而缩短程序的运行时间。例如:逆序对问题。
-
算法的时间复杂度一般由若干次常数级时间组成,因此减少算法运行时间时也应降低算法的常数级时间。例如本次实验中最后再计算平方根,中间比较过程可以比较距离的平方值。
算法时间分析
可以看到,当对于小数量级的数据时,暴力穷举法要优于分治法,但当数量级较大时,分治法明显优于暴力穷举法。这是由于分治法需要开辟内存空间并通过递归栈实现递归,因此将消耗更多时间,并对小数量级下的最终时间消耗产生较大影响。
因此对于"最近点对"问题最聪明的方法是当数量级小于 10² 时选择暴力穷举法进行运算,当数量级大于 10² 时选择分治法进行运算。