HDU.1007 Quoit Design

一、题目解读

1、原题

HDU.1007 Quoit Design

2、分类

分治法——最近点对

3、题意

给定一些点,求一个圆的半径、满足“圆最多只能使1个点在其内部”。

再稍微转化一下,就是求一堆点里最小的两点间距,然后再除以 2 2 2

4、输入输出格式

输入/输出要求与格式
输入样例个数通过输入 N = 0 N=0 N=0标识输入结束
输入格式(每个样例)第一行输入一个数 N N N,后 N N N行每行都输入一组坐标 x x x y y y(空格隔开)
输出格式(每个样例)每行输出一个结果
输出精度结果精确到小数点后 2 2 2

5、数据范围

数据范围
N N N 2 ≤ N ≤ 1 0 5 2 \leq N \leq 10^5 2N105
( x , y ) (x, y) (x,y) x , y ∈ R x, y \in \mathbb{R} x,yR

二、题解参考

1、总体思路

思路时间复杂度具体解释
穷举法 O ( n 2 ) O(n^2) O(n2)穷举所有两个点间的距离,找最小
分治法 O ( n log ⁡ n ) O(n\log n) O(nlogn)求左右两个半区间的最小值,然后考虑跨区间的合并

2、思路②

(1).分析

分治法的主要思想是将大的问题划分为若干个小的子问题。

在这里主要就表现为,求若干个点的最小距离困难,但是求2、3个点的最小距离容易。因此,对n个点,我们先根据点的横坐标 x x x进行排序,不断地划分左、右区间,一直划分到只剩2、3个点。

那么很显然,得到的是两个子区间各自内部的距离最小值,我们对这两个值求一个最小值得到 d d d.

跨区间的部分要怎么考虑合并呢?跨区间的部分的合并显然不能一个一个列举,还是会变成穷举的时间复杂度 O ( n 2 ) O(n^2) O(n2).因此,需要考虑优化。

仔细想想的话,我们可以肯定横坐标范围在 [ x m i d − d , x m i d + d ] \left[ x_{mid} - d, x_{mid} + d \right] [xmidd,xmid+d]之外的点都不用考虑。因为要跨越左右区间,所以横坐标 x m i d x_{mid} xmid一定会在被比较的两个点的横坐标之间。

x l e f t _ i < x m i d − d x_{left\_i} < x_{mid} - d xleft_i<xmidd为例:
∵ x l e f t _ i < x m i d − d 且 x r i g h t _ j > x m i d ∴ x r i g h t _ j − x l e f t _ i > d 再 结 合 两 点 间 坐 标 公 式 , 易 知 : l e f t _ i 和 r i g h t _ j 这 两 个 点 间 距 必 定 大 于 d \begin{aligned} &\because x_{left\_i} < x_{mid} - d且x_{right\_j} > x_{mid} \\ &\therefore x_{right\_j} - x_{left\_i} > d \\ &再结合两点间坐标公式,易知:\\ &left\_i和right\_j这两个点间距必定大于d \end{aligned} xleft_i<xmiddxright_j>xmidxright_jxleft_i>dleft_iright_jd

但是这样筛选出来的点仍然有可能有很多个,直接逐个比较的话仍然会超时,因此我们还需要根据其纵坐标 y y y再进行一次优化。

我们将选出来的 c n t cnt cnt个点根据纵坐标再进行一次升序排序,然后从前往后逐个求距离:第 1 1 1个点逐个和后面 c n t − 1 cnt - 1 cnt1个点求距离更新 d d d、第 2 2 2个点逐个和后面 c n t − 2 cnt - 2 cnt2个点求距离更新 d d d、……但是在比较的时候,如果第 j j j个点的纵坐标 y j y_j yj已经比第 i i i个点的纵坐标 y i y_i yi多出 d d d,即 y j − y i > d y_j - y_i > d yjyi>d,那么从第 j j j个点开始往后的点都不可能起到更新 d d d的作用,所以直接break出去,开始外层循环的下一次循环。

这样优化以后,效率会好很多。(印象中当时老师讲的时候说过,有人证明了XXXX,说明了XXXXX最多只会有6个点,所以效率会好很多)

(注):本文的代码参考了这篇文章的代码,因此代码相似度将近95%,本文的思路是对这个代码进行分析理解得到的。

(2).AC代码

HDU(C++/G++)AC代码如下:

#include <iostream>
#include <algorithm>
#include <cmath>
#include <iomanip>

#define N 100005

using namespace std;

struct node
{
	double x;
	double y;
}us[N];

int a[N];

// 将坐标根据x升序排列
bool cmp_x(const node& a, const node&b)
{
	return a.x < b.x;
}

// 将坐标索引根据对应的y升序排列进行排列
bool cmp_yi(int a, int b)
{
	return us[a].y < us[b].y;
}

// 计算两点间距
inline double dis(node p1, node p2)
{
	return sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y));
}

double find(int l, int r)
{
	// 分治到只剩两个点
	if (l + 1 == r)
		return dis(us[l], us[r]);

	// 分治到只剩三个点
	if (l + 2 == r)
		return min(min(dis(us[l], us[l + 1]), dis(us[l + 1], us[r])), dis(us[l], us[r]));

	// 寻找左、右半个区间内的最大、最小距离
	int mid = (l + r) >> 1;
	double d = min(find(l, mid), find(mid + 1, r));

	// 合并左右区间的最小距离
	// (在x ∈ [mid.x - d, mid.x + d]的范围内寻找,再根据纵坐标排序,效率可以提高很多)
	int cnt = 0;
	for (int i = l; i <= r; ++i)
		if (us[i].x >= us[mid].x - d && us[i].x <= us[mid].x + d)
			a[cnt++] = i;
	sort(a, a + cnt, cmp_yi);
	for (int i = 0; i < cnt; ++i)
		for (int j = i + 1; j < cnt; ++j)
		{
			if (us[a[j]].y - us[a[i]].y >= d)
				break;
			d = min(d, dis(us[a[i]], us[a[j]]));
		}

	return d;
}

int main()
{
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);

	int n;
	while (cin >> n, n)
	{
		// 输入
		for (int i = 0; i < n; ++i)
			cin >> us[i].x >> us[i].y;
		
		// 以x升序、将n个坐标进行排序
		sort(us, us + n, cmp_x);

		// 输出分治法查找的结果
		cout << fixed << setprecision(2) << find(0, n - 1) / 2 << endl;
	}

	return 0;
}

三、总结与后话

1、评价

这道题目是一道很典型的分治法例题——“求最近点对”。

2、后话

看了10min题目,才想起来这是去年暑假培训的时候老师讲过的;搜了又搜,才想起来这道题题型是“求最近点对”。

当时将分治法、分治思想的时候,感觉还是有所领悟的(除了二分答案我有点懵),半年内,刷的题合起来总共不到30题。到了半年后的今天,居然连分治法的结构长什么样子都记不太清楚了,实在是丢人。

也当做是个毒鸡汤,警示所有人,ACM没有持续和稳定的刷题练习是很难有所长进的。
要加油啊!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

God-Excious

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值