Lấy giá trị tối đa từ một phạm vi trong mảng chưa sắp xếp


9

Tôi có một mảng chưa được sắp xếp . Tôi có các truy vấn trong đó tôi đưa ra một phạm vi và sau đó giá trị tối đa từ phạm vi đó phải được trả về. Ví dụ:

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

Tôi xây dựng thuật toán hoặc cấu trúc dữ liệu nào để nhanh chóng lấy giá trị tối đa từ bất kỳ phạm vi nào. (Có rất nhiều truy vấn)

EDIT: Đây thực sự là một phiên bản đơn giản của vấn đề thực tế. Tôi có thể có kích thước mảng lớn tới 100000 và số lượng truy vấn lên tới 100000. Vì vậy, tôi chắc chắn yêu cầu một số tiền xử lý sẽ tạo điều kiện cho phản hồi truy vấn nhanh.


5
Tại sao nó không được sắp xếp? Vấn đề là tầm thường nếu nó được sắp xếp, vì vậy cách tiếp cận rõ ràng là sắp xếp nó.

1
@delnan Nếu không có một số cơ chế bổ sung, bạn sẽ mất dấu các giá trị ban đầu trong phạm vi được truy vấn ...
Thijs van Điền

Chỉ định toàn bộ vấn đề của bạn. Nếu kiến ​​thức này (hoặc bất kỳ thông tin nào khác) quan trọng, người ta phải biết để đưa yếu tố đó vào giải pháp.

1
Tôi có đang thiếu thứ gì đó không, hay đây chỉ là vấn đề truy cập các mục từ 2 đến 6 và tìm giá trị tối đa của các yếu tố đó?
Blrfl

@Blrfl: Tôi không nghĩ bạn thiếu thứ gì, ngoại trừ phần có thể là về nhiều truy vấn. Không thực sự rõ ràng liệu có bất kỳ điểm nào trong việc xây dựng một cấu trúc làm cho các truy vấn rẻ hơn đáng kể so với tìm kiếm tuần tự hay không. (Mặc dù sẽ không có nhiều điểm khi đặt câu hỏi ở đây nếu đó không phải là ý tưởng.)
Mike Sherrill 'Cat Recall'

Câu trả lời:


14

Tôi nghĩ rằng bạn có thể xây dựng một số loại cây nhị phân trong đó mỗi nút đại diện cho giá trị tối đa con của nó:

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

Sau đó, bạn chỉ cần tìm cách xác định nút nào bạn cần kiểm tra tối thiểu để tìm giá trị tối đa trong phạm vi được truy vấn. Trong ví dụ này, để có được giá trị tối đa trong phạm vi chỉ mục [2, 6](bao gồm) bạn sẽ có max(45, 78, 4)thay vì max(9, 45, 78, 2, 4). Khi cây phát triển, mức tăng sẽ lớn hơn.


1
Để làm việc này, có thông tin bị thiếu trong cây ví dụ của bạn: Mỗi nút bên trong phải có cả mức tối đa và tổng số nút con mà nó có. Mặt khác, tìm kiếm không có cách nào để biết rằng (ví dụ) nó không phải nhìn vào tất cả các con của 78(và bỏ qua 2), bởi vì đối với tất cả, nó biết chỉ số 6nằm trong cây con đó.
Izkata


+1: Đây là một kỹ thuật mạnh mẽ để trả lời các truy vấn về các phần phụ của danh sách trong thời gian đăng nhập (N), có thể sử dụng dữ liệu tại nút gốc trong thời gian không đổi từ dữ liệu ở trẻ em.
kevin cline

Ý tưởng này thật tuyệt vời. Nó cho thời gian truy vấn O (logn). Tôi nghĩ rằng @Izkata cũng đã đưa ra một quan điểm tốt. Chúng ta có thể gia tăng nút cây với thông tin về phạm vi trái và phải mà nó bao phủ. Vì vậy, đưa ra một phạm vi, nó biết làm thế nào để chia vấn đề thành hai. Không gian khôn ngoan, tất cả các dữ liệu được lưu trữ ở cấp độ lá. Vì vậy, nó đòi hỏi không gian 2 * N, đó là O (N) để lưu trữ. Tôi không biết cây phân khúc là gì, nhưng đây có phải là ý tưởng đằng sau cây phân khúc không?
Kay

Và về mặt tiền xử lý, phải mất O (n) để xây dựng cây.
Kay

2

Để bổ sung cho câu trả lời của ngoaho91.

