using System; using System.Collections.Generic; using System.Linq; using System.Text; // For StringBuilder in Point.ToString //----------------------------------------------------------------------------- // Point Struct //----------------------------------------------------------------------------- /// /// Represents a point in N-dimensional space. /// TCoord must be a numeric type supporting comparison and conversion. /// /// The numeric type for coordinates (e.g., int, double, float). public readonly struct Point : IEquatable> where TCoord : struct, IComparable, IConvertible // Constraints for numeric operations & sorting { private readonly TCoord[] _coords; public int Dimensions { get; } /// /// Initializes a new point with the specified coordinates. /// The number of coordinates determines the dimension. /// /// The coordinates for the point. /// Thrown if coords is null. /// Thrown if coords is empty. public Point(params TCoord[] coords) { if (coords == null) throw new ArgumentNullException(nameof(coords)); if (coords.Length == 0) throw new ArgumentException("Coordinates cannot be empty.", nameof(coords)); _coords = (TCoord[])coords.Clone(); // Defensive copy Dimensions = _coords.Length; } /// /// Initializes a new point by copying coordinates from an enumerable. /// /// The coordinates for the point. /// Thrown if coords is null. /// Thrown if coords results in an empty collection. public Point(IEnumerable coords) { if (coords == null) throw new ArgumentNullException(nameof(coords)); _coords = coords.ToArray(); if (_coords.Length == 0) throw new ArgumentException("Coordinates cannot be empty.", nameof(coords)); Dimensions = _coords.Length; } /// /// Gets the coordinate value in the specified dimension. /// /// Dimension index (zero-based). /// The coordinate value. /// Thrown if index is out of bounds. public TCoord Get(int index) { // Rely on array's built-in bounds checking for performance // if (index < 0 || index >= Dimensions) // throw new IndexOutOfRangeException($"Index {index} is out of range for dimension {Dimensions}."); return _coords[index]; } /// /// Calculates the squared Euclidean distance between this point and another point. /// /// The other point. /// The squared distance. /// Thrown if points have different dimensions. public double DistanceSquared(Point other) { if (Dimensions != other.Dimensions) throw new ArgumentException("Points must have the same dimensions."); double distSq = 0; for (int i = 0; i < Dimensions; ++i) { // Convert coordinates to double for calculation. // This is a common approach when TCoord isn't guaranteed to be double. // Using System.Numerics.INumber in .NET 7+ would allow direct arithmetic. double d = Convert.ToDouble(Get(i)) - Convert.ToDouble(other.Get(i)); distSq += d * d; } return distSq; } /// /// Returns a string representation of the point (e.g., "(1, 2, 3)"). /// public override string ToString() { var sb = new StringBuilder(); sb.Append('('); for (int i = 0; i < Dimensions; ++i) { if (i > 0) sb.Append(", "); sb.Append(_coords[i]); } sb.Append(')'); return sb.ToString(); } // --- Equality Members --- public bool Equals(Point other) { if (Dimensions != other.Dimensions) return false; // Using SequenceEqual for robust array comparison return _coords.SequenceEqual(other._coords); } public override bool Equals(object obj) { return obj is Point other && Equals(other); } public override int GetHashCode() { int hashCode = Dimensions.GetHashCode(); if (_coords != null) { foreach (var coord in _coords) { // Simple hash combining approach hashCode = HashCode.Combine(hashCode, coord.GetHashCode()); } } return hashCode; } public static bool operator ==(Point left, Point right) { return left.Equals(right); } public static bool operator !=(Point left, Point right) { return !(left == right); } } //----------------------------------------------------------------------------- // KdTree Class //----------------------------------------------------------------------------- /// /// C# k-d tree implementation for fast nearest neighbor searches. /// /// The numeric type for coordinates. public class KdTree where TCoord : struct, IComparable, IConvertible { // Internal Node class private class Node { public Point Point { get; } public Node Left { get; set; } // Using properties with private setters if needed, or public fields public Node Right { get; set; } public Node(Point point) { Point = point; Left = null; Right = null; } // Helper to access coordinate for sorting/comparison public TCoord Get(int index) => Point.Get(index); // Helper for distance calculation public double DistanceSquared(Point pt) => Point.DistanceSquared(pt); } // --- Fields --- private readonly Node[] _nodes; // Store nodes contiguously for potential cache benefits private readonly Node _root; private readonly int _dimensions; // State for the nearest neighbor search private Node _bestNode = null; private double _bestDistSq = double.MaxValue; private int _visitedCount = 0; // --- Comparer for Sorting Nodes --- private class NodeComparer : IComparer { private readonly int _dimensionIndex; public NodeComparer(int dimensionIndex) { _dimensionIndex = dimensionIndex; } public int Compare(Node x, Node y) { // Comparison relies on the IComparable constraint return x.Get(_dimensionIndex).CompareTo(y.Get(_dimensionIndex)); } } // --- Constructors --- /// /// Builds a k-d tree from a collection of points. /// /// The points to add to the tree. /// Thrown if points is null. /// Thrown if points is empty or contains points with inconsistent dimensions. public KdTree(IEnumerable> points) { if (points == null) throw new ArgumentNullException(nameof(points)); var pointList = points.ToList(); // Materialize the list if (pointList.Count == 0) throw new ArgumentException("Point collection cannot be empty.", nameof(points)); _dimensions = pointList[0].Dimensions; _nodes = new Node[pointList.Count]; for (int i = 0; i < pointList.Count; i++) { if (pointList[i].Dimensions != _dimensions) throw new ArgumentException($"All points must have the same dimension ({_dimensions}). Point {i} has dimension {pointList[i].Dimensions}.", nameof(points)); _nodes[i] = new Node(pointList[i]); } _root = MakeTree(0, _nodes.Length, 0); } /// /// Builds a k-d tree by generating points using a function. /// /// A function that returns a Point. /// The number of points to generate. /// Thrown if pointGenerator is null. /// Thrown if count is zero or negative. /// Thrown if the generator produces points with inconsistent dimensions. public KdTree(Func> pointGenerator, int count) { if (pointGenerator == null) throw new ArgumentNullException(nameof(pointGenerator)); if (count <= 0) throw new ArgumentOutOfRangeException(nameof(count), "Count must be positive."); _nodes = new Node[count]; Point firstPoint = pointGenerator(); _dimensions = firstPoint.Dimensions; _nodes[0] = new Node(firstPoint); for (int i = 1; i < count; i++) { Point p = pointGenerator(); if (p.Dimensions != _dimensions) throw new InvalidOperationException($"Generated points must have consistent dimensions ({_dimensions}). Point {i} has dimension {p.Dimensions}."); _nodes[i] = new Node(p); } _root = MakeTree(0, _nodes.Length, 0); } // --- Tree Building Method --- private Node MakeTree(int begin, int end, int index) { if (end <= begin) return null; // Base case: empty range // Calculate median index int n = begin + (end - begin) / 2; // Sort the segment [begin, end) based on the current dimension 'index' // This partitions the array around the median element at index 'n' // Array.Sort sorts the range [begin, begin + length), so length is end - begin Array.Sort(_nodes, begin, end - begin, new NodeComparer(index)); // Median element becomes the current node Node currentNode = _nodes[n]; // Cycle to the next dimension for children int nextIndex = (index + 1) % _dimensions; // Recursively build left and right subtrees currentNode.Left = MakeTree(begin, n, nextIndex); currentNode.Right = MakeTree(n + 1, end, nextIndex); return currentNode; } // --- Public API --- /// /// Gets the number of dimensions for points in this tree. /// public int Dimensions => _dimensions; /// /// Returns true if the tree is empty. /// public bool IsEmpty => _nodes.Length == 0; /// /// Returns the number of nodes visited during the last call to Nearest. /// public int Visited => _visitedCount; /// /// Returns the squared distance between the query point and the nearest point found by the last call to Nearest. /// Returns double.PositiveInfinity if Nearest hasn't been called or the tree is empty. /// public double DistanceSquared => _bestNode != null ? _bestDistSq : double.PositiveInfinity; /// /// Returns the Euclidean distance between the query point and the nearest point found by the last call to Nearest. /// Returns double.PositiveInfinity if Nearest hasn't been called or the tree is empty. /// public double Distance => _bestNode != null ? Math.Sqrt(_bestDistSq) : double.PositiveInfinity; /// /// Finds the nearest point in the tree to the given point. /// /// The query point. /// The nearest point found in the tree. /// Thrown if the tree is empty. /// Thrown if the query point has different dimensions than the tree. public Point Nearest(Point point) { if (_root == null) throw new InvalidOperationException("Cannot search an empty tree."); if (point.Dimensions != _dimensions) throw new ArgumentException($"Query point dimension ({point.Dimensions}) must match tree dimension ({_dimensions})."); // Reset search state _bestNode = null; _bestDistSq = double.MaxValue; // Use MaxValue for initial comparison _visitedCount = 0; // Start recursive search Nearest(_root, point, 0); // _bestNode should not be null if _root wasn't null return _bestNode.Point; } // --- Recursive Nearest Neighbor Search --- private void Nearest(Node node, Point point, int index) { if (node == null) return; _visitedCount++; double dSq = node.DistanceSquared(point); // If this node is better than the current best, update best if (_bestNode == null || dSq < _bestDistSq) { _bestDistSq = dSq; _bestNode = node; } // Perfect match found, can stop (early exit) if (_bestDistSq == 0) return; // Determine difference along the splitting dimension // Convert coordinates to double for the difference calculation double dx = Convert.ToDouble(node.Get(index)) - Convert.ToDouble(point.Get(index)); // Cycle dimension int nextIndex = (index + 1) % _dimensions; // Decide which subtree to visit first (the one containing the point) Node nearerNode = dx > 0 ? node.Left : node.Right; Node furtherNode = dx > 0 ? node.Right : node.Left; // Search the nearer subtree first Nearest(nearerNode, point, nextIndex); // Pruning: Check if the hypersphere crosses the splitting plane. // If dx^2 >= bestDistSq, the other subtree cannot contain a closer point. if (dx * dx >= _bestDistSq) { return; // Prune the further subtree } // Hypersphere crosses the plane, search the further subtree Nearest(furtherNode, point, nextIndex); } } //----------------------------------------------------------------------------- // Example Usage & Testing //----------------------------------------------------------------------------- public class Program { static void TestWikipedia() { Console.WriteLine("Wikipedia example data:"); var points = new Point[] { new Point(2, 3), new Point(5, 4), new Point(9, 6), new Point(4, 7), new Point(8, 1), new Point(7, 2) }; var tree = new KdTree(points); var queryPoint = new Point(9, 2); Point nearest = tree.Nearest(queryPoint); Console.WriteLine($"Query point: {queryPoint}"); Console.WriteLine($"Nearest point: {nearest}"); Console.WriteLine($"Distance: {tree.Distance:F6}"); // Format distance Console.WriteLine($"Nodes visited: {tree.Visited}"); Console.WriteLine("------------------------------------"); } // Simple random point generator for 3D doubles private static Random _random = new Random(); public static Point RandomPointGenerator(double min, double max) { double range = max - min; double x = min + _random.NextDouble() * range; double y = min + _random.NextDouble() * range; double z = min + _random.NextDouble() * range; return new Point(x, y, z); } static void TestRandom(int count) { Console.WriteLine($"Random data ({count} points):"); // Use the generator constructor var tree = new KdTree(() => RandomPointGenerator(0, 1), count); // Generate a random query point var queryPoint = RandomPointGenerator(0, 1); Point nearest = tree.Nearest(queryPoint); Console.WriteLine($"Query point: {queryPoint}"); Console.WriteLine($"Nearest point: {nearest}"); Console.WriteLine($"Distance: {tree.Distance:F6}"); Console.WriteLine($"Nodes visited: {tree.Visited}"); Console.WriteLine("------------------------------------"); } public static void Main(string[] args) { try { TestWikipedia(); TestRandom(1000); TestRandom(100000); // Reduced count for faster C# demo // TestRandom(1000000); // Can take longer in C# due to sorting overhead vs nth_element } catch (Exception e) { Console.ForegroundColor = ConsoleColor.Red; Console.Error.WriteLine($"An error occurred: {e.Message}"); Console.Error.WriteLine(e.StackTrace); Console.ResetColor(); } } }