空间哈希与拓扑缓存

需求说明

只做基础空间哈希桶(Floor分桶) ,构建索引结构;不做并查集、不做聚类、不找重复点

目标:给定一批PipePointInfo,建立索引 Dictionary<(long bx, long by), List<PipePointInfo>>

后续用途:管线端点匹配管点时,用该索引代替全量双重循环。

核心规则

  1. 分桶:Floor,桶边长 = tolerance
  2. Key:值元组 (long bx, long by)
  3. 查询邻近候选:遍历3×3邻域桶,取出候选管点,再做真实DistanceTo校验

1、基础空间哈希桶构建函数(纯索引,无业务逻辑)

复制代码
/// <summary>
/// 构建管点空间哈希索引(Floor分桶)
/// </summary>
/// <param name="allPoints">全部管点集合</param>
/// <param name="tolerance">桶边长=拓扑容差</param>
/// <returns>桶索引:网格编号 -> 该网格内所有管点</returns>
public static Dictionary<(long bx, long by), List<PipePointInfo>> BuildPointSpatialIndex(
    List<PipePointInfo> allPoints,
    double tolerance)
{
    var index = new Dictionary<(long bx, long by), List<PipePointInfo>>();
    double invTol = 1.0 / tolerance;

    foreach (var pt in allPoints)
    {
        // Floor 计算网格编号
        long bx = (long)Math.Floor(pt.Position.X * invTol);
        long by = (long)Math.Floor(pt.Position.Y * invTol);
        var cellKey = (bx, by);

        if (!index.ContainsKey(cellKey))
        {
            index[cellKey] = new List<PipePointInfo>();
        }
        index[cellKey].Add(pt);
    }
    return index;
}

返回数据结构

复制代码
Dictionary<(long bx, long by), List<PipePointInfo>> spatialIndex;
  • Key:(bx,by) 网格单元编号
  • Value:落在该网格内所有PipePointInfo列表

2、配套查询方法:根据一个空间点,获取容差范围内候选管点

查询逻辑:计算目标点所在网格,遍历3×3邻域桶,收集候选,再距离过滤。

复制代码
/// <summary>
/// 根据坐标,从空间索引查找容差范围内所有管点候选
/// </summary>
public static List<PipePointInfo> QueryNearPoints(
    Point3d targetPt,
    Dictionary<(long bx, long by), List<PipePointInfo>> spatialIndex,
    double tolerance)
{
    var result = new List<PipePointInfo>();
    double invTol = 1.0 / tolerance;

    long bx0 = (long)Math.Floor(targetPt.X * invTol);
    long by0 = (long)Math.Floor(targetPt.Y * invTol);

    // 遍历3×3邻域桶
    for (int dx = -1; dx <= 1; dx++)
    {
        for (int dy = -1; dy <= 1; dy++)
        {
            var cell = (bx0 + dx, by0 + dy);
            if (spatialIndex.TryGetValue(cell, out var candidates))
            {
                foreach (var pt in candidates)
                {
                    // 精确距离校验(桶只是筛选候选,不能替代距离判断)
                    if (targetPt.DistanceTo(pt.Position) <= tolerance)
                    {
                        result.Add(pt);
                    }
                }
            }
        }
    }
    return result;
}

3、改造你原来【管线-管点拓扑匹配】代码(替换双重循环)

原来:foreach(line) foreach(pt) O(M*N)暴力循环

现在:先构建索引,每条管线的起点、终点调用QueryNearPoints

复制代码
double tol = ThirdPartyCommonTools.PipeTopologyConfiguration.Tolerance;
// 1. 一次性构建空间索引
var spatialIndex = BuildPointSpatialIndex(allPoints, tol);

// 2. 遍历管线,利用索引匹配管点
foreach (var line in allLines)
{
    // 匹配起点附近全部管点
    var startCandidates = QueryNearPoints(line.StartPoint, spatialIndex, tol);
    foreach (var pt in startCandidates)
    {
        line.StartPointIds.Add(pt.Id);
        pt.ConnectedLines.Add(line.Id);
    }

    // 匹配终点附近全部管点
    var endCandidates = QueryNearPoints(line.EndPoint, spatialIndex, tol);
    foreach (var pt in endCandidates)
    {
        line.EndPointIds.Add(pt.Id);
        pt.ConnectedLines.Add(line.Id);
    }
}

4、结构要点总结(便于你理解记忆)

  1. 索引结构

    Dictionary<(long bx, long by), List>

  2. 分桶公式(固定Floor,杜绝Round)

    invTol = 1.0 / tolerance
    bx = Floor(X * invTol)
    by = Floor(Y * invTol)

  3. 查询范式
    目标点算出(bx0,by0),循环dx:-1,0,1dy:-1,0,1读取邻桶候选,候选必须再DistanceTo校验

5、兼容老.NET Framework(不能使用ValueTuple时的替换版本)

把键从 (long bx,long by) 替换为 Tuple<long,long>

复制代码
Dictionary<Tuple<long, long>, List<PipePointInfo>> index
var cellKey = Tuple.Create(bx, by);

查询部分同步修改即可。

先给核心结论

理解大体方向正确,但不能直接一刀切删掉 ConnectedLinesStartPointIdsEndPointIds,这里存在两种不同设计模式,不能互相完全替代:

1)预缓存拓扑模式(你现在代码)

实体内部保存双向关联ID列表 ;优点:已知实体ID,直接读取关联对象,O(1) ;代价:构建阶段需要暴力匹配。

2)仅保留空间哈希索引、不缓存拓扑(惰性模式)

