web-dev-qa-db-ja.com

1つの直線と交差できる長方形の最大数

私は次のように述べているこのチャレンジ問題を見つけました:

XY平面にn個の長方形があると仮定します。この平面に描かれた1つの直線と交差できる長方形の最大数を計算するプログラムを記述します。

see image for an example

私はかなり長い間ブレーンストーミングをしてきましたが、解決策を見つけることができませんでした。多分ある段階では、動的プログラミングのステップを使用しますが、開始方法を理解できませんでした。

31
Tapan Vaishnav

以下はO(n ^ 2 log n)ソリューションのスケッチです。

まず、予備知識が他の回答と共有されました。いくつかの長方形を通過する線がある場合、それをいくつかの長方形のコーナーを通過するまで、2つの辺のいずれかに変換できます。その後、そのコーナーを回転の中心として固定し、別のコーナーを通過するまで線を2つの側面のいずれかに回転させます。プロセス全体で、ラインと長方形の辺の間のすべての交点はこれらの辺にとどまっていたため、交点の数は、ラインが横切る長方形の数と同じでした。結果として、O(n ^ 2)でキャップされた2つの長方形のコーナーを通過するラインのみを考慮することができ、任意のラインの無限空間と比較して歓迎される改善です。

では、これらすべての行を効率的にチェックするにはどうすればよいでしょうか。まず、1つの点Aを修正し、Aを通過するすべての線を考慮する外側のループがあるとします。AにはO(n)の選択肢があります。

これで、1つの点Aが固定され、他のすべてのコーナーBを通過するすべての線ABを検討したいと思います。これを行うには、まずABの極角、つまり角度軸OxとベクトルABの間。角度は-PIから+ PIまで、または0から2 PIまで、またはその他の方法で測定されます。角度を並べ替えるために円をカットするポイントは任意です。ソートはO(n log n)で行われます。

今、私たちはポイントBを持っています1、B2、...、Bk 点Aの周りの極角でソートされます(それらの数kは4n-4のようなもので、点Aがコーナーであるものを除くすべての長方形のすべてのコーナー)。まず、線ABを見てください。1 そして、O(n)でその線が交差する長方形の数を数えます。その後、ABを回転することを検討してください。1 ABへ2、次にAB2 ABへ、ABまでk。ローテーション中に発生するイベントは次のとおりです。

  • ABに回転するとき、およびB ある長方形の最初の角です。交差する長方形の数は、回転線がBに当たるとすぐに1ずつ増えます。

  • ABに回転するときj、およびBj ある長方形の最後の角です。交差する長方形の数は、線がBを超えて回転するとすぐに1ずつ減少しますj

最初と最後のコーナーは、いくつかのO(n)前処理で、並べ替えの後、順序付けられたイベントを考慮する前に、確立できます。

つまり、次のようなイベントに回転して、O(1)で交差する長方形の数を更新できます。そして、合計k = O(n)イベントがあります。あとは、アルゴリズム全体でこの量のグローバル最大値を追跡することです。答えはまさにこの最大値です。

アルゴリズム全体は、宣伝されているとおり、O(n *(n log n + n + n))で実行されます。これは、O(n ^ 2 log n)です。

8
Gassa

(飛行機の回転を検討した私の以前の回答の編集)

O(n^2)アルゴリズムのスケッチは次のとおりです。これは、GassaのアイデアとEvgeny Kluevのデュアルライン配置への参照を、ソートされたangularシーケンスとして組み合わせたものです。

二重に接続されたEdgeリストまたは同様の構造から始め、O(1)時間でEdgeを分割できるようにし、2次元平面に入力するときに作成した面をトラバースするメソッドを作成します。簡単にするために、下の四角形の12隅のうち3つだけを使用してみましょう。

_9|     (5,9)___(7,9)
8|         |   |
7|    (4,6)|   |
6|    ___C |   |
5|   |   | |   |
4|   |___| |   |
3|  ___    |___|(7,3)
2| |   |  B (5,3)
1|A|___|(1,1)
 |_ _ _ _ _ _ _ _
   1 2 3 4 5 6 7
_

次の変換に従って、2つの平面に3つの点(コーナー)を挿入します。

_point p => line p* as a*p_x - p_y
line l as ax + b => point l* as (a, -b)
_

