重複する範囲を平坦化するアルゴリズム


16

重複する可能性のある数値範囲のリストを平坦化(分割)する良い方法を探しています。問題は、この質問の問題と非常に似ています。重複する日付範囲を分割する最も速い方法、および他の多くの方法です。

ただし、範囲は整数だけではなく、JavascriptやPythonなどで簡単に実装できる適切なアルゴリズムを探しています。

サンプルデータ: サンプルデータ

ソリューション例: ここに画像の説明を入力してください

これが重複している場合はおApびしますが、解決策はまだ見つかりません。


緑が青の上にあるが、黄色とオレンジの下にあることをどのように判断しますか?色の範囲は順番に適用されていますか?その場合、アルゴリズムは明らかなようです。ただ...えー、色の範囲を順番に適用してください。
ロバートハーヴェイ

1
はい、それらは順番に適用されます。しかし、それが問題です。範囲をどのように「適用」しますか?
ジョリーワット

1
色を頻繁に追加/削除しますか、それともクエリ速度を最適化する必要がありますか?通常、いくつの「範囲」がありますか?3?3000?
テラスティン

色を頻繁に追加/削除することはなく、10から20の範囲で4桁以上の精度があります。セットが1000項目以上の長さである必要があるため、セットメソッドはあまり適していません。私が行った方法は、Pythonで投稿した方法です。
ジョリーワット

回答:


10

スタックを使用して、左から右に歩いて、自分の色を追跡します。個別のマップの代わりに、データセット内の10個の数値をブレークポイントとして使用します。

空のスタックで開始し、start0に設定して、最後に到達するまでループします。

  • スタックが空の場合:
    • で始まる最初の色を探し、startそれとすべての下位ランクの色をスタックにプッシュします。フラット化されたリストで、その色の始まりをマークします。
  • その他(空でない場合):
    • の上位ランクの色の次の開始点をstart見つけ、現在の色の終わりを見つけます
      • 次の色が最初に始まる場合、その色とその途中にあるものをスタックにプッシュします。現在の色の終わりをこの色の始まりとして更新し、この色の始まりを平坦化されたリストに追加します。
      • 存在せず、現在の色が最初にstart終了する場合、この色の最後に設定し、スタックからそれをポップし、次に高いランクの色を確認します
        • startが次の色の範囲内にある場合、この色をから始まるフラット化されたリストに追加しstartます。
        • スタックが空になったら、ループを続行します(最初の箇条書きに戻ります)。

これは、サンプルデータが与えられたメンタルランスルーです。

# 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.

「スタックに向かう途中の何か」とはどういう意味ですか?
Guillaume07

1
@ Guillaume07現在の開始と選択された次の開始の間のランクのすべて。サンプルデータには表示されていませんが、黄色が緑の前に開始するようにシフトされていることを想像してください。黄色と緑の両方をスタックにプッシュして、黄色が終了しても緑の終わりがスタックの適切な場所にあるようにする必要がありますそのため、最終結果に表示されます
イズカタ

もう1つ理解できないと思うのは、「スタックが空の場合:開始時または開始前に最初の色を探してください」と最初に言うのは、コードサンプルで「#スタックは空です。次を探して0以降の開始点」。一度それが前であり、一度それが後である場合
Guillaume07

1
@ Guillaume07誤字です。正しいバージョンはコードブロックに2回あります(2番目は、「Stack is empty。」で始まる下部近くのコメントです)。その箇条書きを編集しました。
イズカタ

3

このソリューションは最も簡単に思えます。(または、少なくとも、最も把握しやすい)

必要なのは、2つの範囲を減算する関数だけです。言い換えれば、これを与えるもの:

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

どちらも簡単です。次に、各範囲を最も低いものから順に繰り返し、それぞれについて、その上のすべての範囲を順番に減算します。そして、あなたはそれを持っています。


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

この関数を使用すると、残りは次のように実行できます(「範囲」はPythonキーワードなので、「スパン」は範囲を意味します)

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

これは[['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]]]、正しいを与えます。


最後にあなたの出力が...問題の予想される出力と一致していません
Izkata

@Iskata Gosh、私は不注意でした。それは別のテストからの出力であるに違いありません。今を修正、感謝
Jollywatt

2

データのスコープがサンプルデータと本当に似ている場合は、次のようなマップを作成できます。

map = [0 .. 150]

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

次に、このマップを歩いて範囲を生成します

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

動作させるには、サンプルデータのように値が比較的狭い範囲内にある必要があります。

編集:真のフロートを使用するには、マップを使用して高レベルのマッピングを生成し、元のデータを参照して境界を作成します。

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

これは非常に良い解決策です。私は以前にそれを見つけました。しかし、私はより一般的な解決策を探していますすることができます( - 770.100これは563.807のようなもののために最善ではないでしょう)...任意のフロート範囲を管理する
Jollywatt

1
値を丸めてマップを生成することにより、それを一般化できると思いますが、エッジ上の位置を2色としてマークします。次に、2色の場所が表示されたら、元のデータに戻って境界を決定します。
ロボットをゲット

2

Scalaでの比較的簡単なソリューションを次に示します。別の言語に移植するのはそれほど難しくないはずです。

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

applySetすでに適用されているすべての範囲を取り込んで重複を見つけ、重複を除いた新しいセットと、新しい範囲と新しく分割された範囲を返します。 各入力範囲でfoldLeft繰り返し呼び出しapplyます。


0

一連の範囲を開始順にソートするだけです。すべてをカバーする範囲を追加します(-oo .. + oo)。範囲rを追加するには:

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.