实体只保留自身几何信息,不存任何相连对象ID;需要关联关系时,现场用空间索引做邻近搜索、距离校验;优点:不需要一次性构建庞大拓扑;代价:每次查询都要执行一次邻近检索。

分开把关键点讲清楚

1、空间哈希桶解决了什么?

它只解决一件事:根据坐标快速拿到周边候选实体,消除O(M*N)全量遍历

空间哈希 ≈ 空间上的候选筛选器;

它不能根据 ObjectId 反向查找关联,它的查询入口永远是【坐标Point3d】,不是ObjectId。

2、关键矛盾点(你推理里缺失的一环)

  • 场景A:我现在手上有一个实体ID/实体对象(PipePointInfo),我要找和它相连的管线

    • 如果你删掉 ConnectedLines
      你只能拿 pt.Position → 空间哈希邻域搜索 → 筛选管线 → 距离校验;每次调用都要重新检索计算
    • 如果你保留 ConnectedLines
      直接读取List,立刻拿到管线ID;不需要任何空间计算。
  • 场景B:我手上只有一个坐标点,想查找附近管点

    此时只能用空间哈希,缓存的拓扑集合派不上用场。

3、两种架构选型对比(管网开发二选一)

方案一:预构建双向拓扑(原有模式,可搭配空间哈希优化构建过程)

PipePointInfo.ConnectedLines

PipeLineInfo.StartPointIds / EndPointIds

  • 构建阶段:利用空间哈希替代双层暴力循环(就是我们前面写的代码),快速算出关联关系,存入集合;
  • 使用阶段:任意实体,直接读取关联ID;连通性遍历、广度优先搜索、环路查找非常顺畅;
  • 缺点:实体移动、删除后缓存失效,需要整体重建拓扑。

✅ 改良方案:构建拓扑的算法升级(空间哈希),但是拓扑缓存字段继续保留

这是工程里最常用方案:只优化"建表速度",不改动上层业务访问方式。

方案二:纯惰性查询,不缓存任何拓扑(你设想的方案)

彻底删除 ConnectedLinesStartPointIdsEndPointIds

实体类只保留自身属性:Id、图层、坐标、块名。

全局只维护一份空间哈希索引

业务逻辑变化:

  • 需要"管点相连管线":拿管点坐标 → QueryNearPoints,现场匹配;

  • 需要"管线两端关联管点":拿起点、终点坐标,现场邻域搜索;

  • 优点:数据结构轻量化;不存在拓扑缓存不一致问题;适合交互式频繁编辑场景;

  • 致命短板:连通性遍历性能差

    举例子:广度优先遍历管网连通区域,一个管点会被反复访问多次,每次都重复执行邻域搜索+距离计算,大量重复运算。

4、一句话纠正你的推论

❌ 错误理解:有空间哈希桶 → 就可以删除实体上所有拓扑关联集合

✅ 正确理解:

空间哈希是【构建关联关系的加速工具】,不等同于【关联关系本身】。

空间哈希解决「如何快速找到邻居」;

实体上的ID集合解决「保存已经找到的邻居结果」。

5、给你基于管网工具开发的选型建议

结合你当前业务:重复管点筛查、管网拓扑校验、批量数据处理(偏向一次性批量分析,不是交互式实时编辑)

推荐采用折中路线:

  1. 保留 ConnectedLines / StartPointIds / EndPointIds 拓扑缓存字段;
  2. 把原来O(M*N)双层循环替换为空间哈希版本做拓扑构建;
  3. 上层遍历连通性、重复点分组的业务代码完全不用改动,只提速底层构建流程。

如果你未来开发交互式命令(选中实体即时查询相连管线、移动实体实时更新),再考虑切换到纯惰性、无拓扑缓存的架构。

举个直观类比帮助记忆

  • 暴力双层循环:挨家挨户全城找人;
  • 空间哈希:建立小区楼栋门牌索引,只搜索目标周边楼栋(找人的搜索工具);
  • ConnectedLines集合:找到邻居之后,把邻居电话写在自家本子上(查找结果缓存 );
    有楼栋索引 ≠ 每家本子上不需要记邻居电话;想快速打电话,还是依赖本子。

这个CAD管网拓扑的分层设计逻辑比较绕,工作任务模式可以帮你整理两套架构完整样板代码和切换条件,要不要用它继续?

相关推荐
明志数科41 分钟前
具身智能真机采集工程实践:5类高频失效模式与规避清单
人工智能·算法·机器学习
大熊背2 小时前
IspPipeline色相旋转模块实现
人工智能·算法·计算机视觉
爱学习的小白柏2 小时前
同城双活的核心不是双活,是逼着数据别出机房
大数据·人工智能·算法·langchain·ai编程
鹿角片ljp2 小时前
LeetCode 31:下一个排列|右找小,右找大,交换,右反转
java·数据结构·算法
橘子汽水1683 小时前
Leetcode 198,118打家劫舍,杨辉三角
算法·leetcode
LuminousCPP3 小时前
数据结构-排序(三):快速排序进阶|挖坑法、双指针交换与手动栈非递归实现
c语言·数据结构·笔记·算法·排序算法
数据知道4 小时前
国密算法实战——SM2/SM3/SM4 在国产系统中的应用
网络·算法·安全·网络安全·密码学·哈希算法
zander2585 小时前
LeetCode 128. 最长连续序列
数据结构·算法
linx2955 小时前
单元四 · 对称认知·上:内存与指针
c语言·开发语言·数据结构·嵌入式硬件·算法