Algorithmus zum Abflachen überlappender Bereiche

16

Ich suche nach einer guten Möglichkeit, eine Liste potenziell überlappender numerischer Bereiche zu reduzieren (aufzuteilen). Das Problem ähnelt dem dieser Frage: Schnellster Weg, um überlappende Datumsbereiche aufzuteilen , und viele andere.

Die Bereiche sind jedoch nicht nur ganze Zahlen, und ich suche nach einem anständigen Algorithmus, der leicht in Javascript oder Python usw. implementiert werden kann.

Beispieldaten: Beispieldaten

Beispiellösung: Bildbeschreibung hier eingeben

Entschuldigung, wenn dies ein Duplikat ist, aber ich muss noch eine Lösung finden.

Jollywatt
quelle
Wie stellen Sie fest, dass Grün über Blau, aber unter Gelb und Orange liegt? Werden die Farbbereiche in der richtigen Reihenfolge angewendet? In diesem Fall ist der Algorithmus offensichtlich. nur ... ähm, wende die Farbbereiche der Reihe nach an.
Robert Harvey
1
Ja, sie werden der Reihe nach angewendet. Aber das ist das Problem - wie würden Sie die Bereiche "anwenden"?
Jollywatt
1
Fügen Sie häufig Farben hinzu oder entfernen Sie Farben, oder müssen Sie die Abfragegeschwindigkeit optimieren? Wie viele "Bereiche" werden Sie normalerweise haben? 3? 3000?
Telastyn
Es werden nicht sehr häufig Farben hinzugefügt / entfernt, und es wird irgendwo zwischen 10 und 20 Bereiche geben, mit einer Genauigkeit von 4 und mehr Stellen. Aus diesem Grund ist die Set-Methode nicht ganz geeignet, da die Sets mehr als 1000 Elemente enthalten müssen. Die Methode, mit der ich gegangen bin, ist die, die ich in Python gepostet habe.
Jollywatt

Antworten:

10

Gehen Sie mit einem Stapel von links nach rechts, um festzustellen, auf welcher Farbe Sie sich befinden. Verwenden Sie anstelle einer diskreten Karte die 10 Zahlen in Ihrem Datensatz als Haltepunkte.

Beginnen Sie mit einem leeren Stapel und setzen Sie ihn startauf 0, bis das Ende erreicht ist:

  • Wenn der Stapel leer ist:
    • Suchen Sie nach der ersten Farbe, die bei oder nach beginnt start, und legen Sie sie und alle Farben mit niedrigerem Rang auf den Stapel. Markieren Sie in Ihrer reduzierten Liste den Anfang dieser Farbe.
  • sonst (falls nicht leer):
    • Suchen Sie den nächsten Anfangspunkt für eine höherrangige Farbe in oder nach startund suchen Sie das Ende der aktuellen Farbe
      • Wenn die nächste Farbe zuerst beginnt, schieben Sie sie und alles andere auf dem Weg dorthin auf den Stapel. Aktualisieren Sie das Ende der aktuellen Farbe als den Anfang dieser und fügen Sie den Anfang dieser Farbe der reduzierten Liste hinzu.
      • Wenn es keine gibt und die aktuelle Farbe zuerst endet, setzen Sie sie startauf das Ende dieser Farbe, entfernen Sie sie vom Stapel und überprüfen Sie die Farbe mit dem nächsthöheren Rang
        • Wenn startsich die Farbe innerhalb des Bereichs der nächsten Farbe befindet, fügen Sie diese Farbe der reduzierten Liste hinzu, beginnend mit start.
        • Wenn der Stapel leer ist, setzen Sie die Schleife einfach fort (kehren Sie zum ersten Aufzählungspunkt zurück).

In Anbetracht Ihrer Beispieldaten ist dies ein mentaler Durchlauf:

