一、问题描述
Description
给你 N 个点,你要找到一个周长最小的三角形。
为了减小问题的难度,这里的三角形也包括共线的三点。
Input
第一行包含一个整数 N 表示点的个数。
接下来 N 行每行有两个整数,表示这个点的坐标(x,y) 。
Output
输出只有一行,包含一个6位小数,为周长最短的三角形的周长(四舍五入)。
Sample Input 1
4
1 1
2 3
3 3
3 4
Sample output 1
3.414214
二、解题分析
如果没有约束条件可以直接按输入的数据顺序,三重循环,取三点求周长,得到最少周长。
但本题需要优化搜索空间并减少计算量,所以采用分治方法。
问题分解:首先,将原始问题分解为更小的子问题。在这里,可以将点集分成两个部分,分别在左右两边递归计算最小周长。(按x坐标或有坐标排序后,用二分)
计算子问题的最小周长:对左右两个部分分别计算最小周长。通过递归,这些子问题会一直分解到最小规模,即一个或两个点的情况,这时直接计算这些点之间的距离即可。(分别计算左右部分的周长)
合并子问题的解:获得左右两个部分的最小周长后,比较两部分的最小周长值,取其中较小的值作为整体的最小周长。(比较左右得到的最小周长)
考虑中间部分:在合并阶段,还需要考虑左右两个部分之间的情况。这时候,可能存在一个最小周长的三角形跨越了左右两个部分,所以需要考虑中间部分的情况。(中间部分也可能是最小三角形,将于中点距离大于左右两个部分的最小周长添加到一个集合,再将和里的点按y坐标排序,然后暴力查找,条件是取的两个点的dy要小于左右两个部分的最小周长)。
比较并更新最小周长:在中间部分,以中间的一个点作为基准,向左右两边延伸找到可能构成更小周长三角形的点集。然后计算这些点集中的三角形周长,选取最小的作为中间部分的最小周长,并与左右部分的最小周长进行比较,取较小者。
最终结果:最终得到的最小周长即为整个点集构成的最小周长三角形。
为什么能得到最小周长?
通过递归地考虑左右两部分和中间部分,分治算法能够覆盖整个搜索空间,并最终得到最小周长的三角形。
三、代码实现
import java.util.*;
public class myMinTriangle {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(); // 读取输入整数n
Point points[] = new Point[n]; // 创建Point数组存储坐标点
for(int i=0; i<n; i++){
double x = sc.nextDouble(); // 读取x坐标
double y = sc.nextDouble(); // 读取y坐标
points[i] = new Point(x, y); // 存储坐标点到数组中
}
Arrays.sort(points, Comparator.comparingDouble(p -> p.x)); // 按照x坐标对点进行排序
double minPerimeter = findMinTrianglePerimeter(points, 0, n - 1); // 寻找最小周长的三角形
System.out.printf("%.6f\n", minPerimeter); // 输出结果
}
// 计算两点之间的距离
static double calculateDistance(Point a, Point b) {
double dx = a.x - b.x; // 计算x方向的差值
double dy = a.y - b.y; // 计算y方向的差值
return Math.sqrt(dx * dx + dy * dy); // 返回两点之间的欧氏距离
}
// 寻找最小周长的三角形
static double findMinTrianglePerimeter(Point[] points, int left, int right) {
if (left + 2 > right) return Double.MAX_VALUE; // 如果点的数量小于3个,返回最大值
if (left + 2 == right) { // 如果只有3个点
return calculateDistance(points[left], points[left + 1]) + // 计算三角形的周长
calculateDistance(points[left + 1], points[left + 2]) +
calculateDistance(points[left], points[left + 2]);
}
int mid = (left + right) / 2; // 计算中间点的索引
double leftMin = findMinTrianglePerimeter(points, left, mid); // 递归处理左半部分
double rightMin = findMinTrianglePerimeter(points, mid + 1, right); // 递归处理右半部分
double minDistance = Math.min(leftMin, rightMin); // 计算左右两部分的最小周长
List<Point> strip = new ArrayList<>(); // 创建strip列表存储中间部分的点
for (int i = left; i <= right; i++) {
if (Math.abs(points[i].x - points[mid].x) < minDistance) { // 找出位于中间部分的点
strip.add(points[i]); // 将满足条件的点添加到strip列表中
}
}
strip.sort(Comparator.comparingDouble(p -> p.y)); // 按照y坐标对strip列表进行排序
for (int i = 0; i < strip.size(); i++) {
for (int j = i + 1; j < strip.size() && strip.get(j).y - strip.get(i).y < minDistance; j++) {
for (int k = j + 1; k < strip.size() && strip.get(k).y - strip.get(i).y < minDistance; k++) {
double perimeter = calculateDistance(strip.get(i), strip.get(j)) + // 计算三角形的周长
calculateDistance(strip.get(j), strip.get(k)) +
calculateDistance(strip.get(i), strip.get(k));
minDistance = Math.min(minDistance, perimeter); // 更新最小周长
}
}
}
return minDistance; // 返回最小周长
}
}
class Point {
double x;
double y;
public Point() {
}
public Point(double x, double y) {
this.x = x;
this.y = y;
}
}