重複する範囲のセットを重複しない範囲に分割する方法は?
-
07-07-2019 - |
質問
範囲のセットがあるとしましょう:
- 0-100: 'a'
- 0-75: 'b'
- 95-150: 'c'
- 120-130: 'd'
明らかに、これらの範囲は特定のポイントで重複しています。元の範囲(この場合、範囲の後の文字)に関連付けられた情報を保持しながら、重複しない範囲のリストを作成するためにこれらの範囲をどのように分析しますか?
たとえば、アルゴリズムを実行した後の上記の結果は次のようになります。
- 0-75: 'a'、 'b'
- 76-94: 'a'
- 95-100: 'a'、 'c'
- 101-119: 'c'
- 120-130: 'c'、 'd'
- 131-150: 'c'
解決
オーディオサンプルをミキシングする(部分的にオーバーラップする)プログラムを作成するときに、同じ質問がありました。
「開始イベント」を追加しましたおよび「イベントの停止」 (各アイテムについて)リストに追加し、リストを時点でソートしてから、順番に処理します。時間の代わりに整数ポイントを使用し、サウンドを混合する代わりに、範囲に対応するセットにシンボルを追加することを除いて、同じことを行うことができます。空の範囲を生成するか、単にそれらを省略するかはオプションです。
編集
おそらくいくつかのコード...
# input = list of (start, stop, symbol) tuples
points = [] # list of (offset, plus/minus, symbol) tuples
for start,stop,symbol in input:
points.append((start,'+',symbol))
points.append((stop,'-',symbol))
points.sort()
ranges = [] # output list of (start, stop, symbol_set) tuples
current_set = set()
last_start = None
for offset,pm,symbol in points:
if pm == '+':
if last_start is not None:
#TODO avoid outputting empty or trivial ranges
ranges.append((last_start,offset-1,current_set))
current_set.add(symbol)
last_start = offset
elif pm == '-':
# Getting a minus without a last_start is unpossible here, so not handled
ranges.append((last_start,offset-1,current_set))
current_set.remove(symbol)
last_start = offset
# Finish off
if last_start is not None:
ranges.append((last_start,offset-1,current_set))
完全にテストされていないことは明らかです。
他のヒント
エンドポイントのリストを作成してソートし、開始点と終了点によって範囲のリストにインデックスを付けます。次に、ソートされたエンドポイントのリストを反復処理し、それぞれについて、範囲をチェックして、その時点で開始/停止しているエンドポイントを確認します。
これはおそらく、コードでより適切に表現されています...範囲がタプルで表現されている場合:
ranges = [(0,100,'a'),(0,75,'b'),(95,150,'c'),(120,130,'d')]
endpoints = sorted(list(set([r[0] for r in ranges] + [r[1] for r in ranges])))
start = {}
end = {}
for e in endpoints:
start[e] = set()
end[e] = set()
for r in ranges:
start[r[0]].add(r[2])
end[r[1]].add(r[2])
current_ranges = set()
for e1, e2 in zip(endpoints[:-1], endpoints[1:]):
current_ranges.difference_update(end[e1])
current_ranges.update(start[e1])
print '%d - %d: %s' % (e1, e2, ','.join(current_ranges))
これを振り返ってみると、より効率的な(または少なくともより見栄えの良い)方法がなかったら驚くでしょう。
説明するのは集合論の例です。集合、交点、集合の差を計算する一般的なアルゴリズムについては、次を参照してください。
www.gvu.gatech.edu/~jarek/graphics /papers/04PolygonBooleansMargalit.pdf
この論文はグラフィックスを対象としていますが、一般的な集合論にも適用できます。正確な読み物ではありません。
擬似コード:
unusedRanges = [ (each of your ranges) ]
rangesInUse = []
usedRanges = []
beginningBoundary = nil
boundaries = [ list of all your ranges' start and end values, sorted ]
resultRanges = []
for (boundary in boundaries) {
rangesStarting = []
rangesEnding = []
// determine which ranges begin at this boundary
for (range in unusedRanges) {
if (range.begin == boundary) {
rangesStarting.add(range)
}
}
// if there are any new ones, start a new range
if (rangesStarting isn't empty) {
if (beginningBoundary isn't nil) {
// add the range we just passed
resultRanges.add(beginningBoundary, boundary - 1, [collected values from rangesInUse])
}
// note that we are starting a new range
beginningBoundary = boundary
for (range in rangesStarting) {
rangesInUse.add(range)
unusedRanges.remove(range)
}
}
// determine which ranges end at this boundary
for (range in rangesInUse) {
if (range.end == boundary) {
rangesEnding.add(range)
}
}
// if any boundaries are ending, stop the range
if (rangesEnding isn't empty) {
// add the range up to this boundary
resultRanges.add(beginningBoundary, boundary, [collected values from rangesInUse]
for (range in rangesEnding) {
usedRanges.add(range)
rangesInUse.remove(range)
}
if (rangesInUse isn't empty) {
// some ranges didn't end; note that we are starting a new range
beginningBoundary = boundary + 1
}
else {
beginningBoundary = nil
}
}
}
単体テスト:
最後に、resultRangesに探している結果があり、unusedRangesとrangeInUseが空、beginningBoundaryがnilで、usedRangesに使用されているunusedRangesが含まれている必要があります(ただしrange.endでソートされています)。
(1,1)のような間隔のサポートを含む、テスト済みのEdmundsへの同様の回答:
class MultiSet(object):
def __init__(self, intervals):
self.intervals = intervals
self.events = None
def split_ranges(self):
self.events = []
for start, stop, symbol in self.intervals:
self.events.append((start, True, stop, symbol))
self.events.append((stop, False, start, symbol))
def event_key(event):
key_endpoint, key_is_start, key_other, _ = event
key_order = 0 if key_is_start else 1
return key_endpoint, key_order, key_other
self.events.sort(key=event_key)
current_set = set()
ranges = []
current_start = -1
for endpoint, is_start, other, symbol in self.events:
if is_start:
if current_start != -1 and endpoint != current_start and \
endpoint - 1 >= current_start and current_set:
ranges.append((current_start, endpoint - 1, current_set.copy()))
current_start = endpoint
current_set.add(symbol)
else:
if current_start != -1 and endpoint >= current_start and current_set:
ranges.append((current_start, endpoint, current_set.copy()))
current_set.remove(symbol)
current_start = endpoint + 1
return ranges
if __name__ == '__main__':
intervals = [
(0, 100, 'a'), (0, 75, 'b'), (75, 80, 'd'), (95, 150, 'c'),
(120, 130, 'd'), (160, 175, 'e'), (165, 180, 'a')
]
multiset = MultiSet(intervals)
pprint.pprint(multiset.split_ranges())
[(0, 74, {'b', 'a'}),
(75, 75, {'d', 'b', 'a'}),
(76, 80, {'d', 'a'}),
(81, 94, {'a'}),
(95, 100, {'c', 'a'}),
(101, 119, {'c'}),
(120, 130, {'d', 'c'}),
(131, 150, {'c'}),
(160, 164, {'e'}),
(165, 175, {'e', 'a'}),
(176, 180, {'a'})]