如果只考虑3D点,那么对于“最小的封闭球”来说,这种朴素的方法不是很好吗?足够好

本文关键字:方法 来说 很好 3D 如果 | 更新日期: 2023-09-27 17:53:48

在我们的例子中使用c#。

public class Sphere
{
    public Point Center { get; set; }
    public float Radius { get; set; }
    public Sphere(IEnumerable<Point> points)
    {
        Point first = points.First();
        Point vecMaxZ = first;
        Point vecMinZ = first;
        Point vecMaxY = first;
        Point vecMinY = first;
        Point vecMinX = first;
        Point vecMaxX = first;
        foreach (Point current in points)
        {
            if (current.X < vecMinX.X)
            {
                vecMinX = current;
            }
            if (current.X > vecMaxX.X)
            {
                vecMaxX = current;
            }
            if (current.Y < vecMinY.Y)
            {
                vecMinY = current;
            }
            if (current.Y > vecMaxY.Y)
            {
                vecMaxY = current;
            }
            if (current.Z < vecMinZ.Z)
            {
                vecMinZ = current;
            }
            if (current.Z > vecMaxZ.Z)
            {
                vecMaxZ = current;
            }
        }
        //the lines bellow assure at least 2 points sit on the surface of the sphere.
        //I'm pretty sure the algorithm is solid so far, unless I messed up the if/elses.
        //I've been over this, looking at the variables and the if/elses and they all
        //seem correct, but our own errors are the hardest to spot,
        //so maybe there's something wrong here.
        float diameterCandidateX = vecMinX.Distance(vecMaxX);
        float diameterCandidateY = vecMinY.Distance(vecMaxY);
        float diameterCandidateZ = vecMinZ.Distance(vecMaxZ);
        Point c;
        float r;
        if (diameterCandidateX > diameterCandidateY)
        {
            if (diameterCandidateX > diameterCandidateZ)
            {
                c = vecMinX.Midpoint(vecMaxX);
                r = diameterCandidateX / 2f;
            }
            else
            {
                c = vecMinZ.Midpoint(vecMaxZ);
                r = diameterCandidateZ / 2f;
            }
        }
        else if (diameterCandidateY > diameterCandidateZ)
        {
            c = vecMinY.Midpoint(vecMaxY);
            r = diameterCandidateY / 2f;
        }
        else
        {
            c = vecMinZ.Midpoint(vecMaxZ);
            r = diameterCandidateZ / 2f;
        }
        //the lines bellow look for points outside the sphere, and if one is found, then:
        //1 let dist be the distance from the stray point to the current center
        //2 let diff be the equal to dist - radius
        //3 radius will then the increased by half of diff.
        //4 a vector with the same direction as the stray point but with magnitude equal to diff is found
        //5 the current center is moved by half the vector found in the step above.
        //
        //the stray point will now be included
        //and, I would expect, the relationship between the center and other points will be mantained:
        //if distance from p to center = r / k,
        //then new distance from p to center' = r' / k,
        //where k doesn't change from one equation to the other.
        //this is where I'm wrong. I cannot figure out how to mantain this relationship.
        //clearly, I'm moving the center by the wrong amount, and increasing the radius wrongly too.
        //I've been over this problem for so much time, I cannot think outside the box.
        //my whole world is the box. The box and I are one.
        //maybe someone from outside my world (the box) could tell me where my math is wrong, please.
        foreach (Point current in points)
        {
            float dist = current.Distance(c);
            if (dist > r)
            {
                float diff = dist - r;
                r += diff / 2f;
                float scaleFactor = diff / current.Length();
                Point adjust = current * scaleFactor;
                c += adjust / 2f;
            }
        }
        Center = c;
        Radius = r;
    }
    public bool Contains(Point point) => Center.Distance(point) <= Radius;
    public override string ToString() => $"Center: {Center}; Radius: {Radius}";
}
public class Point
{
    public float X { get; set; }
    public float Y { get; set; }
    public float Z { get; set; }
    public Point(float x, float y, float z)
    {
        X = x;
        Y = y;
        Z = z;
    }
    public float LengthSquared() => X * X + Y * Y + Z * Z;
    public float Length() => (float) Math.Sqrt(X * X + Y * Y + Z * Z);
    public float Distance(Point another)
    {
        return (float) Math.Sqrt(
            (X - another.X) * (X - another.X)
          + (Y - another.Y) * (Y - another.Y)
          + (Z - another.Z) * (Z - another.Z));
    }
    public float DistanceSquared(Point another)
    {
        return (X - another.X) * (X - another.X)
             + (Y - another.Y) * (Y - another.Y)
             + (Z - another.Z) * (Z - another.Z);
    }
    public Point Perpendicular()
    {
        return new Point(-Y, X, Z);
    }
    public Point Midpoint(Point another)
    {
        return new Point(
            (X + another.X) / 2f,
            (Y + another.Y) / 2f,
            (Z + another.Z) / 2f);
    }
    public override string ToString() => $"({X}, {Y}, {Z})";
    public static Point operator +(Point p1, Point p2)
    {
        return new Point(p1.X + p2.X, p1.Y + p2.Y, p1.Z + p2.Z);
    }
    public static Point operator *(Point p1, float v)
    {
        return new Point(p1.X * v, p1.Y * v, p1.Z * v);
    }
    public static Point operator /(Point p1, float v)
    {
        return new Point(p1.X / v, p1.Y / v, p1.Z / v);
    }
}
//Note: this class is here so I can be able to solve the problems suggested by
//Eric Lippert.
public class Line
{
    private float coefficient;
    private float constant;
    public Line(Point p1, Point p2)
    {
        float deltaY = p2.Y - p1.Y;
        float deltaX = p2.X - p1.X;
        coefficient = deltaY / deltaX;
        constant = coefficient * -p1.X + p1.Y;
    }
    public Point FromX(float x)
    {
        return new Point(x, x * coefficient + constant, 0);
    }
    public Point FromY(float y)
    {
        return new Point((y - constant) / coefficient, y, 0);
    }
    public Point Intersection(Line another)
    {
        float x = (another.constant - constant) / (coefficient - another.coefficient);
        float y = FromX(x).Y;
        return new Point(x, y, 0);
    }
}