ポイントを_A, B, C_の順に入力してみましょう。最初に_A => y = x - 1_を入力します。これまでのところ、エッジは1つしかないため、_B => y = 5x - 3_を挿入します。これにより、頂点_(1/2, -1/2)_が作成され、エッジが分割されます。 (このソリューションの洗練された側面の1つは、二重平面の各頂点(点)が、実際には長方形の角を通る線の二重点であることです。_1 = 1/2*1 + 1/2_および_3 = 1/2*5 + 1/2_を観察してください_(1,1)_および_(5,3)_)

最後の点_C => y = 4x - 6_に入ると、交差する左端の面(不完全な面である可能性があります)を探します。この検索はO(n)時間です。これは、各面を試す必要があるためです。頂点_(-3, -18)_を見つけて作成し、_5x - 3_の下部エッジを分割し、エッジを上に移動して、頂点_x - 1_で_(5/3, 2/3)_の右半分を分割します。最初に左端の面を見つけ、次に各面をトラバースしてエッジを分割し、頂点(ラインの交点)をマークするため、各挿入にはO(n)時間があります。

デュアルプレーンでは、次のようになります。

enter image description here

ライン配置を構築した後、3つのサンプルポイント(四角形の角)から反復を開始します。ソートされたangularシーケンスを1つの点に関連して再構築する際の魔法の一部は、角度(それぞれがデュアル平面の順序付き線の交点に対応)を右側の点に対応する角度に分割することです(より大きなx座標を使用)および左側のものと2つのシーケンスを連結して、-90度から-270度までの順序付けられたシーケンスを取得します(右側の点は、固定点に対して正の勾配を持つ線に変換されます) ;負の勾配の左側のもの。_(C*) 4x - 6_の線が水平になるまでサービス/画面を時計回りに回転すると、_B*_に正の勾配と_A*_の負の値が表示されます。 。)

なぜ機能するのですか?元の平面の点pが二重平面の線_p*_に変換される場合、その二重線を左から右にトラバースすることは、線をpの周りで回転させることに対応しますpも通過する元の平面。二重線は、この回転線のすべての勾配を、負の無限大(垂直)からゼロ(水平)から無限大(再び垂直)までのx座標でマークします。

(rectangle-count-logicを要約して、angularシーケンスを繰り返しながら現在の長方形のcount_arrayを更新します。1の場合、現在の交差カウントをインクリメントします。4で、線が直接コーナーではなく、0に設定して現在の交差数を減らします。)

_Pick A, lookup A*
=> x - 1.

Obtain the concatenated sequence by traversing the edges in O(n)
=> [(B*) 5x - 3, (C*) 4x - 6] ++ [No points left of A]

Initialise an empty counter array, count_array of length n-1

Initialise a pointer, ptr, to track rectangle corners passed in
the opposite direction of the current vector.

Iterate:
  vertex (1/2, -1/2)
  => line y = 1/2x + 1/2 (AB)

  perform rectangle-count-logic

  if the slope is positive (1/2 is positive):
    while the point at ptr is higher than the line:
      perform rectangle-count-logic

  else if the slope is negative:
    while the point at ptr is lower than the line:
      perform rectangle-count-logic

  => ptr passes through the rest of the points up to the corner
     across from C, so intersection count is unchanged

  vertex (5/3, 2/3)
  => line y = 5/3x - 2/3 (AC)
_

_(5,9)_がAC (y = 5/3x - 2/3)の線より上にあることがわかります。つまり、この時点では、右端の長方形との交差を数え、まだカウントをリセットしておらず、合計で3つの長方形になります。この行。

二重平面のグラフでは、他のangularシーケンスも見ることができます。

_for point B => B* => 5x - 3: [No points right of B] ++ [(C*) 4x - 6, (A*) x - 1]

for point C => C* => 4x - 6: [(B*) 5x - 3] ++ [(A*) x - 1]
(note that we start at -90 deg up to -270 deg)
_
4

解決

グラフのすべての線のスペースでは、コーナーを通過する線は、数または交差が減少しそうな線とまったく同じです。つまり、それぞれが極大値を形成します。

そして、少なくとも1つのコーナーを通過するすべての線について、同じ数の交点を持つ2つのコーナーを通過する関連する線が存在します。

結論は、問題の局所的な最大値を完全に表すセットを形成する2つの長方形のコーナーによって形成される線をチェックするだけでよいということです。それらの中から、交差が最も多いものを選びます。

