Thuật toán làm phẳng phạm vi chồng chéo


16

Tôi đang tìm kiếm một cách làm phẳng (chia nhỏ) một danh sách các phạm vi số có khả năng chồng lấp. Vấn đề rất giống với câu hỏi này: Cách nhanh nhất để phân chia phạm vi ngày chồng chéo và nhiều phạm vi khác.

Tuy nhiên, phạm vi không chỉ là số nguyên và tôi đang tìm kiếm một thuật toán hợp lý có thể dễ dàng thực hiện trong Javascript hoặc Python, v.v.

Dữ liệu mẫu: Dữ liệu mẫu

Giải pháp ví dụ: nhập mô tả hình ảnh ở đây

Xin lỗi nếu đây là một bản sao, nhưng tôi vẫn chưa tìm ra giải pháp.


Làm thế nào để bạn xác định rằng màu xanh lá cây là trên cùng của màu xanh, nhưng dưới màu vàng và màu cam? Là các phạm vi màu được áp dụng theo thứ tự? Nếu đó là trường hợp, thuật toán có vẻ rõ ràng; chỉ ... erm, áp dụng các dải màu theo thứ tự.
Robert Harvey

1
Vâng, chúng được áp dụng theo thứ tự. Nhưng đó là vấn đề mà bạn sẽ 'áp dụng' phạm vi?
Jollywatt

1
Bạn có thường xuyên thêm / xóa màu hoặc bạn cần tối ưu hóa cho tốc độ truy vấn? Bạn thường có bao nhiêu "phạm vi"? 3? 3000?
Telastyn

Không được thêm / xóa màu rất thường xuyên và sẽ có khoảng từ 10-20 phạm vi, với độ chính xác hơn 4 chữ số. Đó là lý do tại sao phương thức thiết lập không phù hợp lắm, vì các bộ sẽ phải dài hơn 1000 mục. Phương pháp tôi đã thực hiện là phương pháp tôi đã đăng trong Python.
Jollywatt

Câu trả lời:


10

Đi bộ từ trái sang phải, sử dụng ngăn xếp để theo dõi màu sắc bạn đang mặc. Thay vì một bản đồ riêng biệt, hãy sử dụng 10 số trong tập dữ liệu của bạn làm điểm dừng.

Bắt đầu với một ngăn xếp trống và đặt startthành 0, lặp cho đến khi chúng ta kết thúc:

  • Nếu ngăn xếp trống:
    • Tìm kiếm màu đầu tiên bắt đầu tại hoặc sau đó start, và đẩy nó và tất cả các màu được xếp hạng thấp hơn lên ngăn xếp. Trong danh sách làm phẳng của bạn, đánh dấu sự bắt đầu của màu đó.
  • khác (Nếu không trống):
    • Tìm điểm bắt đầu tiếp theo cho bất kỳ màu nào được xếp hạng cao hơn tại hoặc sau startvà tìm điểm kết thúc của màu hiện tại
      • Nếu màu tiếp theo bắt đầu trước, đẩy nó và bất cứ thứ gì khác trên đường đến nó lên ngăn xếp. Cập nhật phần cuối của màu hiện tại làm phần đầu của màu này và thêm phần bắt đầu của màu này vào danh sách dẹt.
      • Nếu không có màu nào và màu hiện tại kết thúc trước, hãy đặt thành startcuối màu này, bật nó ra khỏi ngăn xếp và kiểm tra màu được xếp hạng cao nhất tiếp theo
        • Nếu startnằm trong phạm vi của màu tiếp theo, hãy thêm màu này vào danh sách làm phẳng, bắt đầu từ start.
        • Nếu ngăn xếp trống, chỉ cần tiếp tục vòng lặp (quay trở lại điểm đầu tiên).

Đây là một sự cố gắng thông qua dữ liệu mẫu của bạn:

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

bạn có ý nghĩa gì bởi "bất cứ điều gì khác trên đường đến nó"?
Guillaume07

