並べ替えられていない配列の範囲から最大値を取得しています


9

私が持っている未ソート配列を。範囲を指定し、その範囲の最大値を返さなければならないクエリがあります。例えば:

array[]={23,17,9,45,78,2,4,6,90,1};
query(both inclusive): 2 6
answer: 78

任意の範囲から最大値をすばやく取得するために構築するアルゴリズムまたはデータ構造。(クエリがたくさんあります)

編集: これは確かに実際の問題の単純なバージョンです。配列のサイズは100000まで、クエリの数は100000までにすることができます。そのため、高速なクエリ応答を促進する前処理が必要です。


5
なぜ分類されていないのですか?ソートされている場合、問題は簡単です。そのため、明らかなアプローチは、ソートすることです。

1
@delnan余分なメカニズムがないと、クエリ対象の範囲に元々あった値を追跡できません...
Thijs van Dien 2013年

問題全体を指定してください。この知識(またはその他の情報)が重要な場合は、それをソリューションに組み込むために知っておく必要があります。

1
何かが足りないのでしょうか、それとも2から6の項目にアクセスしてそれらの要素の最大値を見つけるだけの問題ですか?
Blrfl 2013年

@Blrfl:たぶん、多くのクエリに関する部分を除いて、何も不足しているとは思いません。クエリをシーケンシャル検索より大幅に安価にする構造を構築することに意味があるかどうかは、はっきりしていません。(もしそれがアイデアでなかったら、ここで質問する意味はあまりありません。)
マイクシェリル '猫のリコール'

回答:


14

各ノードがその子の最大値を表す、ある種のバイナリツリーを構築できると思います。

            78           
     45            78     
  23    45     78      6  
23 17  9 45   78 2    4 6   

次に、照会する範囲の最大値を見つけるために最低限チェックする必要があるノードを決定する方法を見つけるだけで済みます。この例では、インデックス範囲の最大値[2, 6](両端を含む)を取得するには、max(45, 78, 4)ではなくを使用しmax(9, 45, 78, 2, 4)ます。木が成長するにつれて、ゲインは大きくなります。


1
これが機能するためには、サンプルツリーから欠落している情報があります。各内部ノードには、最大値とその子ノードの総数の両方が必要です。それ以外の場合、検索では(たとえば)のすべての子を見る必要がない78(そしてをスキップする2)必要がないことを知ることができません6
イズカタ2013年

それ以外の場合は、+ 1は、これがかなり独創的であると感じた場合
イズカタ

+1:これは、log(N)時間でリストの部分範囲に関するクエリに回答するための強力な手法です。ルートノードのデータを子のデータから一定の時間で計算できる場合でも使用できます。
ケビンクライン2013年

このアイデアは素晴らしいです。O(logn)クエリ時間を提供します。@Izkataも良い点だと思います。カバーする左と右の範囲に関する情報をツリーノードに追加できます。したがって、範囲を指定すると、問題を2つに分割する方法がわかります。空間的には、すべてのデータはリーフレベルで保存されます。したがって、2 * Nのスペースが必要です。これは、格納するためのO(N)です。セグメントツリーとは何かわかりませんが、これはセグメントツリーの背後にある考え方ですか?
ケイ

また、前処理に関しては、ツリーを構築するためにO(n)が必要です。
ケイ

2

ngoaho91の答えを補足するため。

この問題を解決する最良の方法は、セグメントツリーデータ構造を使用することです。これにより、このようなクエリにO(log(n))で応答できます。つまり、アルゴリズムの全体的な複雑度はO(Q logn)となり、Qはクエリの数になります。単純なアルゴリズムを使用した場合、全体の複雑さはO(Q n)になり、明らかに遅くなります。

ただし、セグメントツリーの使用には欠点があります。それは多くのメモリを消費しますが、多くの場合、速度よりもメモリを気にしません。

このDSで使用されるアルゴリズムについて簡単に説明します。

セグメントツリーは、バイナリ検索ツリーの特殊なケースにすぎません。すべてのノードは、割り当てられた範囲の値を保持しています。ルートノードには、範囲[0、n]が割り当てられます。左の子には範囲[0、(0 + n)/ 2]が割り当てられ、右の子には[(0 + n)/ 2 + 1、n]が割り当てられます。この方法でツリーが構築されます。

ツリーを作成

/*
    A[] -> array of original values
    tree[] -> Segment Tree Data Structure.
    node -> the node we are actually in: remember left child is 2*node, right child is 2*node+1
    a, b -> The limits of the actual array. This is used because we are dealing
                with a recursive function.
*/