# Initial data.
flattened = []
stack = []
start = 0
# Stack is empty.  Look for the next starting point at 0 or later: "b", 0 - Push it and all lower levels onto stack
flattened = [ (b, 0, ?) ]
stack = [ r, b ]
start = 0
# End of "b" is 5.4, next higher-colored start is "g" at 2 - Delimit and continue
flattened = [ (b, 0, 2), (g, 2, ?) ]
stack = [ r, b, g ]
start = 2
# End of "g" is 12, next higher-colored start is "y" at 3.5 - Delimit and continue
flattened = [ (b, 0, 2), (g, 2, 3.5), (y, 3.5, ?) ]
stack = [ r, b, g, y ]
start = 3.5
# End of "y" is 6.7, next higher-colored start is "o" at 6.7 - Delimit and continue
flattened = [ (b, 0, 2), (g, 2, 3.5), (y, 3.5, 6.7), (o, 6.7, ?) ]
stack = [ r, b, g, y, o ]
start = 6.7
# End of "o" is 10, and there is nothing starting at 12 or later in a higher color.  Next off stack, "y", has already ended.  Next off stack, "g", has not ended.  Delimit and continue.
flattened = [ (b, 0, 2), (g, 2, 3.5), (y, 3.5, 6.7), (o, 6.7, 10), (g, 10, ?) ]
stack = [ r, b, g ]
start = 10
# End of "g" is 12, there is nothing starting at 12 or later in a higher color.  Next off stack, "b", is out of range (already ended).  Next off stack, "r", is out of range (not started).  Mark end of current color:
flattened = [ (b, 0, 2), (g, 2, 3.5), (y, 3.5, 6.7), (o, 6.7, 10), (g, 10, 12) ]
stack = []
start = 12
# Stack is empty.  Look for the next starting point at 12 or later: "r", 12.5 - Push onto stack
flattened = [ (b, 0, 2), (g, 2, 3.5), (y, 3.5, 6.7), (o, 6.7, 10), (g, 10, 12), (r, 12.5, ?) ]
stack = [ r ]
start = 12
# End of "r" is 13.8, and there is nothing starting at 12 or higher in a higher color.  Mark end and pop off stack.
flattened = [ (b, 0, 2), (g, 2, 3.5), (y, 3.5, 6.7), (o, 6.7, 10), (g, 10, 12), (r, 12.5, 13.8) ]
stack = []
start = 13.8
# Stack is empty and nothing is past 13.8 - We're done.
Izkata
quelle
was meinst du mit "irgendetwas anderes auf dem weg dorthin auf den stapel"?
Guillaume07
1
@ Guillaume07 Irgendetwas von Rängen zwischen dem aktuellen und dem gewählten nächsten Start. Die Beispieldaten zeigen es nicht, aber stellen Sie sich vor, das Gelb wurde verschoben, um vor Grün zu beginnen - Sie müssen sowohl Grün als auch Gelb auf den Stapel schieben, sodass das Ende von Grün am Ende von Gelb immer noch an der richtigen Stelle im Stapel liegt so dass es immer noch im Endergebnis
auftaucht
Ein anderer Gedanke, den ich nicht verstehe, ist, warum Sie zuerst sagen: "Wenn der Stapel leer ist: Suchen Sie nach der ersten Farbe, die am oder vor dem Start beginnt", dann kommentieren Sie im Codebeispiel "# Stack is empty Startpunkt bei 0 oder später ". So ist es einmal vor und einmal später
Guillaume07
1
@ Guillaume07 Ja, ein Tippfehler, die richtige Version befindet sich zweimal im Codeblock (die zweite ist der Kommentar am unteren Rand, der mit "Stack is empty" beginnt.). Ich habe diesen Aufzählungspunkt bearbeitet.
Izkata,
3

Diese Lösung scheint die einfachste zu sein. (Oder zumindest am einfachsten zu begreifen)

Alles, was benötigt wird, ist eine Funktion, um zwei Bereiche zu subtrahieren. Mit anderen Worten, etwas, das folgendes ergibt:

A ------               A     ------           A    ----
B    -------    and    B ------        and    B ---------
=       ----           = ----                 = ---    --

Welches ist einfach genug. Dann können Sie einfach jeden der Bereiche durchlaufen, beginnend mit dem niedrigsten, und für jeden Bereich nacheinander alle darüber liegenden Bereiche abziehen. Und da hast du es.


Hier ist eine Implementierung des Range-Subtrahierers in Python:

def subtractRanges((As, Ae), (Bs, Be)):
    '''SUBTRACTS A FROM B'''
    # e.g, A =    ------
    #      B =  -----------
    # result =  --      ---
    # Returns list of new range(s)

    if As > Be or Bs > Ae: # All of B visible
        return [[Bs, Be]]
    result = []
    if As > Bs: # Beginning of B visible
        result.append([Bs, As])
    if Ae < Be: # End of B visible
        result.append([Ae, Be])
    return result

Mit dieser Funktion können Sie den Rest folgendermaßen erledigen: (Ein "span" bedeutet einen Bereich, da "range" ein Python-Schlüsselwort ist.)

spans = [["red", [12.5, 13.8]],
["blue", [0.0, 5.4]],
["green", [2.0, 12.0]],
["yellow", [3.5, 6.7]],
["orange", [6.7, 10.0]]]

i = 0 # Start at lowest span
while i < len(spans):
    for superior in spans[i+1:]: # Iterate through all spans above
        result = subtractRanges(superior[1], spans[i][1])
        if not result:      # If span is completely covered
            del spans[i]    # Remove it from list
            i -= 1          # Compensate for list shifting
            break           # Skip to next span
        else:   # If there is at least one resulting span
            spans[i][1] = result[0]
            if len(result) > 1: # If there are two resulting spans
                # Insert another span with the same name
                spans.insert(i+1, [spans[i][0], result[1]])
    i += 1

print spans