時間の複雑さ

このソリューションでは、最初に2つのコーナーを通過するすべての線を回復する必要があります。そのような行の数はO(n ^ 2)です。

次に、特定の線と長方形の間の交差の数を数える必要があります。これは明らかに、各長方形と比較することによりO(n)で行うことができます。

続行するにはより効率的な方法があるかもしれませんが、このアルゴリズムはせいぜいO(n ^ 3)であることがわかっています。

Python3の実装

これはPythonこのアルゴリズムの実装です。効率よりも可読性を重視していますが、上記の定義とまったく同じです。

def get_best_line(rectangles):
    """
    Given a set of rectangles, return a line which intersects the most rectangles.
    """

    # Recover all corners from all rectangles
    corners = set()
    for rectangle in rectangles:
        corners |= set(rectangle.corners)

    corners = list(corners)

    # Recover all lines passing by two corners
    lines = get_all_lines(corners)

    # Return the one which has the highest number of intersections with rectangles
    return max(
        ((line, count_intersections(rectangles, line)) for line in lines),
        key=lambda x: x[1])

この実装では、次のヘルパーを使用します。

def get_all_lines(points):
    """
    Return a generator providing all lines generated
    by a combination of two points out of 'points'
    """
    for i in range(len(points)):
        for j in range(i, len(points)):
            yield Line(points[i], points[j])

def count_intersections(rectangles, line):
    """
    Return the number of intersections with rectangles
    """
    count = 0

    for rectangle in rectangles:
        if line in rectangle:
           count += 1

    return count

そして、長方形と線のデータ構造として機能するクラス定義があります。

import itertools
from decimal import Decimal

class Rectangle:
    def __init__(self, x_range, y_range):
        """
        a rectangle is defined as a range in x and a range in y.
        By example, the rectangle (0, 0), (0, 1), (1, 0), (1, 1) is given by
        Rectangle((0, 1), (0, 1))
        """
        self.x_range = sorted(x_range)
        self.y_range = sorted(y_range)

    def __contains__(self, line):
        """
        Return whether 'line' intersects the rectangle.
        To do so we check if the line intersects one of the diagonals of the rectangle
        """
        c1, c2, c3, c4 = self.corners

        x1 = line.intersect(Line(c1, c4))
        x2 = line.intersect(Line(c2, c3))

        if x1 is True or x2 is True \
                or x1 is not None and self.x_range[0] <= x1 <= self.x_range[1] \
                or x2 is not None and self.x_range[0] <= x2 <= self.x_range[1]:

            return True

        else:
            return False

    @property
    def corners(self):
        """Return the corners of the rectangle sorted in dictionary order"""
        return sorted(itertools.product(self.x_range, self.y_range))


class Line:
    def __init__(self, point1, point2):
        """A line is defined by two points in the graph"""
        x1, y1 = Decimal(point1[0]), Decimal(point1[1])
        x2, y2 = Decimal(point2[0]), Decimal(point2[1])
        self.point1 = (x1, y1)
        self.point2 = (x2, y2)

    def __str__(self):
        """Allows to print the equation of the line"""
        if self.slope == float('inf'):
            return "y = {}".format(self.point1[0])

        else:
            return "y = {} * x + {}".format(round(self.slope, 2), round(self.Origin, 2))

    @property
    def slope(self):
        """Return the slope of the line, returning inf if it is a vertical line"""
        x1, y1, x2, y2 = *self.point1, *self.point2

        return (y2 - y1) / (x2 - x1) if x1 != x2 else float('inf')

    @property
    def Origin(self):
        """Return the Origin of the line, returning None if it is a vertical line"""
        x, y = self.point1

        return y - x * self.slope if self.slope != float('inf') else None

    def intersect(self, other):
        """
        Checks if two lines intersect.
        Case where they intersect: return the x coordinate of the intersection
        Case where they do not intersect: return None
        Case where they are superposed: return True
        """

        if self.slope == other.slope:

            if self.Origin != other.Origin:
                return None

            else:
                return True

        Elif self.slope == float('inf'):
            return self.point1[0]

        Elif other.slope == float('inf'):
            return other.point1[0]

        Elif self.slope == 0:
            return other.slope * self.Origin + other.Origin

        Elif other.slope == 0:
            return self.slope * other.Origin + self.Origin

        else:
            return (other.Origin - self.Origin) / (self.slope - other.slope)

