给定 n 条线段 (p1, q1), (p2, q2), … (pn, qn),判断给定的线段是否彼此相交。
例子:
输入:{{1, 5}, {4, 5}}, {{2, 5}, {10, 1}},{{3, 2}, {10, 3}},{{6, 4}, {9, 4}},{{7, 1}, {8, 1}}
输出:2
解释:线 {{1, 5}, {4, 5}}, {{2, 5}, {10, 1}} 相交,线 {{2, 5}, {10, 1}},{{3, 2}, {10, 3}} 也相交。
我们已经讨论了检测两个给定线段是否相交的问题。
c++ https://blog.youkuaiyun.com/hefeng_aspnet/article/details/141713655
java https://blog.youkuaiyun.com/hefeng_aspnet/article/details/141713762
python https://blog.youkuaiyun.com/hefeng_aspnet/article/details/141714389
C# https://blog.youkuaiyun.com/hefeng_aspnet/article/details/141714420
javascript https://blog.youkuaiyun.com/hefeng_aspnet/article/details/141714442
在这篇文章中,我们扩展了这个问题。这里我们给出了 n 条线段,我们需要找出任意两个线段是否相交。
朴素算法解决这个问题的一个朴素解决方案是检查每一对线并检查该对是否相交。我们可以在 O(1) 时间内检查两个线段。因此,这种方法需要 O(n 2 )。
扫描线算法:我们可以使用扫描线算法在O(nLogn)时间内解决这个问题。该算法首先从左到右沿 x 轴对端点进行排序,然后从左到右穿过所有点的垂直线并检查是否相交。以下是详细步骤。
1)假设有 n 条给定的线。必须有 2n 个端点来表示 n 条线。根据 x 坐标对所有点进行排序。排序时保留一个标志以指示该点是其线的左点还是右点。
2)从最左边的点开始。对每个点执行以下操作
a)如果当前点是其线段的左点,则检查其线段与其正上方和正下方的线段的交点。并将其线添加到活动线段(可见左端点但未可见右端点的线段)。请注意,我们只考虑那些仍然处于活动状态的邻居。...
b )如果当前点是右点,则从活动列表中删除其线段并检查其两个活动邻居(正上方和正下方的点)是否相互相交。
步骤2就像从最左边的点到最右边的点从所有点传递一条垂直线。这就是为什么该算法被称为扫描线算法。扫描线技术在许多其他几何算法中都很有用,例如计算2D Voronoi 图。
应该使用什么数据结构才能有效实现?
在步骤2中,我们需要存储所有活动线段。我们需要高效地完成以下操作:
a) 插入新线段
b) 删除线段
c) 根据 y 坐标值查找前任和后继
上述操作的明显选择是自平衡二叉搜索树,如 AVL 树、红黑树。使用自平衡 BST,我们可以在 O(Logn) 时间内完成上述所有操作。
此外,在步骤 1 中,我们可以使用最小堆数据结构,而不是排序。构建最小堆需要 O(n) 时间,每次提取最小值操作都需要 O(Logn) 时间(参见此文档)。
伪代码:
以下伪代码不使用堆。它只是对数组进行排序:
sweepLineIntersection(Points[0..2n-1]):
1. Sort Points[] from left to right (according to x coordinate)
2. Create an empty Self-Balancing BST T. It will contain
all active line Segments ordered by y coordinate.
// Process all 2n points
3. for i = 0 to 2n-1
// If this point is left end of its line
if (Points[i].isLeft)
T.insert(Points[i].line()) // Insert into the tree
// Check if this points intersects with its predecessor and successor
if ( doIntersect(Points[i].line(), T.pred(Points[i].line()) )
return true
if ( doIntersect(Points[i].line(), T.succ(Points[i].line()) )
return true
else // If it's a right end of its line
// Check if its predecessor and successor intersect with each other
if ( doIntersect(T.pred(Points[i].line(), T.succ(Points[i].line()))
return true
T.delete(Points[i].line()) // Delete from tree
4. return False
示例:
让我们考虑以下取自此处的示例。有 5 条线段1、2、3、4和5。绿色虚线表示扫描线。
示例解释任何相交的线段
按左端点对线段进行排序
将垂直扫描线穿过各个部分
当击中左端点时,将线段放入 RB 树中,按 y 值排序
检查新线段与 ABOVE 之间的交集以及新线段与 BELOW 之间的交集
当击中右端点时从 RB 树中删除线段
检查 ABOVE 和 BELOW 之间的交点
如果两条线段相交,它们最终会成为邻居
当添加其中一个段时,邻居将首先检查交叉点
当另一个段被删除时,邻居,第二次检查将捕捉到交叉点
相交线最终将与扫描线相交。
无误报
假设没有垂直线,没有三条线在同一点相交
扫描线:RB 树包含 1
扫描线:RB 树包含 2、1,无相交
扫描线:RB 树包含 2、1、3,无相交
扫描线:RB 树包含 2、3,相交 扫描线:RB 树包含 2、4、3
,无相交 扫描
线:RB 树包含 2、4、3、5,无相交 扫描
线:RB 树包含 2、4、3,无相交
扫描线:RB 树包含 2、3,相交
扫描线:RB 树为空
For n segments: sort segments n lg n n RB-Inserts n lg n n RB-Deletes n lg n O(1) comparison
参考链接:Any intersecting segments
回到示例:
以下是算法遵循的步骤。从左到右的所有点都逐一处理。我们维护一个自平衡二叉搜索树。
处理线段 1 的左端点:将 1 插入树中。树包含1。无交点。
处理线段 2 的左端点: 检查1和2的交点。将2插入树中。找到 1&2 的交点(“请注意,上面的伪代码在此时返回”)。我们可以从这里继续报告所有交点。树包含1、 2。
处理线段 3 的左端点: 检查 3与1的交点 。无交点。将3插入树中。树包含 2、 1、 3。
处理线段 1 的右端点: 从树中删除1。检查2和3的交点。报告2和3的交点。树包含 2、 3。
处理线段 4 的左端点: 检查线4与线2和3 的交点。无交点。将4插入树中。树包含 2、 4、 3。
处理线段 5 的左端点:检查5与3 的交点 。无交点。将5插入树中。树包含 2、4、3、5。
处理线段 5 的右端点: 从树中删除 5。树包含2、 4、 3。
处理线段 4 的右端点: 从树中删除 4。树包含2、 4、 3。检查2与 3的交集。报告2与3的交集(请注意,再次报告了 2 与 3 的交集。我们可以添加一些逻辑来检查重复项)。树包含 2、 3。
线段 2 和 3 的右端点被处理: 都从树中删除,树变为空。
#include <bits/stdc++.h>
using namespace std;
// A point in 2D plane
struct Point
{
int x, y;
};
// A line segment with left as Point
// with smaller x value and right with
// larger x value.
struct Segment
{
Point left, right;
};
// An event for sweep line algorithm
// An event has a point, the position
// of point (whether left or right) and
// index of point in the original input
// array of segments.
struct Event {
int x, y;
bool isLeft;
int index;
Event(int x, int y, bool l, int i) : x(x), y(y), isLeft(l), index(i) {}
// This is for maintaining the order in set.
bool operator<(const Event& e) const {
if(y==e.y)return x<e.x;
return y < e.y;
}
};
// Given three collinear points p, q, r, the function checks if
// point q lies on line segment 'pr'
bool onSegment(Point p, Point q, Point r)
{
if (q.x <= max(p.x, r.x) && q.x >= min(p.x, r.x) &&
q.y <= max(p.y, r.y) && q.y >= min(p.y, r.y))
return true;
return false;
}
// To find orientation of ordered triplet (p, q, r).
// The function returns following values
// 0 --> p, q and r are collinear
// 1 --> Clockwise
// 2 --> Counterclockwise
int orientation(Point p, Point q, Point r)
{
// See
//c++ https://blog.youkuaiyun.com/hefeng_aspnet/article/details/141713859
//java https://blog.youkuaiyun.com/hefeng_aspnet/article/details/141717851
//python https://blog.youkuaiyun.com/hefeng_aspnet/article/details/141717981
//C# https://blog.youkuaiyun.com/hefeng_aspnet/article/details/141718039
//Javascript https://blog.youkuaiyun.com/hefeng_aspnet/article/details/141718097
// for details of below formula.
int val = (q.y - p.y) * (r.x - q.x) -
(q.x - p.x) * (r.y - q.y);
if (val == 0) return 0; // collinear
return (val > 0)? 1: 2; // clock or counterclock wise
}
// The main function that returns true if line segment 'p1q1'
// and 'p2q2' intersect.
bool doIntersect(Segment s1, Segment s2)
{
Point p1 = s1.left, q1 = s1.right, p2 = s2.left, q2 = s2.right;
// Find the four orientations needed for general and
// special cases
int o1 = orientation(p1, q1, p2);
int o2 = orientation(p1, q1, q2);
int o3 = orientation(p2, q2, p1);
int o4 = orientation(p2, q2, q1);
// General case
if (o1 != o2 && o3 != o4)
return true;
// Special Cases
// p1, q1 and p2 are collinear and p2 lies on segment p1q1
if (o1 == 0 && onSegment(p1, p2, q1)) return true;
// p1, q1 and q2 are collinear and q2 lies on segment p1q1
if (o2 == 0 && onSegment(p1, q2, q1)) return true;
// p2, q2 and p1 are collinear and p1 lies on segment p2q2
if (o3 == 0 && onSegment(p2, p1, q2)) return true;
// p2, q2 and q1 are collinear and q1 lies on segment p2q2
if (o4 == 0 && onSegment(p2, q1, q2)) return true;
return false; // Doesn't fall in any of the above cases
}
// Find predecessor of iterator in s.
set<Event>::iterator pred(set<Event> &s, set<Event>::iterator it) {
return it == s.begin() ? s.end() : --it;
}
// Find successor of iterator in s.
set<Event>::iterator succ(set<Event> &s, set<Event>::iterator it) {
return ++it;
}
// Returns true if any two lines intersect.
int isIntersect(Segment arr[], int n)
{
unordered_map<string,int> mp; // to note the pair for which intersection is checked already
// Pushing all points to a vector of events
vector<Event> e;
for (int i = 0; i < n; ++i) {
e.push_back(Event(arr[i].left.x, arr[i].left.y, true, i));
e.push_back(Event(arr[i].right.x, arr[i].right.y, false, i));
}
// Sorting all events according to x coordinate.
sort(e.begin(), e.end(), [](Event &e1, Event &e2) {return e1.x < e2.x;});
// For storing active segments.
set<Event> s;
int ans=0;
// Traversing through sorted points
for (int i=0; i<2*n; i++)
{
Event curr = e[i];
int index = curr.index;
// If current point is left of its segment
if (curr.isLeft)
{
// Get above and below points
auto next = s.lower_bound(curr);
auto prev = pred(s, next);
// Check if current point intersects with
// any of its adjacent
bool flag=false;
if (next != s.end() && doIntersect(arr[next->index], arr[index])){
string s=to_string(next->index+1)+" "+to_string(index+1);
if(mp.count(s)==0){mp[s]++;ans++;} //if not already checked we can increase count in map
}
if (prev != s.end() && doIntersect(arr[prev->index], arr[index])){
string s=to_string(prev->index+1)+" "+to_string(index+1);
if(mp.count(s)==0){mp[s]++;ans++;} //if not already checked we can increase count in map
}
// if same line segment is there then decrease answer as it got increased twice
if(prev != s.end() && next != s.end() && next->index==prev->index)ans--;
// Insert current point (or event)
s.insert(curr);
}
// If current point is right of its segment
else
{
// Find the iterator
auto it=s.find(Event(arr[index].left.x, arr[index].left.y, true, index));
// Find above and below points
auto next = succ(s, it);
auto prev = pred(s, it);
// If above and below point intersect
if (next != s.end() && prev != s.end())
{ string s=to_string(next->index+1)+" "+to_string(prev->index+1);
string s1=to_string(prev->index+1)+" "+to_string(next->index+1);
if (mp.count(s)==0&&mp.count(s1)==0&&doIntersect(arr[prev->index], arr[next->index]))
ans++;
mp[s]++;
}
// Remove current segment
s.erase(it);
}
}
//print pair of lines having intersection
for(auto &pr:mp){
cout<<"Line: "<<pr.first<<"\n";
}
return ans;
}
// Driver code
int main() {
Segment arr[] = { {{1, 5}, {4, 5}}, {{2, 5}, {10, 1}},{{3, 2}, {10, 3}},{{6, 4}, {9, 4}},{{7, 1}, {8, 1}}};
int n = sizeof(arr)/sizeof(arr[0]);
cout<<"Number of intersection points: "<<isIntersect(arr, n);
return 0;
}
输出:
线:2 3
线:1 2
交点数:2
时间复杂度:第一步是排序,需要 O(nLogn) 时间。第二步处理 2n 个点,处理每个点需要 O(Logn) 时间。因此,总体时间复杂度为 O(nLogn)
辅助空间: O(N)
参考文献:
http://courses.csail.mit.edu/6.006/spring11/lectures/lec24.pdf
http://www.youtube.com/watch?v=dePDHVovJlE