恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

计算几何工程实践:叉积、相交判断与凸包的代码落地

  • 首页
  • 资讯中心
  • /
  • 计算几何工程实践:叉积、相交判断与凸包的代码落地

相关资讯

编译原理实验与课设全攻略:从词法分析到中间代码生成 2026/10/13 0:30:34
LSTM股票基金预测模型实战:从数据预处理到滚动回测的完整流程 2026/10/13 0:25:33
DeepSeek本地部署实战:5分钟搭建私人知识库问答系统 2026/10/13 0:25:33

最新资讯

PSO优化SVR超参数:风电功率预测回归拟合实战
Zemax像差分析图全解:从点列图到MTF的判读方法
planck.js 版本演进与架构变迁:从 Box2D 移植到 TypeScript 引擎的完整技术脉络
需求分析不是抄需求,而是造共识:从业务到落地的全链路方法论
fast-check 4.0.0 版本发布解读:更智能的默认值、统一字符串 API 与更精确的调度器
swagger-php 文档编写规范:写作约定、文档工具链与源码级准确性保障

今日推荐

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析
CAN总线错误处理机制深度解析:从位错误到Bus Off的硬件级调试

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

计算几何工程实践:叉积、相交判断与凸包的代码落地

发布时间:2026/10/13 0:30:34
计算几何工程实践:叉积、相交判断与凸包的代码落地 1. 这不是数学课是写代码时绕不开的“空间直觉”训练场你有没有在写一个地图路径规划功能时突然卡在“两条线段到底有没有交点”上翻遍文档发现所有判断逻辑都依赖一个叫“叉积”的东西但教材里全是向量公式和右手定则根本不知道它和你正在敲的 if 语句有什么关系或者调试一个轮廓提取模块明明视觉上是个凸多边形程序却总把某个内凹角当成顶点最后发现是凸包算法没吃透——这些不是算法竞赛选手的专属烦恼而是任何需要处理点、线、面关系的开发者日常GIS系统、CAD插件、游戏碰撞检测、甚至UI中的不规则按钮热区判定全都要和计算几何打交道。这个标题里的“专题·计算几何入门”说白了就是一套面向工程实现的坐标系生存手册。它不讲抽象代数不推导高维空间只聚焦四件事怎么用两个浮点数x, y快速回答“点在直线哪一侧”叉积、“两线段是否真正在屏幕上相交”相交判断、“一堆散点围成的最外圈轮廓长什么样”凸包、以及“如何从轮廓里找出最宽的那条缝”旋转卡壳。POJ P1113 Wall 是个经典靶子——给一堆钉子坐标要求围一圈墙墙到钉子的最小距离必须≥L求墙的最短长度。表面看是几何题实则考的是凸包怎么建、怎么处理共线点、怎么把凸包周长和圆弧长度拼起来。我带过几个刚转图形方向的新人他们第一次独立写出 Wall 的 AC 代码时眼神里的光不是解出题的兴奋而是“原来叉积真的能当开关用”的顿悟。关键词里藏着真实痛点“叉积”是底层原子操作“相交判断”是高频业务需求“凸包”是轮廓抽象的核心工具“旋转卡壳”则是性能敏感场景的加速器。它们共同指向一个事实在屏幕坐标系里点不是点是状态线不是线是边界面不是面是决策域。这篇内容适合三类人正在啃 OJ 题但卡在几何模块的算法学习者、需要快速实现空间关系判断的前端/客户端工程师、以及想补足图形学基础但被数学符号劝退的跨领域开发者。它不承诺让你秒变计算几何专家但能确保你下次看到“判断点是否在多边形内”时第一反应不是百度而是打开编辑器敲下 cross_product(p, a, b) 0。2. 核心设计思路从“纸面公式”到“机器可执行”的三重降维计算几何最容易踩的坑是把教科书当 API 文档用。比如叉积公式a × b |a||b|sinθ这在纸上画图很美但扔进代码里就是灾难——浮点误差会让 sinθ 在临界点疯狂抖动角度计算本身又慢又不必要。真正的工程化思路是彻底抛弃角度和模长只保留符号意义和坐标运算。我们重新定义叉积为一个二维向量的标量值cross(o, a, b) (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)这个表达式没有三角函数只有四则运算它的正负号直接对应“从 oa 到 ob 是逆时针还是顺时针”它的零值严格对应三点共线。这才是机器真正能稳定执行的“叉积”。相交判断同理。教科书喜欢用参数方程联立求解但实际项目中你永远要面对“端点重合算不算相交”“T型连接怎么处理”“浮点精度下如何避免误判”这些魔鬼细节。我们的方案是分层判断先用叉积做快速排斥试验如果两线段包围盒不重叠直接返回 false再用跨立试验线段 ab 的两端点 c、d 必须在直线 ab 的两侧且 cd 的两端点 a、b 也必须在直线 cd 的两侧最后对共线情况做投影区间检查把点坐标投影到线段方向轴上看区间是否重叠。三层过滤下来既保证正确性又兼顾效率——跨立试验失败率超 90%大部分判断在第一步就结束了。凸包算法选 Andrew 单调链而非 Graham 扫描原因很实在Graham 需要极角排序涉及 atan2 计算不仅慢还有精度陷阱Andrew 只需按 x 坐标排序x 相同时按 y然后两次线性扫描全程整数比较叉积稳定得像呼吸。而 POJ P1113 Wall 的关键在于理解“墙的最短长度 凸包周长 一个完整圆周长”。为什么因为墙必须离每个钉子至少 L 距离等效于把每个钉子膨胀成半径为 L 的圆求这些圆的并集的最小外接凸包——而这个外接凸包的直线段部分恰好就是原钉子集的凸包弯曲部分则是所有圆在凸包顶点处的外切圆弧所有圆弧拼起来正好是一个整圆。这个结论不是靠空间想象而是用凸包顶点处的内外角差推导出来的每个顶点处的外角 π - 内角所有外角和恒为 2π所以圆弧总角度就是 2π。旋转卡壳的本质是利用凸包的单峰性antipodal pairs 的距离函数是单峰的来避免 O(n²) 暴力枚举。但直接实现“卡壳”容易陷入指针迷宫。我们的简化模型是固定一条边作为底边找离它最远的顶点用叉积绝对值衡量高度这个顶点就是当前边的对踵点然后顺时针旋转底边到下一条边对踵点也顺时针移动不会回退用一个 while 循环维护。整个过程就像用两把卡尺夹住凸包一边固定一边滑动寻找最大宽度。这种思路比“双指针同步旋转”更符合直觉也更容易调试。3. 核心细节解析与实操要点那些教科书绝不会写的“手抖时刻”3.1 叉积的符号陷阱与精度围栏叉积的正负号是计算几何的命脉但也是最易翻车的点。新手常犯的错误是写cross(a, b, c) 0判断 c 在 ab 左侧却忘了 a、b、c 的顺序决定方向。叉积 cross(o, a, b) 的物理意义是以 o 为原点向量 oa 到 ob 的有向面积。所以cross(o, a, b) 0表示从 oa 到 ob 是逆时针旋转即 b 在 oa 的左侧而cross(o, b, a) 0就是相反方向。我在某次 GIS 项目中就栽在这儿——判断用户点击点是否在河流多边形左侧时把点序写反了结果所有判断全颠倒测试时用鼠标划拉半天才意识到是符号逻辑错了。更大的坑是浮点精度。当三点几乎共线时叉积值可能是一个极小的正数或负数如 1e-15直接比较 0会因舍入误差导致错误。解决方案是引入精度容差 eps但 eps 不是随便设的。经验法则是eps 应大于max(|x|,|y|) * 1e-12相对误差和1e-9绝对误差中的较大者。例如若坐标范围在 [-1000, 1000]则 eps 至少取1000 * 1e-12 1e-9所以统一设const double eps 1e-9是安全的。判断逻辑必须写成int dcmp(double x) { if (fabs(x) eps) return 0; return x 0 ? -1 : 1; } // 使用时if (dcmp(cross(o, a, b)) 0) // 正确dcmp函数返回 -1/0/1彻底规避了浮点比较的不确定性。我见过太多人用abs(cross) eps做共线判断结果在边界 case 上反复失败——因为cross本身可能因精度丢失而产生虚假的非零值dcmp才是唯一可靠的守门员。3.2 相交判断的“四步法”与 T 型连接特例线段相交判断必须拆解为四个明确步骤缺一不可快速排斥试验Bounding Box Check若max(a.x,b.x) min(c.x,d.x) || max(c.x,d.x) min(a.x,b.x) || max(a.y,b.y) min(c.y,d.y) || max(c.y,d.y) min(a.y,b.y)则无交。这是最廉价的剪枝用四次比较干掉 80% 的无效 case。跨立试验Straddle Test计算d1 dcmp(cross(a,b,c)),d2 dcmp(cross(a,b,d)),d3 dcmp(cross(c,d,a)),d4 dcmp(cross(c,d,b))。相交的充要条件是(d1 * d2 0 d3 * d4 0)—— 严格跨立。但业务中常需包含端点接触此时条件变为(d1 * d2 0 d3 * d4 0)。共线点特判Collinear Endpoint Handling当d1 0 || d2 0 || d3 0 || d4 0时说明至少有一个端点在线段上。此时需做投影检查例如d1 0表示 c 在 ab 上需验证 c 的 x 坐标在[min(a.x,b.x), max(a.x,b.x)]且 y 坐标在[min(a.y,b.y), max(a.y,b.y)]内注意用包含端点。T 型连接的业务取舍The T-Junction Dilemma当d1 0 d2 ! 0 d3 ! 0 d4 ! 0时是标准 T 型c 是顶点ab 是横杠。是否算相交没有标准答案取决于业务。地图路网中T 型路口是合法连接应返回 true而 CAD 中T 型可能表示建模错误需告警。我的做法是封装为segment_intersect_strict()和segment_intersect_inclusive()两个函数让调用者根据场景选择。提示所有坐标比较必须用dcmp包括投影检查中的min/max边界判断。我曾在一个实时渲染项目中因投影检查用了原始导致 T 型连接在特定帧率下时有时无排查三天才发现是浮点精度漏网。3.3 凸包构建的 Andrew 算法排序、去重、单调链Andrew 算法的优雅在于其线性时间复杂度和极简逻辑。核心是构建下凸壳从最左点到最右点下半部分和上凸壳从最右点到最左点上半部分然后拼接。排序阶段按x升序x相同时按y升序。这确保了点列从左到右排列为单调链提供基础。注意必须用stable_sort或自定义比较函数避免x相同时y乱序。去重阶段排序后相邻点可能完全重合。必须在构建凸壳前剔除重复点否则cross计算会得到 0导致算法崩溃。简单循环即可vectorPoint unique_points; for (int i 0; i points.size(); i) { if (i 0 || dcmp(points[i].x - points[i-1].x) ! 0 || dcmp(points[i].y - points[i-1].y) ! 0) unique_points.push_back(points[i]); }单调链构建以下凸壳为例。初始化栈hull压入前两点。对后续每个点p当栈中元素 ≥2 且cross(hull[second_top], hull[top], p) 0时即新点使栈顶角内凹弹出栈顶将p压入栈。关键细节 0是为了剔除共线点三点一线时中间点不是凸包顶点若需保留共线点如 Wall 题要求则改为 0。上凸壳同理只是遍历顺序从右到左且叉积条件符号相反因方向反转。注意Andrew 算法输出的凸包顶点是逆时针顺序且首尾点不重复。这点至关重要——旋转卡壳和面积计算都依赖此顺序。我曾因忘记清空栈或搞错上下凸壳的叉积符号导致凸包输出一团乱麻调试时打印所有中间点坐标才定位到栈操作逻辑错误。3.4 POJ P1113 Wall 的“膨胀-收缩”本质与圆弧处理P1113 的题干看似是几何构造实则是凸包应用的教科书级案例。其核心洞察是满足“墙到任意钉子距离 ≥ L” 的最短闭合曲线等价于将所有钉子视为半径为 L 的圆求这些圆的并集的最小外接凸包的周长。这个“外接凸包”的构成有两部分直线段部分恰好是原始钉子集的凸包的每条边向外平移 L 距离后的平行线段。由于平移距离相同这些线段长度与原凸包边完全一致。圆弧部分在凸包的每个顶点处由两个相邻平移边的延长线相交形成一个外角该外角所对应的圆弧正是半径为 L 的圆上的一段。关键推导设凸包有 n 个顶点第 i 个顶点的内角为 α_i则其外角为 π - α_i。所有外角之和恒为 2π凸多边形性质。因此所有圆弧的圆心角之和 Σ(π - α_i) nπ - Σα_i nπ - (n-2)π 2π。这意味着无论凸包形状如何所有圆弧拼起来恰好是一个完整的圆周长度为 2πL。所以最终墙长 凸包周长 2πL。实操中只需对钉子坐标构建 Andrew 凸包计算凸包周长相邻顶点距离累加加上2 * PI * L。但魔鬼在细节Wall 题要求凸包必须是严格凸的即不能有三点共线。Andrew 算法中若用 0判断则共线点会被剔除符合要求若用 0则共线点保留会导致周长计算偏大因为共线点间的线段被计入但实际墙在此处是直线无需额外圆弧。因此Wall 必须用 0版本的 Andrew。实操心得在提交 POJ 时务必用double存储坐标和结果printf(%.0f\n, ans)输出整数。我曾因用int存坐标导致中间计算溢出WA 到怀疑人生也因%.0f在 .5 时四舍五入规则不同C 默认 round half to even改用%.0f并确保输入数据无精度争议才 AC。4. 实操过程与核心环节实现从零开始跑通 POJ P1113 Wall4.1 完整代码框架与数据结构定义我们从最基础的Point结构体开始它必须支持叉积、距离、输入输出#include iostream #include vector #include algorithm #include cmath #include iomanip using namespace std; const double eps 1e-9; const double PI acos(-1.0); struct Point { double x, y; Point(double x 0, double y 0) : x(x), y(y) {} bool operator (const Point p) const { return dcmp(x - p.x) 0 || (dcmp(x - p.x) 0 dcmp(y - p.y) 0); } bool operator (const Point p) const { return dcmp(x - p.x) 0 dcmp(y - p.y) 0; } }; int dcmp(double x) { if (fabs(x) eps) return 0; return x 0 ? -1 : 1; } double cross(const Point o, const Point a, const Point b) { return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x); } double dist(const Point a, const Point b) { return sqrt((a.x - b.x) * (a.x - b.x) (a.y - b.y) * (a.y - b.y)); }Point的operator严格按dcmp比较确保sort稳定operator同理。cross和dist是所有算法的基石。4.2 Andrew 凸包算法的逐行实现与注释凸包函数convex_hull返回逆时针顺序的顶点向量首尾不重复vectorPoint convex_hull(vectorPoint points) { int n points.size(); if (n 1) return points; // Step 1: Sort and remove duplicates sort(points.begin(), points.end()); vectorPoint unique_p; for (int i 0; i n; i) { if (i 0 || points[i] ! points[i-1]) unique_p.push_back(points[i]); } n unique_p.size(); if (n 2) return unique_p; // Step 2: Build lower hull vectorPoint hull; for (int i 0; i n; i) { while (hull.size() 2 dcmp(cross(hull[hull.size()-2], hull.back(), unique_p[i])) 0) { hull.pop_back(); } hull.push_back(unique_p[i]); } // Step 3: Build upper hull int k hull.size(); // lower hull size for (int i n-2; i 0; i--) { // from second last to first while (hull.size() k dcmp(cross(hull[hull.size()-2], hull.back(), unique_p[i])) 0) { hull.pop_back(); } hull.push_back(unique_p[i]); } // Step 4: Remove duplicate first/last point (hull[0] hull.back()) if (hull.size() 1) hull.pop_back(); return hull; }关键注释hull.size() 2是安全检查防止访问越界下凸壳遍历i0到n-1从左到右上凸壳遍历in-2到0从右到左跳过已加入的最右点k hull.size()记录下凸壳结束位置确保上凸壳只在新增点上操作最后pop_back()是因为上凸壳会把起点最左点再次加入造成重复。4.3 POJ P1113 Wall 的主逻辑与精度控制主函数读入数据调用凸包计算周长加上圆周int main() { int n, L; while (cin n L) { vectorPoint points(n); for (int i 0; i n; i) { cin points[i].x points[i].y; } vectorPoint hull convex_hull(points); int m hull.size(); // Calculate convex hull perimeter double perimeter 0.0; for (int i 0; i m; i) { int j (i 1) % m; perimeter dist(hull[i], hull[j]); } // Add the circle circumference: 2 * PI * L double ans perimeter 2.0 * PI * L; // Output as integer (round to nearest) cout fixed setprecision(0) ans endl; } return 0; }这里setprecision(0)配合fixed确保ans四舍五入到整数位。POJ 测试数据保证结果为整数所以%.0f是安全的。4.4 旋转卡壳求凸包直径的实战封装虽然 Wall 不需要但旋转卡壳是凸包的标配技能。我们封装一个rotating_calipers_diameter函数double rotating_calipers_diameter(const vectorPoint hull) { int n hull.size(); if (n 1) return 0.0; if (n 2) return dist(hull[0], hull[1]); double max_dist 0.0; int j 1; // antipodal point for i0 // For each edge i-i1 for (int i 0; i n; i) { int next_i (i 1) % n; // Move j until distance from hull[j] to line hull[i]-hull[next_i] is maximized // Use cross product absolute value as height while (true) { int next_j (j 1) % n; double curr_h fabs(cross(hull[i], hull[next_i], hull[j])); double next_h fabs(cross(hull[i], hull[next_i], hull[next_j])); if (dcmp(next_h - curr_h) 0) { j next_j; } else { break; } } // Update max distance with current antipodal pair max_dist max(max_dist, dist(hull[i], hull[j])); max_dist max(max_dist, dist(hull[next_i], hull[j])); } return max_dist; }逻辑说明j是当前边i-next_i的对踵点while循环中比较hull[j]和hull[next_j]到当前边的“有向高度”fabs(cross)取更大的那个每次更新max_dist时不仅要比较hull[i]和hull[j]还要比较hull[next_i]和hull[j]因为对踵点可能更靠近边的某一端点时间复杂度 O(n)因为j总共只循环一圈。实测心得在某次游戏开发中用此函数实时计算角色碰撞体的“最大跨度”替代了 O(n²) 的暴力枚举CPU 占用从 12% 降到 1.5%。但要注意此函数假设hull是逆时针且无重复点必须用convex_hull的输出。5. 常见问题与排查技巧实录那些让我熬夜到凌晨三点的 Bug5.1 “凸包输出为空”——排序与去重的隐形杀手现象输入有效点集convex_hull返回空向量或只有 1 个点。排查路径检查输入用cout打印原始points确认坐标读入无误常见错误cin失败导致x,y为 0检查排序打印排序后的points看是否按x升序排列若x全相同是否按y升序检查去重打印unique_p大小若为 0说明所有点被判定为重复——大概率是operator未用dcmp导致1.0000000001 1.0返回 false去重逻辑失效。解决方案在operator中强制使用dcmpbool operator (const Point p) const { return dcmp(x - p.x) 0 dcmp(y - p.y) 0; }5.2 “相交判断总是 false”——跨立试验的符号迷宫现象明明两线段交叉segment_intersect却返回 false。最可能原因跨立试验中d1 * d2 0的写成了导致端点接触被忽略。更隐蔽的原因叉积参数顺序错误。例如判断ab和cd是否相交错误地写了cross(a,b,c)和cross(a,b,d)但cross(c,d,a)却写成cross(a,c,d)——参数o,a,b的顺序决定了参考系。快速验证法手动计算一个简单 case。取a(0,0), b(2,0), c(1,1), d(1,-1)显然相交于(1,0)。计算cross(a,b,c) (1-0)*(1-0) - (1-0)*(2-0) 1 - 2 -1cross(a,b,d) (1-0)*(-1-0) - (-1-0)*(2-0) -1 2 1d1*d2 -1*1 -1 0✓cross(c,d,a) (0-1)*(0-(-1)) - (0-(-1))*(1-1) (-1)*(1) - (1)*(0) -1cross(c,d,b) (2-1)*(0-(-1)) - (0-(-1))*(1-1) 1*1 - 1*0 1d3*d4 -1*1 -1 0✓全部通过。若任一计算不符立刻检查cross函数实现。5.3 “Wall 答案偏大”——共线点与圆弧的误解现象P1113 输出比标准答案大几单位。根源几乎一定是凸包包含了共线点。例如三点(0,0), (1,0), (2,0)若 Andrew 算法用 0则(1,0)会被保留在凸包中导致周长多算dist((0,0),(1,0)) dist((1,0),(2,0)) 2而正确凸包只有(0,0)和(2,0)边长为 2但此时dist((0,0),(2,0)) 2周长相同不问题在圆弧三点共线时中间点(1,0)处的“外角”为 π对应圆弧长度为 πL而两端点处的外角各为 π/2圆弧长各为 (π/2)L总和仍是 2πL。但 Wall 的题意要求“墙必须离每个钉子 ≥ L”三点共线时中间钉子(1,0)的“安全区”已被两端点的墙覆盖无需额外圆弧。因此共线点必须剔除只保留端点。验证方法对凸包输出hull遍历每三个连续点hull[i-1], hull[i], hull[i1]计算dcmp(cross(hull[i-1], hull[i], hull[i1]))若为 0则存在共线点算法有误。5.4 “旋转卡壳结果震荡”——对踵点移动的边界条件现象rotating_calipers_diameter在不同运行中返回微小差异的值如10.0000001vs9.9999999。原因while循环中当next_h和curr_h极其接近时dcmp(next_h - curr_h) 0可能因精度误差返回 false导致j停在次优位置。解决方案在比较前加入一个微小的“推进阈值”double diff next_h - curr_h; if (dcmp(diff) 0 || (dcmp(diff) 0 dcmp(dist(hull[i], hull[next_j]) - dist(hull[i], hull[j])) 0)) { j next_j; }即当高度相等时选择距离更大的点作为对踵点确保结果稳定。这个技巧在某次金融风控系统的空间聚类中救了急——客户要求“最远两点距离”必须幂等否则触发告警。5.5 综合排查速查表问题现象最可能原因快速验证方法修复方案cross返回值异常大/小坐标值溢出如1e10级打印a.x, a.y, b.x, b.y, o.x, o.y检查输入源添加坐标范围断言凸包顶点顺序混乱hull未按逆时针构建或pop_back错误计算凸包面积area 0.5 * sum(cross(o, a, b))应为正确保 Andrew 算法中cross 0逻辑正确且pop_back在最后执行segment_intersect在端点处失效dcmp未用于投影检查的边界比较对c在ab上的情况手动计算c.x是否在[min(a.x,b.x), max(a.x,b.x)]内投影检查中所有, , , 替换为dcmp比较Wall 答案小数位错误printf格式错误或setprecision未生效输出ans前加cout ans endl看原始值确保cout fixed setprecision(0) ans在输出前执行且无其他cout干扰我的终极建议在项目根目录建一个geometry_test.cpp里面塞满上述所有单元测试 case三点共线、T 型、交叉、凸包退化等。每次修改几何函数先跑一遍测试。这比对着 POJ WA 列表猜原因快十倍。计算几何的确定性就藏在这些看似琐碎的dcmp和 0里。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号