上記のコードの実際の例を次に示します。

rectangles = [
    Rectangle([0.5, 1], [0, 1]),
    Rectangle([0, 1], [1, 2]),
    Rectangle([0, 1], [2, 3]),
    Rectangle([2, 4], [2, 3]),
]

# Which represents the following rectangles (not quite to scale)
#
#  *
#  *   
#
# **     **
# **     **
#
# **
# **

最適なソリューションでは、3つの長方形を通過するラインを見つける必要があり、それが実際に出力するものであることがはっきりとわかります。

print('{} with {} intersections'.format(*get_best_line(rectangles)))
# prints: y = 0.50 * x + -5.00 with 3 intersections
4

次のアルゴリズムはどうでしょう:

RES = 0 // maximum number of intersections
CORNERS[] // all rectangles corners listed as (x, y) points

for A in CORNERS
    for B in CORNERS // optimization: starting from corner next to A
        RES = max(RES, CountIntersectionsWithLine(A.x, A.y, B.x, B.y))

return RES

つまり、各長方形の角から他の長方形の角に線を描き始め、交点の最大数を見つけます。 @westonで提案されているように、Aの隣のコーナーから内部ループを開始することで、同じ行を2回計算することを回避できます。

3

角度atで回転する線を考慮し、すべての長方形をこの線に投影すると、N個の線分が得られます。この線に垂直に交差する長方形の最大数は、横座標を増やし、左から右に出会う間隔の数を維持することによって端点を並べ替えることで簡単に取得できます(端点が始点か終点かを追跡します)。これは緑色で示されています。

これで、2つの長方形が2つの内部接線の間に含まれる角度ですべての線と交差します[赤の例]。そのため、考慮されるすべての「イベント」角度(つまり、カウントの変化を観察できるすべての角度)は次のようになります。 N(N-1)角度。

次に、総当たりの解決スキームは

  • すべての限界角度(それらのO(N²))、

    • 長方形を回転線に投影します(O(N)操作)。

    • オーバーラップをカウントし、最大のものを維持します(O(N Log N)でソートし、次にO(N)カウントします)。

これは、合計O(N³LogN)操作を受け取ります。

enter image description here

ソートを段階的に行うことができれば、すべての角度でソートを完全にやり直す必要がないと仮定すると、O(N³)まで下げられた複雑さを期待できます。これを確認する必要があります。


注:

線が1つの長方形のコーナーを通過するのを制限するソリューションは間違っています。四角形の4つのコーナーから別の四角形全体にくさびを描画すると、3つを通る線が存在していても、触れられない四角形全体を配置できる空のスペースが残ります。

enter image description here

2
Yves Daoust

他のすべての長方形に対する現在のコーナーの関係を4nのそれぞれの間隔ツリーに挿入するためにコーナーを少し反復するAndriy Berestovskyyのアイデアを適応させることにより、O(n^2 (log n + m))動的プログラミングメソッドを持つことができます。反復サイクル。

新しいツリーが作成しようとしているコーナーに作成されます。各四角形の4つの角について、他の各四角形を繰り返し処理します。挿入するのは、ペアの長方形の最も遠いコーナーが現在の固定コーナーを基準にして作成する弧を示す角度です。

すぐ下の例では、固定された下の長方形の角Rについて、中央の長方形のレコードを挿入するときに、Rに関連してp2からp1までの弧を示す角度を挿入します(約(37 deg, 58 deg))。次に、Rに関連して高い長方形をチェックするときに、Rに関連して(p4について)p3から(50 deg, 62 deg)までの弧をマークする角度の間隔を挿入します。

次の円弧レコードを挿入するとき、すべての交差する間隔に対してそれをチェックし、ほとんどの交差の記録を保持します。

enter image description here

(私たちの目的のために360度の円の任意の円弧は180度回転した対応物を持っているので、任意のカットオフを作成する必要があるかもしれないことに注意してください(他の洞察が歓迎されます)。たとえば、これは45度から315度は2つに分割されます:[0、45]と[135、180]。分割されていない弧はどちらか一方としか交差できませんが、どちらの方法でも、長方形が二重にならないように追加のハッシュが必要になる場合がありますカウントされます。)

1