Cách tốt nhất để giải quyết vấn đề này là sử dụng cấu trúc dữ liệu Segment Tree. Điều này cho phép bạn trả lời các truy vấn như vậy trong O (log (n)), điều đó có nghĩa là tổng độ phức tạp của thuật toán của bạn sẽ là O (Q logn) trong đó Q là số lượng truy vấn. Nếu bạn sử dụng thuật toán ngây thơ, tổng độ phức tạp sẽ là O (Q n) rõ ràng là chậm hơn.

Tuy nhiên, có một nhược điểm của việc sử dụng Cây phân đoạn. Nó chiếm rất nhiều bộ nhớ, nhưng rất nhiều lần bạn ít quan tâm đến bộ nhớ hơn là về tốc độ.

Tôi sẽ mô tả ngắn gọn các thuật toán được sử dụng bởi DS này:

Cây phân đoạn chỉ là trường hợp đặc biệt của Cây tìm kiếm nhị phân, trong đó mọi nút giữ giá trị của phạm vi mà nó được gán. Nút gốc, được gán phạm vi [0, n]. Con trái được gán phạm vi [0, (0 + n) / 2] và con phải [(0 + n) / 2 + 1, n]. Bằng cách này, cây sẽ được xây dựng.

Tạo cây :

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

Cây truy vấn

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

Nếu bạn cần giải thích thêm, chỉ cần cho tôi biết.

BTW, Segment Tree cũng hỗ trợ cập nhật một phần tử hoặc một phạm vi phần tử trong O (log n)


sự phức tạp của việc lấp đầy cây là gì?
Pieter B

Bạn phải đi qua tất cả các yếu tố và phải O(log(n))thêm từng yếu tố vào cây. Do đó, tổng độ phức tạp làO(nlog(n))
Andrés

1

Thuật toán tốt nhất sẽ là trong thời gian O (n) như dưới đây, hãy bắt đầu, kết thúc là chỉ số giới hạn của phạm vi

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 vì chỉ lặp lại thuật toán mà OP đã cố gắng cải thiện.
kevin cline

1
+1 để đăng một giải pháp cho vấn đề đã nêu. Đây thực sự là cách duy nhất để làm điều đó nếu bạn có một mảng và không biết giới hạn nào sẽ là một tiên nghiệm . (Mặc dù tôi sẽ khởi tạo maxđến a[i]và bắt đầu forvòng lặp tại i+1.)
Blrfl

@kevincline Không chỉ nghỉ ngơi - nó còn nói "Có, bạn đã có thuật toán tốt nhất cho nhiệm vụ này", với một cải tiến nhỏ (nhảy tới start, dừng lại end). Và tôi đồng ý, đây cách tốt nhất để tra cứu một lần. Câu trả lời của @ ThijsvanDien chỉ tốt hơn nếu việc tra cứu sẽ diễn ra nhiều lần, vì mất nhiều thời gian hơn để thiết lập ban đầu.
Izkata

Cấp, tại thời điểm đăng câu trả lời này, câu hỏi không bao gồm chỉnh sửa xác nhận rằng anh ta sẽ thực hiện nhiều truy vấn trên cùng một dữ liệu.
Izkata

1

Các giải pháp dựa trên cây nhị phân / cây phân đoạn thực sự đang chỉ đúng hướng. Tuy nhiên, người ta có thể phản đối rằng họ cần rất nhiều bộ nhớ, tuy nhiên. Có hai giải pháp cho những vấn đề này:

  1. Sử dụng cấu trúc dữ liệu ngầm thay vì cây nhị phân
  2. Sử dụng cây M-ary thay vì cây nhị phân

Điểm đầu tiên là bởi vì cây có cấu trúc cao, bạn có thể sử dụng cấu trúc giống như đống để xác định ngầm định cây chứ không phải đại diện cho cây bằng các nút, con trỏ trái và phải, khoảng thời gian, vv .. Điều đó giúp tiết kiệm rất nhiều bộ nhớ không có hiệu suất - bạn không cần phải thực hiện thêm một chút số học con trỏ.

Điểm thứ hai là, với chi phí của một ít công việc hơn trong quá trình đánh giá, bạn có thể sử dụng cây M-ary thay vì cây nhị phân. Chẳng hạn, nếu bạn sử dụng cây 3 lá, bạn sẽ tính tối đa 3 phần tử cùng một lúc, sau đó 9 phần tử cùng một lúc, sau đó là 27, v.v. Dung lượng lưu trữ bổ sung cần thiết là N / (M-1) - bạn có thể chứng minh bằng cách sử dụng công thức chuỗi hình học. Nếu bạn chọn M = 11, ví dụ, bạn sẽ yêu cầu 1/10 lưu trữ của phương thức cây nhị phân.