我可以放心地假设这将至少运行得和那些通常考虑具有任意维数的点的可能性的高级算法一样快,为了鲁棒性起见,从2到任何数,如1000或10,000维。

我只需要三维空间,不能多也不能少。由于我没有计算机科学的学位(或者任何与此相关的学位,我是一名高二学生),我在分析算法的性能和资源消耗方面有困难。所以我的问题基本上是:与那些花哨的算法相比,我的"最小封闭球体"算法在性能和资源消耗方面是否更好?有没有一个点,我的算法坏了,而专业的没有,这意味着它执行得很差,会造成明显的损失(比如,如果我有太多的点)。

EDIT 1:我编辑代码是因为它根本没有意义(我很饿,现在是下午4点,我一整天都没有吃东西)。我认为这个更有意义,但不确定它是否正确。最初的问题是:如果这个算法解决了问题,在我们提前知道所有点都有3个维度的情况下,它是否能很好地与标准的专业算法竞争?

EDIT 2:现在我很确定性能很差,我失去了实现一个简单的算法来找到最小的封闭球体的所有希望。我只是想做点有用的东西。请检查最新的更新

EDIT 3:也不工作。我不干了。

EDIT 4:最后,我不知道…大约5个小时。我想明白了。耶稣基督。这个是可行的。有人能告诉我性能问题吗?与专业算法相比,它真的很差吗?我可以改哪几行使它更好?它会在某个点断裂吗?请记住,我将始终使用它的3D点。

