算法设计与分析:分治法 - 最近点对问题

【算法设计与分析】实验一:分治法 - 最近点对问题


一、实验目的与要求

1. 实验目的

  1. 理解并掌握分治法的基本原理,并能将其应用于解决实际问题。
  2. 学会最近点对问题求解方法。

2. 实验要求

  1. 准确理解最近点对问题的含义和解决方法。
  2. 掌握分治法的基本原理,能灵活运用在最近点对问题上。
  3. 能编写出正确实现分治法解决最近点对问题的代码。
  4. 分析程序的运行结果,并与理论分析比较。

二、实验内容

编写一个程序,利用分治法解决最近点对问题,并测试程序的运行结果。


三、实验方法

  1. 学习并深入理解最近点对问题的基本概念和应用背景。
  2. 学习并理解分治法的基本理论,并掌握如何应用分治法来求解最近点对问题。
  3. 根据理论知识,用编程语言(C或C++)编写实现分治法解决最近点对问题的代码。
  4. 运行程序,记录程序的运行时间,并且与理论预测的时间复杂度做对比分析。

四、详细的算法设计及运行结果

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, drd = 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),利用鸽笼原理减少跨区比较次数,能提升大规模数据下的计算效率。


五、实验感想

  1. 设计不同算法解决问题的基础是算法的正确性。在测试不同算法的时候,一定要保证不同算法的正确性。在本实验中,可以通过暴力穷举来验证分治法答案的正确性。

  2. 对于一些数学类问题,都可以通过数学分析并借助分治法进行求解,从而缩短程序的运行时间。例如:逆序对问题。

  3. 算法的时间复杂度一般由若干次常数级时间组成,因此减少算法运行时间时也应降低算法的常数级时间。例如本次实验中最后再计算平方根,中间比较过程可以比较距离的平方值。

算法时间分析

可以看到,当对于小数量级的数据时,暴力穷举法要优于分治法,但当数量级较大时,分治法明显优于暴力穷举法。这是由于分治法需要开辟内存空间并通过递归栈实现递归,因此将消耗更多时间,并对小数量级下的最终时间消耗产生较大影响。

因此对于"最近点对"问题最聪明的方法是当数量级小于 10² 时选择暴力穷举法进行运算,当数量级大于 10² 时选择分治法进行运算。


相关推荐
易筋紫容2 小时前
创建型模式:对象的诞生艺术
开发语言·前端·javascript
888CC++2 小时前
C++ 快速学习指南:从入门到进阶的实战路线
开发语言·c++
2501_926978332 小时前
提示工程的实战报告(二):模型的失败模式与边界行为
人工智能·深度学习·算法
小小晓.2 小时前
C++记:函数
开发语言·c++·算法
casual~2 小时前
模逆元计算方法详解:扩展欧几里得算法与费马小定理
学习·算法·逆元
牡丹雅忻13 小时前
AES 加密模式演进:从 ECB、CBC 到 GCM 的 C# 深度实践
java·开发语言·c#
连续讨伐3 小时前
php小结
开发语言·php
进击的程序猿~3 小时前
Go 并发底层原理面试学习指南
开发语言·面试·golang
脚踏实地皮皮晨3 小时前
002002002_DepandencyObject类2
开发语言·windows·算法·c#·visual studio
练习时长一年3 小时前
@SneakyThrows注解
开发语言