1
@ Guillaume07 Bất cứ thứ gì có thứ hạng giữa hiện tại và bắt đầu được chọn tiếp theo. Dữ liệu mẫu không hiển thị, nhưng hãy tưởng tượng màu vàng đã được chuyển sang bắt đầu trước màu xanh lá cây - bạn phải đẩy cả màu xanh lá cây và màu vàng lên ngăn xếp để khi màu vàng kết thúc, phần cuối của màu xanh lá cây vẫn ở đúng vị trí trong ngăn xếp vì vậy nó vẫn xuất hiện trong kết quả cuối cùng
Izkata

Một suy nghĩ khác tôi không hiểu, xin vui lòng, đó là lý do tại sao bạn nói trước tiên "Nếu ngăn xếp trống: Hãy tìm màu đầu tiên bắt đầu tại hoặc trước khi bắt đầu", sau đó trong mẫu mã bạn nhận xét "# Stack trống rỗng. Hãy tìm tiếp theo điểm bắt đầu từ 0 trở lên ". Vì vậy, một khi nó là trước và một lần sau
Guillaume07

1
@ Guillaume07 Yep, một lỗi đánh máy, phiên bản chính xác nằm trong khối mã hai lần (lần thứ hai là nhận xét gần phía dưới bắt đầu "Ngăn xếp trống."). Tôi đã chỉnh sửa điểm đạn đó.
Izkata

3

Giải pháp này có vẻ đơn giản nhất. (Hoặc ít nhất, dễ nắm bắt nhất)

Tất cả những gì cần thiết là một hàm để trừ hai phạm vi. Nói cách khác, một cái gì đó sẽ cung cấp cho điều này:

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

Mà đủ đơn giản. Sau đó, bạn có thể chỉ cần lặp lại qua từng phạm vi, bắt đầu từ mức thấp nhất và lần lượt trừ đi tất cả các phạm vi trên nó. Và bạn có nó rồi đấy!


Đây là một triển khai của phép trừ phạm vi trong 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

Sử dụng chức năng này, phần còn lại có thể được thực hiện như sau: ('span' có nghĩa là một phạm vi, vì 'phạm vi' là một từ khóa 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

Điều này cho [['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]]], đó là chính xác.


Đầu ra của bạn ở cuối không khớp với đầu ra dự kiến ​​trong câu hỏi ...
Izkata

@Izkata Trời ạ, tôi bất cẩn. Đó phải là đầu ra từ một thử nghiệm khác. Đã sửa bây giờ, cảm ơn
Jollywatt

2

Nếu dữ liệu thực sự tương tự phạm vi với dữ liệu mẫu của bạn, bạn có thể tạo bản đồ như thế này:

map = [0 .. 150]

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

Sau đó, chỉ cần đi qua bản đồ này để tạo ra các phạm vi

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

Để hoạt động, các giá trị phải nằm trong một phạm vi tương đối nhỏ như trong dữ liệu mẫu của bạn.

Chỉnh sửa: để làm việc với các số float thực, sử dụng bản đồ để tạo ánh xạ mức cao và sau đó tham khảo dữ liệu gốc để tạo ranh giới.

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

Đây là một giải pháp rất hay, tôi đã bắt gặp nó trước đây. Tuy nhiên, tôi đang tìm kiếm một giải pháp chung chung hơn có thể quản lý bất kỳ phạm vi thả nổi tùy ý nào ... (điều này sẽ không tốt nhất cho thứ gì đó như 563.807 - 770.100)
Jollywatt

1
Tôi nghĩ bạn có thể khái quát hóa nó bằng cách làm tròn các giá trị và tạo bản đồ, nhưng đánh dấu một vị trí trên các cạnh là có hai màu. Sau đó, khi bạn thấy một vị trí có hai màu, hãy quay lại dữ liệu gốc để xác định ranh giới.
Gort Robot

2

Đây là một giải pháp tương đối đơn giản trong Scala. Không nên quá khó khăn để chuyển sang ngôn ngữ khác.

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

applylấy một Settrong tất cả các phạm vi đã được áp dụng, tìm các phần trùng lặp, sau đó trả về một tập hợp mới trừ đi các phần trùng lặp và cộng với phạm vi mới và các phạm vi mới được phân chia. foldLeftnhiều lần gọi applyvới mỗi phạm vi đầu vào.


0

Chỉ cần giữ một tập hợp các phạm vi được sắp xếp theo bắt đầu. Thêm phạm vi bao gồm mọi thứ (-oo .. + oo). Để thêm một phạm vi 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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.