Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>Your current Brute-Force algorithm is O(n^2). For just your two 70,000 line polygons that's some factor of almost 10 Billion operations, to say nothing of the 700,000 other polygons. Obviously, no amount of mere code optimization is going to be enough, so you need some kind algorithmic optimization that can lower that O(n^2) without being unduly complicated.</p> <p>The O(n^2) comes from the nested loops in the brute-force algorithm that are each bounded by <code>n</code>, making it O(n*n). The simplest way to improve this would be to find some way to reduce the inner loop so that it is not bound by or dependent on <code>n</code>. So what we need to find is some way to order or re-order the inner list of lines to check the outer line against so that only a part of the full list needs to be scanned.</p> <p>The approach that I am taking takes advantage of the fact that if two line segments intersect, then the range of their X values must overlap each other. Mind you, this doesn't mean that they <em>do</em> intersect, but if their X ranges don't overlap, then they <em>cannot</em> be intersecting so theres no need to check them against each other. (this is true of the Y value ranges also, but you can only leverage one dimension at a time).</p> <p>The advantage of this approach is that these X ranges can be used to order the endpoints of the lines which can in turn be used as the starting and stopping points for which lines to check against for intersection.</p> <p>So specifically what we do is to define a custom class (<code>endpointEntry</code>) that represents the High or Low X values of the line's two points. These endpoints are all put into the same List structure and then sorted based on their X values. </p> <p>Then we implement an outer loop where we scan the entire list just as in the brute-force algorithm. However our inner loop is considerably different. Instead of re-scanning the entire list for lines to check for intersection, we rather <em>start</em> scanning down the sorted endpoint list from the high X value endpoint of the outer loop's line and end it when we pass below that same line's low X value endpoint. In this way, we only check the lines whose range of X values overlap the outer loop's line.</p> <p>OK, here's a c# class demonstrating my approach:</p> <pre><code>class CheckPolygon2 { // internal supporting classes class endpointEntry { public double XValue; public bool isHi; public Line2D line; public double hi; public double lo; public endpointEntry fLink; public endpointEntry bLink; } class endpointSorter : IComparer&lt;endpointEntry&gt; { public int Compare(endpointEntry c1, endpointEntry c2) { // sort values on XValue, descending if (c1.XValue &gt; c2.XValue) { return -1; } else if (c1.XValue &lt; c2.XValue) { return 1; } else // must be equal, make sure hi's sort before lo's if (c1.isHi &amp;&amp; !c2.isHi) { return -1; } else if (!c1.isHi &amp;&amp; c2.isHi) { return 1; } else { return 0; } } } public bool CheckForCrossing(List&lt;Line2D&gt; lines) { List&lt;endpointEntry&gt; pts = new List&lt;endpointEntry&gt;(2 * lines.Count); // Make endpoint objects from the lines so that we can sort all of the // lines endpoints. foreach (Line2D lin in lines) { // make the endpoint objects for this line endpointEntry hi, lo; if (lin.P1.X &lt; lin.P2.X) { hi = new endpointEntry() { XValue = lin.P2.X, isHi = true, line = lin, hi = lin.P2.X, lo = lin.P1.X }; lo = new endpointEntry() { XValue = lin.P1.X, isHi = false, line = lin, hi = lin.P1.X, lo = lin.P2.X }; } else { hi = new endpointEntry() { XValue = lin.P1.X, isHi = true, line = lin, hi = lin.P1.X, lo = lin.P2.X }; lo = new endpointEntry() { XValue = lin.P2.X, isHi = false, line = lin, hi = lin.P2.X, lo = lin.P1.X }; } // add them to the sort-list pts.Add(hi); pts.Add(lo); } // sort the list pts.Sort(new endpointSorter()); // sort the endpoint forward and backward links endpointEntry prev = null; foreach (endpointEntry pt in pts) { if (prev != null) { pt.bLink = prev; prev.fLink = pt; } prev = pt; } // NOW, we are ready to look for intersecting lines foreach (endpointEntry pt in pts) { // for every Hi endpoint ... if (pt.isHi) { // check every other line whose X-range is either wholly // contained within our own, or that overlaps the high // part of ours. The other two cases of overlap (overlaps // our low end, or wholly contains us) is covered by hi // points above that scan down to check us. // scan down for each lo-endpoint below us checking each's // line for intersection until we pass our own lo-X value for (endpointEntry pLo = pt.fLink; (pLo != null) &amp;&amp; (pLo.XValue &gt;= pt.lo); pLo = pLo.fLink) { // is this a lo-endpoint? if (!pLo.isHi) { // check its line for intersection if (pt.line.intersectsLine(pLo.line)) return true; } } } } return false; } } </code></pre> <p>I am not certain what the true execution complexity of this algorithm is, but I suspect that for most non-pathological polygons it will be close to O(n*SQRT(n)) which should be fast enough.</p> <hr> <h2>Explanation of the Inner Loop logic:</h2> <p>The Inner Loop simply scans the endPoints list in the same sorted order as the outer loop. But it will <em>start</em> scanning from where the outer loop from where the outer loop currently is in the list (which is the hiX point of some line), and will only scan until the endPoint values drop below the corresponding loX value of that same line.</p> <p>What's tricky here, is that this cannot be done with an Enumerator (the <code>foreach(..in pts)</code> of the outer loop) because there's no way to enumerate a sublist of a list, nor to start the enumeration based on another enumerations current position. So instead what I did was to use the Forward and Backward Links (fLink and bLink) properties to make a doubly-linked list structure that retains the sorted order of the list, but that I can incrementally scan without enumerating the list:</p> <pre><code>for (endpointEntry pLo = pt.fLink; (pLo != null) &amp;&amp; (pLo.XValue &gt;= pt.lo); pLo = pLo.fLink) </code></pre> <p>Breaking this down, the old-style <code>for</code> loop specifier has three parts: initialization, condition, and increment-decrement. The initialization expression, <code>endpointEntry pLo = pt.fLink;</code> initializes <code>pLo</code> with the forward Link of the current point in the list. That is, the next point in the list, in descending sorted order.</p> <p>Then the body of the inner loop gets executed. Then the increment-decrement <code>pLo = pLo.fLink</code> gets applied, which simply sets the inner loop's current point (<code>pLo</code>) to the next lower point using it's forward-link (<code>pLo.fLink</code>), thus advancing the loop.</p> <p>Finally, it loops after testing the loop condition <code>(pLo != null) &amp;&amp; (pLo.XValue &gt;= pt.lo)</code> which loops so long as the new point isn't null (which would mean that we were at the end of the list) and so long as the new point's <code>XValue</code> is still greater than or equal to the low X value of the outer loop's current point. This second condition insures that the inner loop only looks at lines that are overlapping the line of the outer loop's endpoint.</p> <hr> <p>What is clearer to me now, is that I probably could have gotten around this whole fLink-bLink clumsiness by treating the endPoints List as an array instead:</p> <ol> <li>Fill up the list with endPoints</li> <li>Sort the List by XValue</li> <li>Outer Loop through the list in descending order, using an index iterator (<code>i</code>), instead of a <code>foreach</code> enumerator</li> <li>(A) Inner Loop through the list, using a second iterator (<code>j</code>), starting at <code>i</code> and ending when it passed below <code>pt.Lo</code>.</li> </ol> <p>That I think would be much simpler. I can post a simplified version like that, if you want.</p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload