정렬되지 않은 배열의 범위에서 최대 값 검색


9

나는이 정렬되지 않은 배열을 . 범위를 제공 한 다음 해당 범위의 최대 값을 반환 해야하는 쿼리가 있습니다. 예를 들면 다음과 같습니다.

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

어떤 범위에서든 최대 값을 신속하게 검색하기 위해 어떤 알고리즘 또는 데이터 구조를 구성해야합니까? (많은 검색어가 있습니다)

편집 : 이것은 실제로 실제 문제의 간단한 버전입니다. 배열 크기는 최대 100000이고 최대 쿼리 수는 최대 100,000입니다. 따라서 빠른 쿼리 응답을 용이하게하는 사전 처리가 필요합니다.


5
왜 분류되지 않습니까? 정렬 된 경우 문제는 사소한 것이므로 명확한 접근 방식은 정렬하는 것입니다.

1
@delnan 별도의 메커니즘이 없으면 어떤 값이 원래 쿼리 될 범위에 있었는지 추적 할 수 없습니다.
Thijs van Dien

전체 문제를 지정하십시오. 이 지식 (또는 다른 정보)이 중요하다면, 그 정보를 솔루션에 포함시켜야합니다.

1
내가 누락 된 것이거나 2-6 항목을 방문하여 해당 요소의 최대 값을 찾는 것이 문제입니까?
Blrfl

@ Blrfl : 많은 쿼리 에 대한 부분을 제외하고는 아무것도 누락되지 않았다고 생각 합니다. 쿼리를 순차 검색보다 실질적으로 저렴하게 만드는 구조를 구축하는 데 어떤 점이 있는지는 확실하지 않습니다. (그것이 아이디어가 아니라면 여기에 질문을 할 때 많은 의미가 없지만)
Mike Sherrill 'Cat Recall'5

답변:


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.
이즈 카타

그렇지 않으면, 내가 다소 독창적이라고 생각하는 +1
Izkata

+1 : 이것은 루트 노드의 데이터가 자식의 데이터에서 일정한 시간에 계산 될 수 있더라도 log (N) 시간의 목록 하위 범위에 대한 쿼리에 응답하는 강력한 기술입니다.
케빈 클라인

이 아이디어는 굉장합니다. O (logn) 쿼리 시간을 제공합니다. @Izkata도 좋은 지적을했다고 생각합니다. 왼쪽 및 오른쪽 범위에 대한 정보로 트리 노드를 보강 할 수 있습니다. 따라서 범위가 주어지면 문제를 두 가지로 나누는 방법을 알고 있습니다. 공간적으로 모든 데이터는 리프 수준에서 저장됩니다. 따라서 저장하려면 O (N) 인 2 * N 공간이 필요합니다. 세그먼트 트리가 무엇인지 모르지만 이것이 세그먼트 트리의 기본 개념입니까?
Kay

전처리의 관점에서 트리를 구성하는 데 O (n)이 필요합니다.
Kay

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

추가 설명이 필요하면 알려주십시오.

BTW, 세그먼트 트리는 단일 요소 또는 O (log n)의 요소 범위 업데이트도 지원합니다.


나무를 채우는 복잡성은 무엇입니까?
Pieter B

모든 요소를 ​​살펴보고 O(log(n))각 요소를 트리에 추가 하는 데 소요 됩니다. 따라서 전체 복잡성은O(nlog(n))
Andrés

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
OP가 개선하려는 알고리즘을 반복하기 만하면 -1입니다.
케빈 클라인

1
설명 된 문제에 대한 솔루션을 게시 한 경우 +1입니다. 당신이 배열을 가지고 경계가 될 것 무엇을 모른다면 이건 정말 그것을 할 수있는 유일한 방법입니다 선험적 . (I 초기화 것이지만 maxa[i]하고 시작 for에서 루프를 i+1.)
Blrfl

@kevincline 그냥 쉬는 것이 아니라 "예, 이미이 작업에 가장 적합한 알고리즘을 가지고 있습니다"라는 말이 약간 개선되었습니다 (점프 start, 멈춤 end). 그리고 나는이 동의 하다 한 번 조회를위한 최고의. @ThijsvanDien의 답변은 처음에 설정하는 데 시간이 오래 걸리기 때문에 조회가 여러 번 발생할 경우에만 더 좋습니다.
이즈 카타

물론이 답변을 게시 할 때 동일한 데이터에 대해 많은 쿼리를 수행 할 것임을 확인하는 편집 내용에는 질문에 포함되지 않았습니다.
이즈 카타

1

이진 트리 / 세그먼트 트리 기반 솔루션은 실제로 올바른 방향을 가리키고 있습니다. 그러나 많은 추가 메모리가 필요하다고 반대 할 수도 있습니다. 이러한 문제에 대한 두 가지 해결책이 있습니다.

  1. 이진 트리 대신 암시 적 데이터 구조를 사용하십시오.
  2. 이진 트리 대신 M-ary 트리를 사용하십시오.

첫 번째 요점은 트리가 고도로 구조화되어 있기 때문에 노드, 왼쪽 및 오른쪽 포인터, 간격 등으로 트리를 나타내는 대신 힙과 같은 구조를 사용하여 트리를 암시 적으로 정의 할 수 있다는 것입니다. 성능이 저하되지 않습니다. 포인터 연산을 조금 더 수행해야합니다.

두 번째 요점은 평가하는 동안 약간의 작업 비용이 들지만 이진 트리 대신 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)

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 (
로그온)-C ++ 구현 여기 geeksforgeeks.org/segment-tree-set-1-range-minimum-query/
이전 알고리즘은
모든 쿼리입니다. 선택한 영역을 통과하고 찾으십시오.

따라서이 알고리즘을 사용하여 한 번만 처리하면 예전보다 느립니다. 그러나 많은 수의 쿼리 (십억)를 처리 할 경우 테스트

라인 1에서 0-1000000에서 50000의 난수를 '(space)'(배열)
줄로 분할하여 텍스트 파일을 생성하는 것이 매우 효율적입니다 2 : 1에서 50000까지의 2 개의 난수, '(space)'로 나눕니다 (질문입니다)
...
200000 줄 : 2 줄을 좋아합니다.

이것은 예제 문제이지만, 죄송하지만 이것은
http://vn.spoj.com/problems/NKLINEUP/
에 있습니다. 옛날 방식으로 해결하면 결코 지나치지 않습니다.


3
나는 그것이 관련이 있다고 생각하지 않습니다. 인터벌 트리는 정수가 아닌 인터벌을 보유하며, 허용되는 오퍼레이션은 OP가 요구하는 것과는 다르게 보입니다. 물론 가능한 모든 간격을 생성하여 간격 트리에 저장할 수 있지만 (1) 기하 급수적으로 많은 수가 있으므로 확장되지 않으며 (2) 작업이 여전히 OP처럼 보이지 않습니다. 묻습니다.

내 실수는 구간 트리가 아닌 세그먼트 트리를 의미합니다.
ngoaho91

흥미롭게도 나는이 나무를 본 적이 없다고 생각합니다! IIUC 그래도 여전히 가능한 모든 간격을 저장해야합니다. 나는 그것들의 O (n ^ 2)가 있다고 생각 합니다. (또한 k 개의 결과에 대해 O (log n + k)로 쿼리하지 않아야 합니까?

예, void build_tree ()는 배열을 가로 질러 이동해야합니다. 모든 노드에 대해 최대 (또는 최소) 값을 저장합니다. 그러나 많은 경우에 메모리 비용은 속도보다 중요하지 않습니다.
ngoaho91

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.