文章摘要
这篇文章通过机场安检排队的生动比喻,形象解释了扫描线算法在碰撞检测中的应用。文中将区间检测类比为安检流程:扫描线移动时,人员进出安检区(活动集)触发碰撞检查。作者提供了详细的算法实现步骤,包括事件点排序、活动集维护和碰撞判断,并给出了C#代码示例。该算法通过只关注当前重叠区间,将复杂度从O(n²)优化为O(nlogn),能高效检测一维区间碰撞。文章还指出该方法可扩展到二维场景,具有实用性和扩展性。整体讲解深入浅出,结合生活实例使技术概念更易理解。
一、生活化故事:排队过安检
想象你在机场,大家都要过安检。每个人都有一个“占地范围”(比如背着大包的旅客更宽)。安检门口有一条“扫描线”,从左到右慢慢移动。每当一个人“左肩”到达扫描线时,他进入安检区(活动集);当“右肩”离开扫描线时,他离开安检区。
安检员只需要关注当前在安检区的人,看看他们之间会不会撞到(碰撞)。
二、形象场景图解
假设有5个人(A、B、C、D、E),每个人站在不同的位置,每个人的“占地”是一个区间(比如A站在[1,3],B在[2,4],C在[5,6]……)。
- 扫描线从x=0开始,向右移动。
- 每遇到一个人的“左肩”(区间左端点),他进入安检区。
- 每遇到一个人的“右肩”(区间右端点),他离开安检区。
- 每次有新的人进入安检区时,只需要检查他和安检区里其他人是否有重叠(即是否碰撞)。
三、算法实现过程(配合故事)
1. 事件点准备
- 把每个人的“左肩”和“右肩”都记下来,形成事件点。
- 事件点按x坐标从小到大排序。
- 每个事件点记录是“进入”还是“离开”。
比如:
- A:[1,3],事件点(1,进入A),(3,离开A)
- B:[2,4],事件点(2,进入B),(4,离开B)
- C:[5,6],事件点(5,进入C),(6,离开C)
- 排序后:1(进A), 2(进B), 3(出A), 4(出B), 5(进C), 6(出C)
2. 扫描线移动
- 扫描线从最小x开始,依次处理每个事件点。
进入事件(某人进入安检区):
- 检查他和安检区里所有人是否有重叠(即区间是否有交集)。
- 如果有重叠,说明这两个人可能撞到(碰撞)。
- 把他加入安检区(活动集)。
离开事件(某人离开安检区):
- 把他从安检区移除。
3. 只需关注安检区(活动集)
- 任何时刻,只有安检区里的人才有可能互相碰撞。
- 不在安检区的人,和当前的人一定不会碰撞(因为区间不重叠)。
四、动画式流程举例
假设A[1,3],B[2,4],C[5,6]:
- 扫描线到1,A进入安检区。安检区:A
- 扫描线到2,B进入安检区。安检区:A、B
- 检查A和B,发现A[1,3]和B[2,4]有重叠,记录一次碰撞。
- 扫描线到3,A离开安检区。安检区:B
- 扫描线到4,B离开安检区。安检区:空
- 扫描线到5,C进入安检区。安检区:C
- 扫描线到6,C离开安检区。安检区:空
结果: 只检测了A和B,没必要检测A和C、B和C,因为它们的区间不重叠。
五、总结一句话
扫描线优化碰撞检测就像安检门口的排队:只关注当前在门口的人,谁新进来就和门口的人打个招呼(检查碰撞),谁走了就不用再管他。这样既快又不会漏掉任何可能的碰撞。
下面我用C#给你写一个一维区间碰撞检测的扫描线算法实现。你可以很容易地扩展到二维(比如矩形的x轴投影),原理是一样的。
1. 定义区间对象
public class Interval
{
public int Id; // 标识
public int Left; // 区间左端点
public int Right; // 区间右端点
public Interval(int id, int left, int right)
{
Id = id;
Left = left;
Right = right;
}
}
2. 定义事件点
public class Event
{
public int X; // 事件点的x坐标
public bool IsEnter; // true=进入,false=离开
public Interval Interval;
public Event(int x, bool isEnter, Interval interval)
{
X = x;
IsEnter = isEnter;
Interval = interval;
}
}
3. 扫描线主流程
using System;
using System.Collections.Generic;
using System.Linq;
public class SweepLineCollision
{
public static void Main()
{
// 示例区间
var intervals = new List<Interval>
{
new Interval(1, 1, 3),
new Interval(2, 2, 4),
new Interval(3, 5, 6)
};
// 生成事件点
var events = new List<Event>();
foreach (var interval in intervals)
{
events.Add(new Event(interval.Left, true, interval)); // 进入事件
events.Add(new Event(interval.Right, false, interval)); // 离开事件
}
// 按x坐标排序,进入事件优先于离开事件
events = events.OrderBy(e => e.X)
.ThenBy(e => !e.IsEnter)
.ToList();
// 活动集
var activeSet = new List<Interval>();
// 记录碰撞对
var collisions = new List<(int, int)>();
// 扫描线处理
foreach (var ev in events)
{
if (ev.IsEnter)
{
// 检查与活动集中的所有区间是否重叠
foreach (var other in activeSet)
{
// 只要区间有重叠就算碰撞
if (ev.Interval.Left <= other.Right && ev.Interval.Right >= other.Left)
{
collisions.Add((ev.Interval.Id, other.Id));
Console.WriteLine($"Collision: {ev.Interval.Id} <-> {other.Id}");
}
}
activeSet.Add(ev.Interval);
}
else
{
activeSet.Remove(ev.Interval);
}
}
// 输出所有碰撞对
Console.WriteLine("All Collisions:");
foreach (var pair in collisions)
{
Console.WriteLine($"{pair.Item1} <-> {pair.Item2}");
}
}
}
4. 运行结果
对于示例区间:
- A: [1,3]
- B: [2,4]
- C: [5,6]
输出:
Collision: 2 <-> 1
All Collisions:
2 <-> 1
说明只有A和B发生了碰撞,A和C、B和C没有重叠。
5. 扩展到二维
如果是二维矩形,只需先按x轴做扫描线,活动集里再判断y轴区间是否重叠即可。
6. 总结
- 事件点排序,活动集维护,进入时检查碰撞,离开时移除。
- 只需关注活动集,效率远高于暴力O(n²)。
- 适合大量物体的碰撞检测优化。