int tree[SIZE];

void build_tree(vector<int> A, int node, int a, int b) {
    if (a == b) { // We get to a simple element
        tree[node] = A[a]; // This node stores the only value
    }
    else {
        int leftChild, rightChild, middle;
        leftChild = 2*node;
        rightChild = 2*node+1; // Or leftChild+1
        middle = (a+b) / 2;
        build_tree(A, leftChild, a, middle); // Recursively build the tree in the left child
        build_tree(A, rightChild, middle+1, b); // Recursively build the tree in the right child

        tree[node] = max(tree[leftChild], tree[rightChild]); // The Value of the actual node, 
                                                            //is the max of both of the children.
    }
}

クエリツリー

int query(int node, int a, int b, int p, int q) {
    if (b < p || a > q) // The actual range is outside this range
        return -INF; // Return a negative big number. Can you figure out why?
    else if (p >= a && b >= q) // Query inside the range
        return tree[node];
    int l, r, m;
    l = 2*node;
    r = l+1;
    m = (a+b) / 2;
    return max(query(l, a, m, p, q), query(r, m+1, b, p, q)); // Return the max of querying both children.
}

さらに詳しい説明が必要な場合は、お知らせください。

ところで、セグメントツリーは、O(log n)内の単一の要素または要素の範囲の更新もサポートしています


ツリーを埋める複雑さは何ですか?
Pieter B

すべての要素を確認する必要がありO(log(n))、各要素がツリーに追加されるまでに時間がかかります。したがって、合計複雑さがあるO(nlog(n))
アンドレス・

1

最良のアルゴリズムは、以下のようにO(n)時間です。開始、終了を範囲の境界のインデックスとします。

int findMax(int[] a, start, end) {
   max = Integer.MIN; // initialize to minimum Integer

   for(int i=start; i <= end; i++) 
      if ( a[i] > max )
         max = a[i];

   return max; 
}

4
-1は、OPが改善しようとしていたアルゴリズムを単に繰り返すためです。
ケビンクライン2013年

1
+1は、現状の問題の解決策を投稿します。これは本当に、配列があり、境界がアプリオリになる境界がわからない場合に行う唯一の方法です。(ただし、ループをに初期化maxa[i]て開始しforますi+1。)
Blrfl 2013年

@kevinclineそれは単に言い換えるだけではありません-「はい、あなたはすでにこのタスクに最適なアルゴリズムを持っています」と言っており、少し改善されています(にジャンプ、startで停止end)。そして、私は同意します。これ 1回限りのルックアップに最適です。@ThijsvanDienの回答は、ルックアップが複数回行われる場合にのみ優れています。最初のセットアップに時間がかかるためです。
イズカタ2013年

確かに、この回答を投稿した時点での質問には、同じデータに対して多くのクエリを実行することを確認する編集は含まれていませんでした。
イズカタ2013年

1

バイナリツリー/セグメントツリーベースのソリューションは、確かに正しい方向を指しています。ただし、多くの追加メモリが必要であることに反対する人もいます。これらの問題には2つの解決策があります。

  1. バイナリツリーの代わりに暗黙的なデータ構造を使用する
  2. バイナリツリーの代わりにM-aryツリーを使用する

最初のポイントは、ツリーは高度に構造化されているため、ノード、左と右のポインター、間隔などでツリーを表すのではなく、ヒープのような構造を使用してツリーを暗黙的に定義できるということです。パフォーマンスへの影響はありません。もう少しポインター演算を実行する必要があります。

2番目のポイントは、評価中の作業が少し増える代わりに、バイナリツリーではなくM-aryツリーを使用できることです。たとえば、3進ツリーを使用する場合、一度に最大3つの要素を計算し、次に一度に9つの要素、次に27などを計算します。必要な追加のストレージはN /(M-1)です。幾何級数公式を使用して証明します。たとえば、M = 11を選択した場合、バイナリツリー法のストレージの1/10が必要になります。

これらの単純で最適化されたPythonの実装が同じ結果をもたらすことを確認できます。

class RangeQuerier(object):
    #The naive way
    def __init__(self):
        pass

    def set_array(self,arr):
        #Set, and preprocess
        self.arr = arr

    def query(self,l,r):
        try:
            return max(self.arr[l:r])
        except ValueError:
            return None

class RangeQuerierMultiLevel(object):
    def __init__(self):
        self.arrs = []
        self.sub_factor = 3
        self.len_ = 0

    def set_array(self,arr):
        #Set, and preprocess
        tgt = arr
        self.len_ = len(tgt)
        self.arrs.append(arr)
        while len(tgt) > 1:
            tgt = self.maxify_one_array(tgt)
            self.arrs.append(tgt)

    def maxify_one_array(self,arr):
        sub_arr = []
        themax = float('-inf')
        for i,el in enumerate(arr):
            themax = max(el,themax)
            if i % self.sub_factor == self.sub_factor - 1:
                sub_arr.append(themax)
                themax = float('-inf')
        return sub_arr

    def query(self,l,r,level=None):
        if level is None:
            level = len(self.arrs)-1

        if r <= l:
            return None

        int_size = self.sub_factor ** level 

        lhs,mid,rhs = (float('-inf'),float('-inf'),float('-inf'))

        #Check if there's an imperfect match on the left hand side
        if l % int_size != 0:
            lnew = int(ceil(l/float(int_size)))*int_size
            lhs = self.query(l,min(lnew,r),level-1)
            l = lnew
        #Check if there's an imperfect match on the right hand side
        if r % int_size != 0:
            rnew = int(floor(r/float(int_size)))*int_size
            rhs = self.query(max(rnew,l),r,level-1)
            r = rnew

        if r > l:
            #Handle the middle elements
            mid = max(self.arrs[level][l/int_size:r/int_size])
        return max(max(lhs,mid),rhs)

0

「セグメントツリー」データ構造を試す
2つのステップがある
build_tree()O(n)
query(int min、int max)O(nlogn)

http://en.wikipedia.org/wiki/Segment_tree

編集:

あなたたちは私が送ったウィキを読んでいないだけです!

このアルゴリズムは次のとおり
です。-アレイを1回トラバースしてツリーを構築します。O(n)
-配列の任意の部分の最大値を知りたい次の100000000+回、単にクエリ関数を呼び出します。すべてのクエリのO(logn)
-ここでc ++を実装geeksforgeeks.org/segment-tree-set-1-range-minimum-query/
古いアルゴリズム:
すべてのクエリは、選択された領域をトラバースして検索するだけです。

したがって、このアルゴリズムを使用して1回処理する場合は、古い方法よりも遅くなります。しかし、膨大な数のクエリ(10億)を処理する場合、次のようなテキストファイルを生成することは非常に効率的です。テスト

行1の場合:0-1000000から50000の乱数、 '(スペース)'で分割(
行は配列です)行2:2の1から50000までの乱数、 '(スペース)'で分割(クエリです)
...
200000行目:2行目と同様、ランダムクエリでもあります

これは問題の例です。申し訳ありませんが、ベトナム語
http://vn.spoj.com/problems/NKLINEUP/
にあります。古い方法で解決すると、合格することはありません。


3
それは関係ないと思います。間隔ツリーは整数ではなく間隔を保持し、それらが許可する操作はOPが要求するものとはまったく異なります。もちろん、可能なすべての間隔を生成して間隔ツリーに格納することもできますが、(1)指数関数的に多数あるため、これはスケーリングされず、(2)操作はまだOPのように見えませんに頼む。

私の間違いは、区間ツリーではなくセグメントツリーを意味します。
ngoaho91 2013年

興味深いことに、私はこの木に出くわしたことはありません。ただし、IIUCでは、可能なすべての間隔を保存する必要があります。それらのO(n ^ 2)があると思います。(また、kの結果に対してクエリをO(log n + k)にすべきではありませんか?

はい、void build_tree()は配列を横切る必要があります。すべてのノードの最大値(または最小値)を保存します。しかし、多くの場合、メモリコストは速度よりも重要ではありません。
ngoaho91 2013年

2
O(n)tarun_telangの回答で説明されているように、これが配列の単純な検索よりも速くなることは想像できません。最初の本能はそれO(log n + k)よりも速いですO(n)が、これO(log n + k)はサブ配列の単なる取得です- O(1)開始点と終了点を指定した配列アクセスと同等です。最大値を見つけるには、それをトラバースする必要があります。
イズカタ

0

スパーステーブルと呼ばれるデータ構造を使用して、クエリごとにO(1)(O(n log n)構成で)を実現できます。2の累乗ごとに、この長さの各セグメントの最大値を節約しましょう。ここで、セグメント[l、r)を指定すると、適切なkに対して[l + 2 ^ k)と[r-2 ^ k、r)の最大値の最大値が得られます。重なっていますが大丈夫です

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