EDIT 5:我从Bychenko那里了解到以前的算法仍然不起作用。我在这个问题上睡了一觉,这是我的新算法。我知道它不起作用,我有一个很好的线索,它是错误的,谁能告诉为什么这些特定的计算是错误的,以及如何修复它们?我倾向于认为这与三角函数有关。我的假设对欧几里得空间不成立,因为我总是把向量看成实数实数的集合,在我的例子中,我用它来精确定位欧几里得空间中的一个位置。我很确定我在最后一个循环的某个地方遗漏了一些正弦或余弦(当然,不是确切的正弦或余弦,而是笛卡尔坐标中的等效,因为我们不知道任何角度。

EDIT 5的附录:关于Eric Lippert提出的问题:(1)太琐碎(2)我先为圆做;我将为它添加一个class Line

Point a, b, c; //they are not collinear
Point midOfAB = a.Midpoint(b);
Point midOfBC = b.Midpoint(c);
//multiplying the vector by a scalar as I do bellow doesn't matter right?
Point perpendicularToAB = midOfAB.Perpendicular() * 3;
Point perpendicularToBC = midOfBC.Perpendicular() * 3;
Line bisectorAB = new Line(perpendicularToAB, midOfAB);
Line bisectorBC = new Line(perpendicularToBC, midOfBC);
Point center = bisectorAB.Intersection(bisectorBC);
float distA = center.Distance(a);
float distB = center.Distance(b);
float distC = center.Distance(c);
if(distA == distB && distB == distC)
    //it works (spoiler alert: it doesn't)
else
    //you're a failure, programmer, pick up your skate and practice some ollies

如果只考虑3D点,那么对于“最小的封闭球”来说,这种朴素的方法不是很好吗?足够好

对不起,你的算法错了这解决不了问题。

反例(3分):

A = (0, 0, 0) - closest to origin    (0)
B = (3, 3, 0) - farthest from origin (3 * sqrt(2) == 4.2426...) 
C = (4, 0, 0) 

你的朴素算法声明球体的中心在

P = (3 / sqrt(2), 3 / sqrt(2), 0)

半径
R = 3 / sqrt(2)

可以看到点C = (4, 0, 0)在球面

之外

编辑更新(但幼稚)的算法仍然是错误的。

反例(3分):

 A = (0, 0, 0)
 B = (1, 2, 0)
 C = (4, 1, 0)

根据算法球体的中心在

 P = (2, 1, 0)
与半径

 R = sqrt(5)

,你可以看到这个球不是最小的

n编辑你仍然有一个不正确的算法。在探索灰色地带时(您知道问题所在,但是部分地知道,有漏洞),投资于测试自动化是一个很好的实践。你们应该知道,在三角形的情况下,所有的顶点都应该在球面上;让我们根据以下事实验证您的解决方案:

  public static class SphereValidator {
    private static Random m_Random = new Random();
    private static String Validate() {
      var triangle = Enumerable
        .Range(0, 3)
        .Select(i => new Point(m_Random.Next(100), m_Random.Next(100), m_Random.Next(100)))
        .ToArray();
      Sphere solution = new Sphere(triangle);
      double tolerance = 1.0e-5;
      for (int i = 0; i < triangle.Length; ++i) {
        double r = triangle[i].Distance(solution.Center);
        if (Math.Abs(r - solution.Radius) > tolerance) {
          return String.Format("Counter example'r'n  A: {0}'r'n  B: {1}'r'n  C: {2}'r'n  expected distance to '"{3}'": {4}; actual R {5}",
            triangle[0], triangle[1], triangle[2], (char) ('A' + i), r, solution.Radius);
        }
      }
      return null;
    }
    public static String FindCounterExample(int attempts = 10000) {
      for (int i = 0; i < attempts; ++i) {
        String result = Validate();
        if (!String.IsNullOrEmpty(result))
          Console.WriteLine(result);
        return;
      }
      Console.WriteLine(String.Format("Yes! All {0} tests passed!", attempts));
    }
  }
我刚刚运行了上面的代码,得到:
  Counter example
    A: (3, 30, 9)
    B: (1, 63, 40)
    C: (69, 1, 16)
    expected distance to "A": 35.120849609375; actual R 53.62698

对于一个粗略的近似,计算轴对齐的边界框,然后计算该框的边界球(相同的中心,直径=√(W²+ H²+ D²))。

您可以通过计算到该中心的最大距离来进行细化。