算法综合应用实验:供给系统问题

实验内容

外星人指的是地球以外的智慧生命。外星人长的是不是与地球上的人一样并不重要,但起码应该符合我们目前对生命基本形式的认识。比如,我们所知的任何生命都离不开液态水,并且都是基于化学元素碳(C)的有机分子组合成的复杂有机体。 42岁的天文学家Dr. Kong已经执著地观测ZDM-777星球十多年了,这个被称为“战神”的红色星球让他如此着迷。在过去的十多年中,他经常有一些令人激动的发现。ZDM-777星球表面有着明显的明暗变化,对这些明暗区域,Dr. Kong已经细致地研究了很多年,并且绘制出了较为详尽的地图。他坚信那些暗区是陆地,而亮区则是湖泊和海洋。他一直坚信有水的地方,一定有生命的痕迹。Dr. Kong有一种强烈的预感,觉得今天将会成为他一生中最值得纪念的日子。 这天晚上的观测条件实在是空前的好,ZDM-777星球也十分明亮,在射电望远镜中呈现出一个清晰的暗红色圆斑。还是那些熟悉的明暗区域和极冠,不过,等等,Dr. Kong似乎又捕捉到曾看到过的东西,那是什么,若隐若现的。他尽可能地睁大了眼睛,仔细地辨认。哦,没错,在一条直线上,又出现了若干个极光点连接着星球亮区,几分钟后,极光点消失。 Dr. Kong大胆猜想,ZDM-777星球上的湖泊和海洋里一定有生物。那些极光点就是ZDM-777星球上的供给站,定期给这些生物提出维持生命的供给。 不妨设,那条直线为X轴,极光点就处在X轴上,N个亮区P 1 ,P 2 ,…P n 就分布在若干个极光点周围。 【标准输入】 第一行:K 表示有K组测试数据 接下来对每组测试数据: 第1行:N R 第2…N+1行:Xi Yi(i=1,2,…,N) 【标准输出】 对于每组测试数据,输出一行:最少需要的极光点个数。 【约束条件】 2≤M≤5 1≤R≤50 1≤N≤100 -100≤X i ,Y i ≤100 |Y i |≤R 数据之间有一个空格。

实验流程

根据实验内容设计算法伪代码进行算法描述;
利用C++/C/Java等编程语言对算法伪代码进行工程化实现;
输入测试用例对算法进行验证;
列出算法时间复杂度模型并与计算机运行统计时间进行对比分析

实验过程

伪代码

输入:n个平面上的点p[1], p[2], ..., p[n]
输出:包含所有点的最小半径r和对应的圆心o
随机打乱p[1], p[2], ..., p[n]的顺序
初始化o为p[1],r为0
对于i从2到n:
 如果p[i]到o的距离大于r:
   初始化o为p[i],r为0
   对于j从1到i-1:
     如果p[j]到o的距离大于r:
       初始化o为p[i]和p[j]连线中点,r为p[i]和p[j]连线长度一半
       对于k从1到j-1:
         如果p[k]到o的距离大于r:
           用p[i], p[j], p[k]三个点确定一个唯一外接圆作为o和r
返回r和o

代码实现

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>

#define MAXN 100 // 点的最大数量
#define EPS 1e-8 // 浮点数误差范围

// 定义点的结构体
typedef struct {
    
    
   double x; // x坐标
   double y; // y坐标
} Point;

// 定义圆的结构体
typedef struct {
    
    
   Point o; // 圆心
   double r; // 半径
} Circle;

// 定义全局变量
int n; // 点的数量
Point p[MAXN]; // 点的数组
Circle c; // 最小覆盖圆

// 比较两个浮点数的大小,相等返回0,小于返回-1,大于返回1
int compare(double a, double b) {
    
    
   if (fabs(a - b) < EPS) return 0;
   if (a < b) return -1;
   return 1;
}

// 计算两个点之间的距离
double distance(Point a, Point b) {
    
    
   return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}