Bạn có thể xác minh rằng các triển khai ngây thơ và được tối ưu hóa này trong Python cho kết quả giống nhau:

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

so với

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

thử cấu trúc dữ liệu "cây phân đoạn"
có 2 bước truy vấn
build_tree () O (n)
(int min, int max) O (nlogn)

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

biên tập:

Các bạn đừng đọc wiki tôi đã gửi!

thuật toán này là:
- bạn duyệt qua mảng 1 lần để xây dựng cây. O (n)
- hơn 100000000 lần tiếp theo bạn muốn biết tối đa bất kỳ phần nào của mảng, chỉ cần gọi hàm truy vấn. O (logn) cho mọi truy vấn
- c ++ triển khai ở đây geekforgeek.org/seribution-tree-set-1-range-minimum-query/
thuật toán cũ là:
mọi truy vấn, chỉ cần duyệt qua vùng đã chọn và tìm.

vì vậy, nếu bạn sẽ sử dụng thuật toán này để xử lý một lần, OK, nó chậm hơn so với cách cũ. nhưng nếu bạn sẽ xử lý số lượng lớn các truy vấn (tỷ đồng), nó rất hiệu quả, bạn có thể tạo ra tập tin văn bản như thế này, vì quá trình

dòng 1: 50000 số ngẫu nhiên 0-1.000.000, tách bằng '(space)' (đó là mảng)
dòng Số ngẫu nhiên 2: 2 từ 1 đến 50000, được chia cho '(dấu cách)' (đó là truy vấn)
...
dòng 200000: thích dòng 2, đó cũng là truy vấn ngẫu nhiên

đây là vấn đề mẫu, xin lỗi, nhưng đây là tiếng Việt
http://vn.spoj.com/probols/NKLINEUP/
nếu bạn giải quyết nó theo cách cũ, bạn không bao giờ vượt qua.


3
Tôi không nghĩ điều đó có liên quan. Một cây khoảng giữ các khoảng, không phải số nguyên và các hoạt động mà chúng cho phép trông không giống như những gì OP yêu cầu. Tất nhiên, bạn có thể tạo tất cả các khoảng có thể và lưu trữ chúng trong một cây khoảng, nhưng (1) có rất nhiều trong số chúng, vì vậy điều này không có quy mô và (2) các hoạt động vẫn không giống như OP hỏi về.

sai lầm của tôi, tôi có nghĩa là cây phân khúc, không phải cây khoảng.
ngoaho91

Thật thú vị, tôi nghĩ rằng tôi chưa bao giờ đi qua cây này! IIUC điều này vẫn yêu cầu lưu trữ tất cả các khoảng thời gian có thể, mặc dù. Tôi nghĩ rằng có O (n ^ 2) trong số đó, khá đắt. (Ngoài ra, không nên truy vấn là O (log n + k) cho kết quả k?

có, void build_tree () phải di chuyển qua mảng. và lưu trữ giá trị tối đa (hoặc tối thiểu) cho mỗi nút. nhưng trong nhiều trường hợp, chi phí bộ nhớ không quan trọng hơn tốc độ.
ngoaho91

2
Tôi không thể tưởng tượng điều này nhanh hơn một O(n)tìm kiếm đơn giản về mảng, như được mô tả trong câu trả lời của tarun_telang. Bản năng đầu tiên O(log n + k)là nhanh hơn O(n), nhưng O(log n + k)chỉ là truy xuất mảng con - tương đương với O(1)truy cập mảng được đưa ra điểm bắt đầu và điểm kết thúc. Bạn vẫn sẽ cần phải duyệt qua nó để tìm mức tối đa.
Izkata

0

Bạn có thể đạt được O (1) cho mỗi truy vấn (với cấu trúc O (n log n)) bằng cách sử dụng cấu trúc dữ liệu được gọi là bảng thưa. Đối với mỗi công suất là 2, hãy tiết kiệm tối đa cho mỗi phân đoạn có độ dài này. Bây giờ phân đoạn đã cho [l, r) bạn nhận được tối đa tối đa trên [l + 2 ^ k) và [r-2 ^ k, r) cho k thích hợp. Chúng chồng lên nhau nhưng không sao

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.