Das gibt [['red', [12.5, 13.8]], ['blue', [0.0, 2.0]], ['green', [2.0, 3.5]], ['green', [10.0, 12.0]], ['yellow', [3.5, 6.7]], ['orange', [6.7, 10.0]]], was richtig ist.

Jollywatt
quelle
Ihre Ausgabe am Ende entspricht nicht der erwarteten Ausgabe in der Frage ...
Izkata
@ Izkata Meine Güte, ich war nachlässig. Das muss die Ausgabe eines anderen Tests gewesen sein. Jetzt
behoben
2

Wenn der Umfang der Daten Ihren Beispieldaten wirklich ähnlich ist, können Sie eine Karte wie die folgende erstellen:

map = [0 .. 150]

for each color:
    for loc range start * 10 to range finish * 10:
        map[loc] = color

Gehen Sie dann einfach durch diese Karte, um die Bereiche zu generieren

curcolor = none
for loc in map:
    if map[loc] != curcolor:
        if curcolor:
            rangeend = loc / 10
        make new range
        rangecolor = map[loc]
        rangestart = loc / 10

Um zu arbeiten, müssen die Werte in einem relativ kleinen Bereich liegen, wie in Ihren Beispieldaten.

Bearbeiten: Um mit echten Floats zu arbeiten, generieren Sie mithilfe der Karte eine übergeordnete Zuordnung und ziehen Sie dann die Originaldaten heran, um die Grenzen zu erstellen.

map = [0 .. 15]

for each color:
   for loc round(range start) to round(range finish):
        map[loc] = color

curcolor = none
for loc in map
    if map[loc] != curcolor:

        make new range
        if loc = round(range[map[loc]].start)  
             rangestart = range[map[loc]].start
        else
             rangestart = previous rangeend
        rangecolor = map[loc]
        if curcolor:
             if map[loc] == none:
                 last rangeend = range[map[loc]].end
             else
                 last rangeend = rangestart
        curcolor = rangecolor
Gort den Roboter
quelle
Das ist eine sehr schöne Lösung, die mir schon mal begegnet ist. Ich bin jedoch auf der Suche nach einer allgemeineren Lösung, mit der beliebige Float-Bereiche verwaltet werden können ... (Dies wäre nicht die beste Lösung für etwa 563.807 - 770.100)
Jollywatt,
1
Ich denke, Sie könnten es verallgemeinern, indem Sie die Werte runden und die Karte generieren, aber einen Ort an den Rändern als zweifarbig markieren. Wenn Sie dann einen Ort mit zwei Farben sehen, kehren Sie zu den ursprünglichen Daten zurück, um die Grenze zu bestimmen.
Gort the Robot
2

Hier ist eine relativ einfache Lösung in Scala. Es sollte nicht zu schwierig sein, auf eine andere Sprache zu portieren.

case class Range(name: String, left: Double, right: Double) {
  def overlapsLeft(other: Range) =
    other.left < left && left < other.right

  def overlapsRight(other: Range) =
    other.left < right && right < other.right

  def overlapsCompletely(other: Range) =
    left <= other.left && right >= other.right

  def splitLeft(other: Range) = 
    Range(other.name, other.left, left)

  def splitRight(other: Range) = 
    Range(other.name, right, other.right)
}

def apply(ranges: Set[Range], newRange: Range) = {
  val left     = ranges.filter(newRange.overlapsLeft)
  val right    = ranges.filter(newRange.overlapsRight)
  val overlaps = ranges.filter(newRange.overlapsCompletely)

  val leftSplit  =  left.map(newRange.splitLeft)
  val rightSplit = right.map(newRange.splitRight)

  ranges -- left -- right -- overlaps ++ leftSplit ++ rightSplit + newRange
}

val ranges = Vector(
  Range("red",   12.5, 13.8),
  Range("blue",   0.0,  5.4),
  Range("green",  2.0, 12.0),
  Range("yellow", 3.5,  6.7),
  Range("orange", 6.7, 10.0))

val flattened = ranges.foldLeft(Set.empty[Range])(apply)
val sorted = flattened.toSeq.sortBy(_.left)
sorted foreach println

applyNimmt einen Setvon allen bereits angewendeten Bereichen auf, findet die Überlappungen und gibt dann einen neuen Satz abzüglich der Überlappungen und zuzüglich des neuen Bereichs und der neu aufgeteilten Bereiche zurück. foldLeftwiederholt Anrufe applymit jedem Eingangsbereich.

Karl Bielefeldt
quelle
0

Halten Sie einfach eine Reihe von Bereichen nach Start sortiert. Bereich hinzufügen, der alles abdeckt (-oo .. + oo). So fügen Sie einen Bereich r hinzu:

let pre = last range that starts before r starts

let post = earliest range that starts before r ends

now iterate from pre to post: split ranges that overlap, remove ranges that are covered, then add r
Kevin Cline
quelle