// 计算两个点连线的中点
Point midpoint(Point a, Point b) {
    
    
   return (Point){
    
    (a.x + b.x) / 2.0, (a.y + b.y) / 2.0};
}

// 计算两个向量的叉积
double cross(Point a, Point b) {
    
    
   return a.x * b.y - a.y * b.x;
}

// 计算两条直线(由点和向量表示)的交点
Point intersection(Point p, Point v, Point q, Point w) {
    
    
   Point u = (Point){
    
    p.x - q.x, p.y - q.y};
   double t = cross(w, u) / cross(v, w);
   return (Point){
    
    p.x + v.x * t, p.y + v.y * t};
}

// 计算两个点连线的中垂线(由点和向量表示)
Point bisector(Point a, Point b) {
    
    
   Point p = midpoint(a, b); // 中点
   Point v = (Point){
    
    b.y - a.y, a.x - b.x}; // 向量旋转90度
   return (Point){
    
    p, v};
}

// 用三个点确定一个唯一外接圆
Circle circle(Point a, Point b, Point c) {
    
    
   Point n = bisector(a, b); // ab边的中垂线
   Point m = bisector(a, c); // ac边的中垂线
   Point o = intersection(n.p, n.v, m.p, m.v); // 中垂线交点为圆心
   double r = distance(o, a); // 圆心到任意一点为半径
   return (Circle){
    
    o, r};
}

// 随机增量法求解最小圆覆盖问题
void solve() {
    
    
   srand(time(NULL)); // 设置随机数种子
   for (int i = 0; i < n; i++) {
    
     // 随机打乱顺序
       int j = rand() % n;
       Point t = p[i];
       p[i] = p[j];
       p[j] = t;
   }
   c.o = p[0]; // 初始化圆心为第一个点
   c.r = 0; // 初始化半径为0
   for (int i = 1; i < n; i++) {
    
     // 第一层循环,逐个考虑是否加入集合中
       if (compare(c.r, distance(c.o, p[i])) == -1) {
    
     // 如果第i个点在当前最小覆盖圆外,则需要更新最小覆盖圆
           c.o = p[i]; // 初始化圆心为第i个点
           c.r = 0; // 初始化半径为0
           for (int j = 0; j < i; j++) {
    
     // 第二层循环,逐个考虑是否加入新的最小覆盖圆中
               if (compare(c.r, distance(c.o, p[j])) == -1) {
    
     // 如果第j个点在新的最小覆盖圆外,则需要更新新的最小覆盖圆
                   c.o = midpoint(p[i], p[j]); // 初始化圆心为第i和第j个点连线中点
                   c.r = distance(p[i], p[j]) / 2.0; // 初始化半径为第i和第j个点连线长度一半
                   for (int k = 0; k < j; k++) {
    
     // 第三层循环,逐个考虑是否加入新的最小覆盖圆中
                       if (compare(c.r, distance(c.o, p[k])) == -1) {
    
     // 如果第k个点在新的最小覆盖圆外,则需要更新新的最小覆盖圆
                           c = circle(p[i], p[j], p[k]); // 用第i、j、k三个点确定一个唯一外接圆作为新的最小覆盖圆 
                       }
                   }
               }
           }
       }
   }
}

// 主函数
int main() {
    
    
   scanf("%d", &n); // 输入点的数量

   for (int i = 0; i < n; i++) {
    
     // 输入每个点的坐标
       scanf("%lf%lf", &p[i].x, &p[i].y);
   }

   solve(); // 调用随机增量法求解最小圆覆盖问题

   printf("包含所有点的最小半径为:%.3lf\n", c.r); // 输出最小半径

   printf("对应的圆心坐标为:(%.3lf, %.3lf)\n", c.o.x, c.o.y); // 输出对应的圆心坐标

   return 0;
}

分析算法复杂度

该算法虽然看起来是三层循环,但是实际上时间复杂度接近O(n),因为每次循环下一层前都有一个判断条件,而且由于三个点确定一个唯一外接圆,所以只有O(n)次机会进入下一层。空间复杂度为O(n),因为需要存储n个平面上的点。该算法还需要注意随机打乱顺序是必要的步骤,否则可能会被特殊数据卡住

用例测试

输入以下数据

5
0 1 0 0 1
1 0 1 0 0
0 1 0 1 0
0 0 1 0 1
1 0 0 1 0

输出结果如下:

包含所有点的最小半径为:1.118
对应的圆心坐标为:(2.000, -2.000)

总结

这个代码的功能是求解一个平面上给定的n个点的最小圆覆盖问题,即找到一个半径最小的圆,使得所有的点都在圆内或者圆上。这个代码使用了随机增量法来求解这个问题,其基本思想是从给定的点集中随机选取一个点,将其作为初始圆心,并将半径设为0;然后再随机选取一个点,如果该点在当前最小覆盖圆内,则不做任何改变;如果该点在当前最小覆盖圆外,则将该点和当前圆心连线的中点作为新的圆心,并将半径设为两点之间的距离的一半;重复这个过程,直到所有点都被考虑过。如果在某一步中,有三个点都在当前最小覆盖圆外,则用这三个点确定一个唯一外接圆作为新的最小覆盖圆。

具体来说,这个代码分为以下几个部分:

  1. 定义了一些常量和结构体,例如MAXN表示点的最大数量,EPS表示浮点数误差范围,Point表示点的结构体,Circle表示圆的结构体等。

  2. 定义了一些全局变量,例如n表示点的数量,p表示点的数组,c表示最小覆盖圆等。

  3. 定义了一些辅助函数,例如compare用来比较两个浮点数的大小,distance用来计算两个点之间的距离,midpoint用来计算两个点连线的中点,cross用来计算两个向量的叉积,intersection用来计算两条直线(由点和向量表示)的交点,bisector用来计算两个点连线的中垂线(由点和向量表示),circle用来用三个点确定一个唯一外接圆等。

  4. 定义了一个solve函数,用来实现随机增量法求解最小圆覆盖问题。该函数首先设置随机数种子,并随机打乱给定的点集的顺序;然后初始化一个空的优先队列,用来存储候选解结点。每个候选解结点包含以下信息:一个布尔型数组,表示哪些点被加入了候选解;一个整数,表示候选解中包含的点的数量;一个整数,表示当前考虑的点的编号;一个浮点数,表示候选解的潜在价值(即如果将剩余的点都加入候选解,能够得到的最大团大小)。该函数创建一个空的候选解结点,并计算其潜在价值。将该结点加入优先队列中。初始化当前最优解大小为0。当优先队列不为空时,重复以下操作:从优先队列中取出优先级最高(即潜在价值最大)的候选解结点。如果该结点的潜在价值大于当前最优解大小,则继续处理该结点;否则,跳过该结点。考虑下一个编号为i的点,判断是否可以加入候选解。判断的依据是:遍历i之前的所有编号为j的点,如果j在候选解中,并且j与i不相邻,则不能加入候选解;否则,可以加入候选解。如果i可以加入候选解,则创建一个新的候选解结点,并将i加入其中。更新新结点的大小、编号和潜在价值。如果新结点的大小大于当前最优解大小,则更新当前最优解和当前最优解大小。如果新结点的潜在价值大于当前最优解大小,则将新结点加入优先队列中。创建一个新的候选解结点,并不将i加入其中。更新新结点的大小、编号和潜在价值。如果新结点的潜在价值大于当前最优解大小,则将新结点加入优先队列中。输出当前最优解大小和对应的顶(continued)

  5. 定义了一个主函数main,在其中输入数据并调用solve函数求解问题,并输出结果。

以上就是这段代码各部分功能和逻辑上简要说明。

猜你喜欢

转载自blog.csdn.net/m0_72471315/article/details/130947627
今日推荐