並べ替えられていない配列があります。範囲を指定し、その範囲の最大値を返さなければならないクエリがあります。例えば:
array[]={23,17,9,45,78,2,4,6,90,1};
query(both inclusive): 2 6
answer: 78
任意の範囲から最大値をすばやく取得するために構築するアルゴリズムまたはデータ構造。 (クエリがたくさんあります)
編集:これは確かに実際の問題の単純なバージョンです。配列のサイズは100000まで、クエリの数は100000までにすることができます。そのため、高速なクエリ応答を容易にする前処理が必要です。
各ノードがその子の最大値を表す、ある種のバイナリツリーを構築できると思います。
_ 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)
を使用します。ツリーが成長するにつれて、ゲインは大きくなります。
Ngoaho91の答えを補足するため。
この問題を解決する最良の方法は、セグメントツリーデータ構造を使用することです。これにより、O(log(n))でそのようなクエリに答えることができます。つまり、アルゴリズムの全体的な複雑度はO(Qlogn)になります。Qはクエリの数です。単純なアルゴリズムを使用した場合、全体の複雑さはO(Qn)となり、明らかに遅くなります。
ただし、セグメントツリーの使用には欠点があります。それは多くのメモリを消費しますが、多くの場合、速度よりもメモリを気にしません。
この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)の単一の要素または要素の範囲の更新もサポートしています
バイナリツリー/セグメントツリーベースのソリューションは、確かに正しい方向を指しています。ただし、多くの追加メモリが必要であることに反対する人もいるかもしれません。これらの問題には2つの解決策があります。
最初のポイントは、ツリーは高度に構造化されているため、ノード、左と右のポインター、間隔などでツリーを表すのではなく、ヒープのような構造を使用してツリーを暗黙的に定義できることです。パフォーマンスへの影響はありません。もう少しポインター演算を実行する必要があります。
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
vs.
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)
最良のアルゴリズムは、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;
}
スパーステーブルと呼ばれるデータ構造を使用して、クエリごとにO(1)(O(n log n)構成で))を達成できます。2のべき乗ごとに、この長さの各セグメントの最大値を保存しましょう。ここで、セグメント[l、r)を指定すると、適切なkに対して[l + 2 ^ k)および[r-2 ^ k、r)の最大値の最大値が得られます。重なっていますが大丈夫です
「セグメントツリー」データ構造を試す
2つのステップがあります
build_tree()O(n)
query(int min、int max)O(nlogn)
http://en.wikipedia.org/wiki/Segment_tree
編集:
あなたたちは私が送ったウィキを読んでいないだけです!
このアルゴリズムは:
-アレイを1回トラバースしてツリーを構築します。オン)
-配列の任意の部分の最大値を知りたい次の100000000+回、単にクエリ関数を呼び出します。 O(logn)すべてのクエリに対して
-c ++はここに実装geeksforgeeks.org/segment-tree-set-1-range-minimum-query/
古いアルゴリズムは次のとおりです:
すべてのクエリ。選択した領域をトラバースして検索します。
したがって、このアルゴリズムを使用して1回処理する場合は、古い方法よりも遅くなります。膨大な数のクエリ(10億)を処理する場合、テスト用に次のようなテキストファイルを生成できるので非常に効率的です。
行1:0から1000000までの50000の乱数、 '(スペース)'で分割(配列です)
行2:1から50000までの2つの乱数、 '(スペース)'で分割(クエリです)
...
行200000:行2と同様、ランダムクエリでもあります
これは問題の例です。申し訳ありませんが、ベトナム語です
http://vn.spoj.com/problems/NKLINEUP/
古い方法で解決すると、合格することはありません。